Integration Testing
In an earlier section of this tutorial we looked at unit testing, but integration tests are at least as important, probably more so, as they exercise the entire application from an end-users perspective, rather than an individual part.
We don’t write integration tests using Selenium or similar, and so avoid the fragility and maintenance effort that such tests often entail. Instead, the framework provides the WrapperFactory domain service which simulates the user interface in a type-safe way. Another term sometimes used is subcutaneous testing; we execute the test "under the skin".
Ex 9.1: Testing bookVisit using an integtest
In this exercise we’ll test the bookVisit mixin action (on Pet entity).
Tasks
-
in the
pom.xmlof the visits module, add the following dependencies:module-visits/pom.xml<dependency> <groupId>org.apache.causeway.testing</groupId> <artifactId>causeway-testing-integtestsupport-applib</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.causeway.persistence</groupId> <artifactId>causeway-persistence-jpa-eclipselink</artifactId> <scope>test</scope> </dependency> -
add an abstract class
VisitModuleIntegTestAbstractfor thevisitsmodule, for other integ tests to subclass:VisitModuleIntegTestAbstract.java@SpringBootTest( classes = VisitModuleIntegTestAbstract.TestApp.class ) @ActiveProfiles("test") public abstract class VisitModuleIntegTestAbstract extends CausewayIntegrationTestAbstractWithFixtures { @SpringBootConfiguration @EnableAutoConfiguration @Import({ CausewayModuleCoreRuntimeServices.class, CausewayModuleSecurityBypass.class, CausewayModulePersistenceJpaEclipselink.class, CausewayModuleTestingFixturesApplib.class, VisitModule.class (1) }) @PropertySources({ @PropertySource(CausewayPresets.H2InMemory_withUniqueSchema), @PropertySource(CausewayPresets.UseLog4j2Test), }) public static class TestApp { } }1 Most of this class is boilerplate, but it does reference the module under test. -
also update the
application-test.ymlfile for thevisitmodule, to ensure that the database schemas for both modules are created:module-visit/src/test/resources/application-test.ymlcauseway: persistence: schema: auto-create-schemas: petowner,visit -
add a class
Bootstrap_IntegTestintegration test, inheriting from the `VisitsModuleIntegTestAbstract:Bootstrap_IntegTest.javapublic class Bootstrap_IntegTest extends VisitModuleIntegTestAbstract { @Test public void checks_can_bootstrap() {} }Make sure this test runs and passes in both the IDE and using "mvn clean install".
Now we can write our actual test:
-
Now add a class
PetOwner_bookVisit_IntegTestintegration test, also inheriting from the `VisitModuleIntegTestAbstract:PetOwner_bookVisit_IntegTest.javapublic class PetOwner_bookVisit_IntegTest extends VisitModuleIntegTestAbstract { @BeforeEach void setup() { fixtureScripts.run(new PetOwner_persona.PersistAll()); (1) } @Test public void happy_case() { // given PetOwner somePetOwner = fakeDataService.enums() (2) .anyOf(PetOwner_persona.class) .findUsing(serviceRegistry); Pet somePet = fakeDataService.collections() .anyOf(somePetOwner.getPets()); List<Visit> before = visitRepository.findByPetOwner(somePetOwner); assertThat(before).isEmpty(); // when LocalDateTime visitAt = clockService.getClock().nowAsLocalDateTime() (3) .plusDays(fakeDataService.ints().between(1, 3)); wrapMixin(PetOwner_bookVisit.class, somePetOwner).act(somePet, visitAt); (4) // then List<Visit> after = visitRepository.findByPetOwner(somePetOwner); assertThat(after).hasSize(1); Visit visit = after.get(0); assertThat(visit.getPet()).isSameAs(somePet); (5) assertThat(visit.getPet().getPetOwner()).isSameAs(somePetOwner); (6) assertThat(visit.getVisitAt()).isEqualTo(visitAt); (6) } @Inject FakeDataService fakeDataService; @Inject VisitRepository visitRepository; @Inject ClockService clockService; }1 uses same fixture script used for prototyping to set up Pets and theirPetOwners.2 uses the FakeDataService to select a random PetOwnerand correspondingPet3 sets up some randomised but valid argument values 4 invokes the action, using the WrapperFactory to simulate the UI 5 asserts that one new Visithas been created for thePet.6 asserts that the state of this new Visitis correctRun the test and check that it passes.
-
write an error scenario which checks that the
visitAtdate cannot be in the past:PetOwner_bookVisit_IntegTest.java@Test public void cannot_book_in_the_past() { // given PetOwner somePetOwner = fakeDataService.enums() .anyOf(PetOwner_persona.class) .findUsing(serviceRegistry); Pet somePet = fakeDataService.collections() .anyOf(somePetOwner.getPets()); // when, then LocalDateTime visitAt = clockService.getClock().nowAsLocalDateTime(); assertThatThrownBy(() -> wrapMixin(PetOwner_bookVisit.class, somePetOwner).act(somePet, visitAt) ) .isInstanceOf(InvalidException.class) .hasMessage("Must book in the future"); }
Ex 9.2: Adds fixture for Visits
Currently we have a fixture for PetOwners and their Pets, but none for Visits.
If we want to write additional integration tests for Visits also, then it’s a good idea to have some fixtures.
They can also be used when prototyping.
In this exercise we’ll therefore add a new fixture for the visit module.
Tasks
-
copy in the following persona enum (we’ll add the
Buildernext):Visit_persona.java/** * Returns the most recent Visit, or the one scheduled. */ @RequiredArgsConstructor public enum Visit_persona implements Persona<Visit, Visit_persona.Builder> { JAMAL_VISITS(PetOwner_persona.JAMAL), CAMILA_VISITS(PetOwner_persona.CAMILA), ARJUN_VISITS(PetOwner_persona.ARJUN), NIA_VISITS(PetOwner_persona.NIA), OLIVIA_VISITS(PetOwner_persona.OLIVIA), LEILA_VISITS(PetOwner_persona.LEILA), MATT_VISITS(PetOwner_persona.MATT), BENJAMIN_VISITS(PetOwner_persona.BENJAMIN), JESSICA_VISITS(PetOwner_persona.JESSICA), DANIEL_VISITS(PetOwner_persona.DANIEL); private final PetOwner_persona petOwner_p; @Override public Builder builder() { return new Builder().setPersona(this); } @Override public Visit findUsing(final ServiceRegistry serviceRegistry) { final var owner = petOwner_p.findUsing(serviceRegistry); final var visits = serviceRegistry.lookupService(VisitRepository.class) .map(x -> x.findByPetOwner(owner)) .orElseThrow(); return lastOf(visits); } private static Visit lastOf(List<Visit> visits) { return visits.get(visits.size()-1); } // ... } -
now add in the
Builder:Visit_persona.java@RequiredArgsConstructor public enum Visit_persona implements Persona<Visit, Visit_persona.Builder> { // ... @Accessors(chain = true) public static class Builder extends BuilderScriptWithResult<Visit> { @Getter @Setter private Visit_persona persona; @Override protected Visit buildResult(final ExecutionContext ec) { final var petOwner = ec.executeChildT(this, persona.petOwner_p); (1) petOwner.getPets().forEach(pet -> { // in the past final var numVisits = fakeDataService.ints().between(2, 4); (2) for (var i = 0; i < numVisits; i++) { final var daysAgo = fakeDataService.ints().between(5, 500); final var minsInTheDay = randomAppointmentTime(); final var appointmentTime = randomAppointmentTimeFromToday(-daysAgo, minsInTheDay); wrapperFactory.wrapMixin(PetOwner_bookVisit.class, petOwner, SyncControl.control().withSkipRules()).act(pet, appointmentTime); } // in the future if (fakeDataService.booleans().coinFlip()) { (3) final var daysAhead = fakeDataService.ints().between(1, 10); final var minsInTheDay = randomAppointmentTime(); final var appointmentTime = randomAppointmentTimeFromToday(daysAhead, minsInTheDay); wrapperFactory.wrapMixin(PetOwner_bookVisit.class, petOwner, SyncControl.control().withSkipRules()).act(pet, appointmentTime); } }); final var numDaysAgo = fakeDataService.ints().between(2, 100); final var lastVisit = clockService.getClock().nowAsLocalDate().minusDays(numDaysAgo); petOwner.setLastVisit(lastVisit); final var visits = wrapperFactory.wrapMixin(PetOwner_visits.class, petOwner, SyncControl.control().withSkipRules()).coll(); return lastOf(visits); } private LocalDateTime randomAppointmentTimeFromToday(int days, int appointmentTime) { return clockService.getClock().nowAsLocalDate().atStartOfDay().plusDays(days).plusMinutes(appointmentTime); } private int randomAppointmentTime() { return (9 * 60) + (fakeDataService.ints().between(0, 32) * 15); } // -- DEPENDENCIES @Inject ClockService clockService; @Inject FakeDataService fakeDataService; } }1 Using the supplied ExecutionContext, we can execute any prerequisites fixtures (in this case to obtain the correspondingPetOwnerandPets)2 we create between 2 and 4 Visits in the past for eachPet.3 we create a Visitin the future for approximately every otherPet. -
add in the
PersistAll:Visit_persona.java@RequiredArgsConstructor public enum Visit_persona implements Persona<Visit, Visit_persona.Builder> { // ... public static class PersistAll extends PersonaEnumPersistAll<Visit, Visit_persona, Builder> { public PersistAll() { super(Visit_persona.class); } } } -
update the top-level
DomainAppDemofixture to create visits rather than petowners/pets:DomainAppDemo.javapublic class DomainAppDemo extends FixtureScript { @Override protected void execute(final ExecutionContext ec) { ec.executeChildren(this, moduleWithFixturesService.getTeardownFixture()); ec.executeChild(this, new Visit_persona.PersistAll()); (1) } @Inject ModuleWithFixturesService moduleWithFixturesService; }1 Because Visit_personaautomatically creates its prereqs, there’s no need to run thePetOwner_personafixture.Currently the
PetOwner_personaisn’t rerunnable, so we have to take some care. However, it’s easy to refactor if we wanted to:PetOwner_persona.javaval petOwner = serviceRegistry.lookupService(PetOwners.class) .map(x -> x.findByNameExact(persona.name)) .orElseGet( () -> petOwners.create(persona.name, null, null, null) );Note that the above change isn’t one of the exercises.