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.xml
of 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
VisitModuleIntegTestAbstract
for thevisits
module, 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.yml
file for thevisit
module, 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_IntegTest
integration 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_IntegTest
integration 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 Pet
s and theirPetOwner
s.2 uses the FakeDataService to select a random PetOwner
and correspondingPet
3 sets up some randomised but valid argument values 4 invokes the action, using the WrapperFactory to simulate the UI 5 asserts that one new Visit
has been created for thePet
.6 asserts that the state of this new Visit
is correctRun the test and check that it passes.
-
write an error scenario which checks that the
visitAt
date 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 Visit
s
Currently we have a fixture for PetOwner
s and their Pet
s, but none for Visit
s.
If we want to write additional integration tests for Visit
s 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
Builder
next):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 correspondingPetOwner
andPet
s)2 we create between 2 and 4 Visit
s in the past for eachPet
.3 we create a Visit
in 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
DomainAppDemo
fixture 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_persona
automatically creates its prereqs, there’s no need to run thePetOwner_persona
fixture.Currently the
PetOwner_persona
isn’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.