Testing Spring Boot application in post-JUnit 4 world
The document discusses the challenges and alternatives in testing Spring Boot applications, highlighting issues with legacy frameworks like JUnit 4 and introducing newer frameworks such as Spock and JUnit 5. It covers the features and drawbacks of these frameworks, their integration with Spring, and provides insights on Spring Boot testing strategies, including autoconfiguration and mocking. The content is rich with examples and explanations aimed at improving test readability, manageability, and effectiveness in a modern development environment.
Agenda
1. What problemswe are facing with legacy testing frameworks;
2. Alternative frameworks - Spock and JUnit 5;
3. Spring Boot testing support under the hood;
4. Demo
TestNG issues
Test classlifecycle:
@BeforeMethod
private void beforeMethod() {
objectUnderTest = null;
MockitoAnnotations.initMocks(this);
….
}
*Funnily enough you can use the same lifecycle for JUnit 5 with
@TestInstance: enum Lifecycle PER_CLASS, PER_METHOD;
8.
Issues with JUnit4
Not possible to have several @RunWith(imagine you want
MockitoJUnitRunner and SpringRunner)
Spock: based onJUnit runner
No hassle with the runner: it even extends Junit runner so it can run by
the tools you used for your tests before.
@RunWith(Sputnik.class)
public abstract class Specification extends MockingApi
….
public class Sputnik extends Runner implements Filterable, Sortable
12.
Spock: formal semantics
JUnittests lack formal semantics
The number one reason for using Spock is to make your tests more
readable.
Spock: test example
def"should fetch Bob and Alice without errors"() {
given:
def response =
mockMvc.perform(MockMvcRequestBuilders.get("/bookings/$id")).andReturn().response
def content = new JsonSlurper().parseText(response.contentAsString)
expect:
response.status == OK.value()
and:
content.passengerName == result
where:
id || result
'5' || "Bob"
'15' || "Alice"
}
15.
Spock: Data Pipesand Tables
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
where:
a << [3, 7, 0]
b << [5, 0, 0]
c << [5, 7, 0]
Spock: error reporting
Niceand layered: Condition not satisfied:
content.passengerName == result
| | | |
| Bob | Bob1
| false
| 1 difference (75% similarity)
| Bob(-)
| Bob(1)
18.
Spock: Interactions
A wayto express which method invocations are expected to occur:
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
19.
Spock: Interactions -powerful but should be
used at your own risk
1 * subscriber./r.*e/("hello") //method name starts with 'r' and ends in 'e')
1 * subscriber.receive(_) // any single argument (including null)
1 * subscriber.receive(*_) // any argument list (incl. the empty argument
list)
1 * _._ // any method call on any mock object
1 * _ // shortcut for and preferred over the above
Groovy “gems”
Everything associatedwith dynamic typing/duck typing should be treated
with extra care:
https://stackoverflow.com/questions/9072307/copy-groovy-class-properties/9072974#9072974
Spring Test: IntegrationTesting
Spring integration testing support has the following primary goals:
● To manage Spring IoC container caching between test execution.
● To provide Dependency Injection of test fixture instances.
● To provide transaction management appropriate to integration
testing.
● To supply Spring-specific base classes that assist developers in
writing integration tests.
43.
Spring Test: howare transactions handled?
By default, the framework will create and roll back a transaction for each
test.
If a test method deletes the contents of selected tables while running
within the transaction managed for the test, the transaction will rollback by
default, and the database will return to its state prior to execution of
the test.
44.
Spring Test: howare transactions handled?
By default, the framework will create and roll back a transaction for each
test.
If a test method deletes the contents of selected tables while running
within the transaction managed for the test, the transaction will rollback by
default, and the database will return to its state prior to execution of
the test.
*More flexibility is available via @Commit and @Rollback
45.
Spring Test: howare transactions handled?
By default, the framework will create and roll back a transaction for each
test.
If a test method deletes the contents of selected tables while running
within the transaction managed for the test, the transaction will rollback by
default, and the database will return to its state prior to execution of
the test.
*Not always that straightforward. Probably depends on implicit
commits and DB engines. Will also only work for MOCK transport.
46.
Spring Test: mainactors
TestContextManager TestContext
TestExecutionListeners
Spring Test: factoriesfor ExecutionListener
Will scan your test class, look at its annotations and add its logic
accordingly.
org.springframework.test.context.TestExecutionListener =
org.springframework.test.context.web.ServletTestExecutionListener,
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,
org.springframework.test.context.support.DependencyInjectionTestExecutionListener,
org.springframework.test.context.support.DirtiesContextTestExecutionListener,
org.springframework.test.context.transaction.TransactionalTestExecutionListener,
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener
50.
Spring Test: staticcache
The Spring TestContext framework stores application contexts in a static
cache. This means that the context is literally stored in a static variable. In
other words, if tests execute in separate processes the static cache
will be cleared between each test execution, and this will effectively
disable the caching mechanism.
51.
Spring Boot +Spock
Detached mocks via the DetachedMockFactory and
SpockMockFactoryBean classes.
class TestConfigurationForSpock {
private final detachedMockFactory = new DetachedMockFactory()
@Bean
BookingService bookingService() {
detachedMockFactory.Mock(BookingService);
}
}
Then just use: @Import(TestConfigurationForSpock)
52.
Spring Boot +Spock: Compilation/Build
caveat
GMavenPlus plugin is far from being perfect.
Check https://github.com/groovy/GMavenPlus/issues/ for issues reports.
53.
Spring Boot +JUnit 5
Will need to include an additional library to use JUnit 5 with Spring
Framework 4.3
54.
Spring Boot +JUnit 5
There’s nothing at all Spock specific in spring-test, but you can find
junit.jupiter is there:
55.
Spring Boot +JUnit 5
Be careful with your surefire version:
56.
Spring Boot +JUnit 5: Compilation/Build
caveat
Not a single jar any more. If you can’t migrate all tests at once you will
need to have both junit 4 and junit 5 on your test classpath.
57.
Spock vs JUnit5: integration with Spring
Both frameworks provide a class with exact same name:
SpringExtension.
However, their name is the only thing they have in common.
58.
Spock vs JUnit5: integration with Spring
SpringExtension
Spock JUnit 5
Tries to find any Spring-specific annotation on a test
class(spec): ContentHierarchy, BootstrapWith,
ContextConfiguration.
Using Spring-provided MetaAnnotaionUtils, so that it
can traverse class hierarchy.
Spring Test SpringExtention implements all possible
JUnit 5 callbacks(.e.g.: BeforeAllCallback,
AfterAllCallback)
If found, creates a TestContextManager and
delegate to it.
Will either get TestContextManager from a store of
create one.
Attaches Spock-specific listener for managing
Mocks:
testContext.setAttribute(MOCKED_BEANS_LIST,
mockedBeans);
Will wrap TestContextManager calls and delegate to
it. E.g.:
public void beforeAll(ExtensionContext context) {
getTestContextManager(context).beforeTestClass();
59.
Spring Boot: autoconfigureslicing
The spring-boot-test-autoconfigure module includes a number of
annotations that can be used to automatically configure different ‘slices’ of
your app for testing purposes.
Examples:
● @WebMvcTest
● @JsonTest
● @DataJpaTest
● @JdbcTest
● @DataMongoTest
● ...
60.
Spring Boot: autoconfigureslicing
@*Test would normally contain several @AutoConfigure* annotations:
For example:
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest
61.
Spring Boot: autoconfigureslicing
To tweak @*Test mechanism, you can use a corresponding
@AutoConfigure* annotation.
For example:
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
public class ExampleRepositoryTests {
// ...
}
62.
Spring Boot: autoconfigureslicing
Have you noticed this relatively unremarkable meta annotation:
@OverrideAutoConfiguration(enabled = false)
It will effectively set:
spring.boot.enableautoconfiguration=false
By applying ContextCustomizer to ContextLoader
String Boot: Bootstrapping
AsTestContextManager is the main entry point for test frameworks; all they
need is to create one:
public TestContextManager(TestContextBootstrapper testContextBootstrapper) {
this.testContext = testContextBootstrapper.buildTestContext();
registerTestExecutionListeners(testContextBootstrapper.getTestExecutionList
eners());
}
66.
Spring Boot: Slicing
Donot litter the application’s main class with configuration settings that
are specific to a particular area of its functionality.
Extract them into specific @Configuration instead.
@SpringBootApplication
@EnableBatchProcessing - DO NOT DO IT THIS WAY
public class SampleApplication { ... }
67.
Spring Boot: Customcomponent scanning
If your application may resemble the following code:
@SpringBootApplication
@ComponentScan({ "com.example.app", "org.acme.another" }) - ALSO BAD
public class SampleApplication { ... }
This effectively overrides the default component scan directive with the
side effect of scanning those two packages regardless of the slice that
you’ve chosen.
68.
Spring Boot: TypeExcludeFilter
AutoConfigurationExcludeFilter- tells Spring Boot to exclude scanning
autoconfigurations. To use SpringFactoriesLoader instead.
TypeExcludeFilter - is an interesting case. While it’s in spring-boot jar, the doc
actually says: primarily used internally to support spring-boot-test.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
69.
Spring Boot: TypeExcludeFilter
Pivotalguys care about your application’s tests so much, they put a
test-specific logic into their main Spring Boot module!
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
@TestConfiguration :
● Unlikea nested @Configuration class which would be used instead of
your application’s primary configuration, a nested @TestConfiguration
class will be used in addition to your application’s primary
configuration;
● When placed on a top-level class, @TestConfiguration indicates that
classes in src/test/java should not be picked up by scanning. Use
explicit @Import to use them.
Spring Boot: overriding configurations
72.
Spring Boot: Mockingand spying beans
Spring Boot includes a @MockBean annotation that can be used to define
a Mockito mock for a bean inside your ApplicationContext.
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTests {
@MockBean
private RemoteService remoteService;
Spring Boot: WebMvcTest
@WebMvcTest
Onlyweb layer is instantiated, not the whole context.
Often @WebMvcTest will be limited to a single controller and used in
combination with @MockBean to provide mock implementations for
required collaborators.
Spring Boot: WebMvcTestvs
AutoConfigureMockMvc
You’ll notice that both WebMvcTest and AutoConfigureMockMvc have a
@ImportAutoConfiguration, but there’s no entry in spring.factories for
WebMvcTest. By design it allows for extensibility. You can add your
own WebMvcTest entry.
In general, WebMvcTest has a lot more configurations but also contains
exclude filter to narrow down it’s search to only web-related components.
77.
Spring Test: Reactivesupport
The WebTestClient builds on the mock request and response to provide
support for testing WebFlux applications without an HTTP server.
78.
Testing reactive apps:same concepts with
different names
Non-reactive Reactive
@SpringBootTest @SpringBootTest
@WebMvcTest @WebFluxTest
@AutoConfigureMockMvc @AutoConfigureWebTestClient
If you wantto understand your reactive code
inside out:
@Test
public void testSteppingThroughFlux() {
Flux<Long> flux = Flux.just(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L);
StepVerifier.create(flux)
.expectNext(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L)
.expectComplete()
.verify();
81.
Spring Boot: SpringBootTest
RANDOM_PORTWebEnvironment
@SpringBootTest(classes = RamlBasedProducerApplication, webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class BookingServiceImplSpockTest extends Specification {
@LocalServerPort
private int port;
@Autowired
MockMvc mockMvc;
82.
Spring Boot: Testutilities
● EnvironmentTestUtils(addEnvironment(env, "org=Spring",
"name=Boot"))
● TestRestTemplate(behave in a test-friendly way by not throwing
exceptions on server-side errors)
● MockRestServiceServer(part of Spring Test)
83.
Demo
Let’s take alook at a set of CRUD tests and re-write them
using JUnit 5, Spock and different Spring Boot/Spring
Framework test features.
https://github.com/yuranos/raml-based-producer-application
https://github.com/yuranos/raml-based-consumer-application
(unit_testing branch)
#4 There are a lot of presentations about JUnit, for example, but it doesn’t bring most people closer to a real adoption.
This presentation is to explain the effort and the risks in migration to a newer testing framework.
#12 When Spring Test docs say “use with JUnit runner”, you actually don’t need it when you use Spock.
#44 Your tests might run against a real database. It’s rare, but possible. We are talking about Integration tests after all.
If you want a transaction to commit — unusual, but occasionally useful when you want a particular test to populate or modify the database — the TestContext framework can be instructed to cause the transaction to commit instead of roll back via the@Commit annotation.
merge() will only care about an auto-generated id(tested on IDENTITY and SEQUENCE) when a record with such an id already exists in your table. In that case merge() will try to update the record. If, however, an id is absent or is not matching any existing records, merge() will completely ignore it and ask a db to allocate a new one. This is sometimes a source of a lot of bugs. Do not use merge() to force an id for a new record.
#62 Everything that starts with @AutoConfigure… is good for fine-tuning.
#64 Can’t use 2 annotations that rely on 2 different @Bootstraps under the hood.
#67 Otherwise they will be picked up by all slice tests, which might not be what you want:
#68 Another source of confusion is classpath scanning. Assume that, while you structured your code in a sensible way, you need to scan an additional package.
#69 protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
In ClassPathScanningCandidateComponentProvider
#70 protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
In ClassPathScanningCandidateComponentProvider
#72 private boolean containsNonTestComponent(Class<?>[] classes) {
for (Class<?> candidate : classes) {
if (!AnnotatedElementUtils.isAnnotated(candidate, TestConfiguration.class)) {
return true;
}
}
return false;
}
In SpringBootTestContextBootstrapper
#73 For example, you may have a facade over some remote service that’s unavailable during development.
#74 There is an option to not start the server at all, but test only the layer below that, where Spring handles the incoming HTTP request and hands it off to your controller. That way, almost the full stack is used, and your code will be called exactly the same way as if it was processing a real HTTP request, but without the cost of starting the server. To do that we will use Spring’s MockMvc, and we can ask for that to be injected for us by using the @AutoConfigureMockMvc annotation on the test case.
https://spring.io/guides/gs/testing-web/
#78 The package org.springframework.mock.http.server.reactive contains mock implementations of ServerHttpRequest and ServerHttpResponse for use in WebFlux applications.
#80 So, @SpringBootTest works fine, but @WebFluxTest - doesn’t.
Apart from that there are a lot of problems with JUnit 5.
A lot of options to configure WebTestClient. Maybe the issues where Undertow specific.
#81 If you want to go beyond Spring’s native support, and dig deeper into Flux and Mono - go for reactor-test.
https://www.programcreek.com/java-api-examples/?api=reactor.test.StepVerifier