Test a ViewModel
A ViewModel is a plain Java class, so you test it like one. The tests need no Vaadin environment and no Spring context, and they run in milliseconds. This guide tests the ViewModel of the ticket triage example.
|
Tip
|
Part of a Series
This is part 3 of the Structure Views with MVVM series. It tests the ViewModel from Write a ViewModel.
|
Write Plain JUnit Tests
Put the test in the package of the ViewModel, under src/test/java, because the ViewModel is package-private. Create the ViewModel with new, and pass it the application service, or a fake or a mock of it. Then call commands as the view would, and read the state with peek():
Source code
TicketsViewModelTest.java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class TicketsViewModelTest {
TicketService service;
TicketsViewModel viewModel;
Ticket printer;
Ticket vpn;
@BeforeEach
void setUp() {
service = new TicketService();
viewModel = new TicketsViewModel(service);
viewModel.refresh(); 1
printer = viewModel.tickets().peek().get(0);
vpn = viewModel.tickets().peek().get(2);
}
@Test
void searchNarrowsTheList() {
viewModel.search("vpn");
assertEquals(1, viewModel.tickets().peek().size());
assertEquals("VPN drops hourly",
viewModel.tickets().peek().getFirst().title());
}
@Test
void openingATicketIsNotAnUnsavedChange() {
viewModel.select(printer);
assertTrue(viewModel.editing().peek());
assertFalse(viewModel.dirty().peek());
assertFalse(viewModel.canSave().peek());
}
@Test
void blankTitleCannotBeSaved() {
viewModel.select(printer);
viewModel.setTitle(" ");
assertEquals("Title is required", viewModel.titleError().peek());
assertFalse(viewModel.canSave().peek());
assertFalse(viewModel.save()); 2
}
@Test
void switchingTicketsWithUnsavedChangesAsksFirst() {
viewModel.select(printer);
viewModel.setTitle("Printer on fire again");
viewModel.select(vpn);
assertTrue(viewModel.confirmingDiscard().peek());
assertEquals("Printer on fire again", viewModel.title().peek());
}
@Test
void keepingTheChangesStaysOnTheTicket() {
viewModel.select(printer);
viewModel.setTitle("Printer on fire again");
viewModel.select(vpn);
viewModel.keepEditing();
assertFalse(viewModel.confirmingDiscard().peek());
assertEquals(printer, viewModel.selected().peek()); 3
assertEquals("Printer on fire again", viewModel.title().peek());
}
@Test
void discardingTheChangesMovesOn() {
viewModel.select(printer);
viewModel.setTitle("Printer on fire again");
viewModel.select(vpn);
viewModel.discardChanges();
assertFalse(viewModel.confirmingDiscard().peek());
assertEquals(vpn, viewModel.selected().peek());
assertEquals("VPN drops hourly", viewModel.title().peek());
assertEquals("Printer on fire",
service.search("printer", null).getFirst().title()); 4
}
}-
Loads the tickets, as the view does after navigation.
-
A command that refuses to act reports it, so the test checks the outcome as well as the state.
-
The grid’s selection follows this signal, so the test also covers what the user sees in the list.
-
The test checks the Model too: discarding the changes didn’t save them.
Each test states one rule of the view without naming a UI component, and its name says which rule. Test every rule: the derived signals, the guards, and the outcomes of the commands.
Read state with peek() in tests. Calling get() outside a computed signal or an effect throws an IllegalStateException.
Test Each Rule Once
The ViewModel tests prove the rules. They can’t prove that the view binds the right field to the right signal, or that a button calls the right command. Browserless tests of the view cover that: they click and type in the view as a user does, and check what the user sees. See Browserless Tests for how to write them.
Write a browserless test for each use case of the view, such as editing and saving a ticket, or keeping the changes when the user switches tickets. Don’t repeat every rule at both levels. When a rule changes, only its ViewModel test should have to change.
Next, edit data without Binder.