
Escaping the magics of the frameworks: 3. Tests
When I want to test a framework-based application, I ran into some magics instructing the framework to run in a test environment. Usually I also need to receive from its dependency mechanism the client mock that simulates an http request.
I could perform an http request and expect an http response. The tests would be less coupled to the implementation details and I would be able to refactor the application more easily.
This is why I am doing this now. I want to be able to refactor the application further and get rid of all the magic in the most comfortable way possible and without breaking the tests.
In this series of articles I’m replacing, magic by magic, some framework’s features until I have no more sorceries to wield. I’m doing it in baby steps, explaining the reasons why, and keeping my application working all the time.
You can read about frameworks and what’s wrong with magics in the previous articles. I also tackled the magical configuration and the power of explicitness in the routing. From now on I will continue where I left off with the code examples tackling the refactoring of the tests in order to decouple them from the implementation.
Please this article on LinkedIn and tag me if you want to start a public conversation on this topic or contact me if you prefer to talk about it privately.
Take back control of my application
The first step to decouple the tests from the Spring Boot implementation details is to take back control of my application.
I started from this situation:
Application.kt
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
@Configuration
@SpringBootApplication
class Application {
@Bean
fun router(greeting: GreetingController): RouterFunction<ServerResponse> {
return router {
greeting.routes(this)
}
}
}
It sounds natural to delegate the application start and stop to the Application
class itself. It’s a behaviour that an app should have.
Application.kt
fun main(args: Array<String>) {
Application().start(args)
}
@Configuration
@SpringBootApplication
class Application {
private var appContext: ConfigurableApplicationContext? = null
private val app = SpringApplication(Application::class.java)
@Bean
fun router(greeting: GreetingController): RouterFunction<ServerResponse> {
return router {
greeting.routes(this)
}
}
fun start(args: Array<String>) {
appContext = app.run(*args)
}
fun stop() {
appContext?.close()
}
}
(See the diff - tag 09-CONTROL-APP
)
I just added two methods: start()
and stop()
. To make them effective, though, I have to keep the SpringApplication
as a private field along with the ConfigurableApplicationContext
that I instantiate when I start the app and I use when I need to stop the app.
Decouple the test assertions
The MockMvc
class mocks the requests made to the SpringBootApplication
along with the responses, and provides a way to chain various assertions about the response. It can also check the json structure and values.
It’s a Spring Boot tool that is injected at runtime and it is totally dependent on the framework magic: @SpringBootTest
.
However I’m going to get rid of this magic, so I cannot rely on MockMvc
anymore.
As a first step I can change the way I write the assertions. This will free them from the framework’s grip.
I have this test file:
GreetingControllerTests.kt
@SpringBootTest
@AutoConfigureMockMvc
class GreetingControllerTests {
@Autowired
private val mockMvc: MockMvc? = null
@Test
fun noParamGreetingShouldReturnDefaultMessage() {
mockMvc!!.perform(MockMvcRequestBuilders
.get("/greeting"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value("Hello, World!"))
}
// ...
}
I’m not a big fan of the json matchers, I think this kind of test should be as straightforward as possible. For example I could verify the whole body or check that the body contains a string like "content": "Hello, World!"
.
However, I also see its usefulness. It helps to verify the json format correctness and simplifies the assertion of more complex json strings.
Now, I want to keep the same json matching feature, but I don’t want to write one myself. Luckily I found a library that does exactly what I need: json-unit-assertj
. So, I added it to the build.gradle
file.
GreetingControllerTests.kt
@SpringBootTest
@AutoConfigureMockMvc
class GreetingControllerTests {
@Autowired
private val mockMvc: MockMvc? = null
@Test
fun noParamGreetingShouldReturnDefaultMessage() {
val response = mockMvc!!.perform(get("/greeting")).andReturn().response
assertThat(response.status).isEqualTo(200)
assertThatJson(response.contentAsString).inPath("$.content").isEqualTo("Hello, World!")
}
// ...
}
(See the diff - tag 10-ACT-ASSERT
)
Now I’m using the mockMvc
only to perform the get request, in that way it will be easier to get rid of it later.
Start the Application
The tests are still coupled to the Spring Boot internalities with some annotations and an injection.
Let’s remove all the Spring Boot annotations @SpringBootTest
and @AutoConfigureMockMvc
, together with MockMvc
.
To do so I have to change 2 things:
1. I must have the application running
I create an Application
instance in the test file, I start it before running all tests and stop it after all tests in the file are run. I used @BeforeAll
and @AfterAll
JUnit annotations.
2. I must perform a real HTTP request
I use the built-in Spring Boot RestTemplate
class to perform a GET request. I only need to change the way I retrieve the response body and the statusCode.
GreetingControllerTests.kt
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GreetingControllerTests {
private val application = Application()
@Test
fun noParamGreetingShouldReturnDefaultMessage() {
val response = get("/greeting")
assertThat(response.statusCode.value()).isEqualTo(200)
assertThatJson(response.body).inPath("$.content").isEqualTo("Hello, World!")
}
@Test
fun paramGreetingShouldReturnTailoredMessage() {
val response = get("/greeting?name=Spring Community")
assertThat(response.statusCode.value()).isEqualTo(200)
assertThatJson(response.body).inPath("$.content").isEqualTo("Hello, Spring Community!")
}
@BeforeAll
fun beforeAll() {
application.start(emptyArray())
}
@AfterAll
fun afterAll() {
application.stop()
}
private fun get(uri: String): ResponseEntity<String> {
return RestTemplate().getForEntity("http://localhost:8080$uri", String::class.java)
}
}
(See the diff - tag 11-START-APP
)
I don’t like so much the way RestTemplate
performs the requests, it is not so expressive. For now I chose to create a private method to make the tests more readable, but I will replace it soon.
Change HTTP Client
Some time ago I created a really simple HTTP client for Kotlin: Topinambur. I kept it as simple as possible to use and to maintain. It can be used statically or it can be instantiated with some specific defaults like the baseUrl
.
I decided to replace RestTemplate
with that library. Of course any other HTTP client will do, the important thing is to have the option to create an instance with a baseUrl without the need to repeat it on every request (it will be useful later).
GreetingControllerTests.kt
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GreetingControllerTests {
private val application = Application()
private val client = Http(baseUrl = "http://localhost:8080")
@Test
fun noParamGreetingShouldReturnDefaultMessage() {
val response = client.get("/greeting")
assertThat(response.statusCode).isEqualTo(200)
assertThatJson(response.body).inPath("$.content").isEqualTo("Hello, World!")
}
@Test
fun paramGreetingShouldReturnTailoredMessage() {
val response = client.get("/greeting?name=Spring Community")
assertThat(response.statusCode).isEqualTo(200)
assertThatJson(response.body).inPath("$.content").isEqualTo("Hello, Spring Community!")
}
@BeforeAll
fun beforeAll() {
application.start(emptyArray())
}
@AfterAll
fun afterAll() {
application.stop()
}
}
(See the diff - tag 12-HTTP-CLIENT
)
Start and stop the app on each test
The tests as they are now could be ok. There is a single app instance that answers to all the tests (first approach). That’s fine. However I would like to instantiate a new application instance for each test (second approach).
Both approaches are good and can be used in different contexts for different reasons.
I would choose the first approach when I can have the same application instance on every test and I need faster tests. The application is started at the beginning of the test file and closed at the end.
I prefer the second approach when I already have a fast Application startup (usually when the app doesn’t inject dependencies at runtime) and when it is important to have the tests completely isolated. The application is started before and stopped after each single test.
Let’s go for the second approach.
I thought that I could start and stop the application with @BeforeEach
and @AfterEach
test annotations, but I would like to do it programmatically without annotations.
In Kotlin there is a way to automatically close a resource. It’s like a try-with-resources
Java statement, but way more expressive. It is .use.
To apply .use
I need to have an AutoCloseable
resource. So my Application
needs to extend the AutoCloseable
class and it needs to have an override fun close()
method that replaces the previous fun stop()
method.
Application.kt
class Application: AutoCloseable {
// ...
override fun close() {
appContext?.close()
}
}
Now the Application
is ready to be closed automatically. Let’s change the tests:
GreetingControllerTests.kt
class GreetingControllerTests {
@Test
fun noParamGreetingShouldReturnDefaultMessage() = runApp { client ->
val response = client.get("/greeting")
assertThat(response.statusCode).isEqualTo(200)
assertThatJson(response.body).inPath("$.content").isEqualTo("Hello, World!")
}
@Test
fun paramGreetingShouldReturnTailoredMessage() = runApp { client ->
val response = client.get("/greeting?name=Spring Community")
assertThat(response.statusCode).isEqualTo(200)
assertThatJson(response.body).inPath("$.content").isEqualTo("Hello, Spring Community!")
}
private fun runApp(test: (client: Http) -> Unit) {
val client = Http("http://localhost:8080")
Application()
.start(emptyArray())
.use { test(client) }
}
}
(See the diff - tag 13-RUN-APP
)
I only needed to create a private runApp
function that receives another function as the first argument. The argument is the test body that needs to be wrapped.
In runApp
I instantiate a client, I start the application, and I run the test block inside the .use
statement with the client as argument.
In that way I only need to use the runApp
function on every test that needs a running Application.
The function starts the application, gives the configured HTTP client to the test block, and closes the server after each test block.
The interesting thing is that it’s only a function, it’s not an annotation. Not magic. Only Kotlin code.
Conclusions
You have seen how I changed the way tests interact with the application.
Beforehand, I was using a framework tool that simulates an http request. The tests needed to know that it’s a Spring Boot application, but they were quite fast even if the app had to instantiate the dependencies at runtime.
Now I’m starting the real application in the tests and I’m performing a real HTTP request on each test. The tests treat the Application as a black box, they are totally decoupled from the application, but they are a bit slower.
I think it’s a matter of needs. For this series of articles I need to be free to change my application structure without breaking my safety net that checks if everything is still working.
What’s next?
Now I’m ready to make the greatest refactoring of the series. I will reduce the magic around the Spring Boot dependency manager. It’s still out of my control. It’s still magic.
Stay tuned :)
Please let me know your opinion
this article on LinkedIn and tag me if you find it interesting.