This is the second article about testing, where we will cover JUnit5 and MockK to test the backend. If you want to refresh your memory on why testing is so important, you can read more about it here.
JUnit
JUnit is a testing framework for JVM compatible languages. It is very useful in TDD, in that the programmer can define his tests before implementing the actual code. Most importantly though, the quick integration of tests inside your application will rapidly tell you whether your new feature has unwanted side effects on existing code, therefore immensely enhancing code stability.
Some features of JUnit include, but are not limited to:
- Open source framework with a gigantic community
- Provides annotations for testing
- Provides annotations for assertions (though often extended with hamcrest)
- Very understandable errors for failed tests
- Very fast executions
- Can be organized in test suites
I believe going into much more detail does not really help, as the advantages of JUnit become much clearer when using it.
Nothing additional is needed to include JUnit in your application, as it will automatically be provided through the spring-boot-starter-test
.
Please also note that I will be using JUnit 5, so the newest version. How to upgrade from version 4 can be found easily online, e.g. here.
MockK
MockK is a bit more special. Generally, Mockito is the mocking framework used together with JUnit. However, for Kotlin, MockK has better support of Kotlin specific features, such as coroutines for example.
To include MockK in your application, simply add the following dependency in your pom:
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.12.0</version>
<scope>test</scope>
</dependency>
If you are used to having Mockito, the change to MockK will take some getting used to. It’s not simply that change though - most likely you will sometimes still have some issues with Kotlin syntax (inline functions for example). But don’t worry, once you get the hang of it, you’ll see how easy it really is!
Unit Tests
Test data
It’s extremely important to set up your test data correctly. Setting up test data sloppily will be dangerous, as you may change one small value in the setup to make your test work, but you may end up breaking some other tests in that case.
Therefore, the ideal way of setting up test data is independent of the Test in my opinion. What this means is that you want to
avoid having some TestDataPRovider
somewhere, that gives you objects. A better way in my opinion, for Java, would be to set up
Fixtures that use the Builder Pattern, but which would not build the object, but instead return the builder.
This way, you can set defaults for all your values, and you’d call it with MyFixture.builder().build()
. Changing a single value could
be done right in your test, and it would not affect other tests one bit.
The reason I mention the Java way is because you can make the same error in Kotlin. Let’s assume that we do not want to set up
the full Account
for every single test. The compiler would even complain if some mandatory fields are not defined, so you couldn’t
even cheat your way through it.
The wrong way
So, you could create a static method (with a companion object in Kotlin) that returns an Account:
companion object {
fun account(): Account {
return Account(id = UUID.randomUUID(),
name = "name",
description = "description",
currency = Currency.getInstance("USD"),
amount = 2.0, addedOn = LocalDate.now())
}
At a first glance, this seems fine. However, what would you now do if you want to have a different test where the name is not “name”. Would you create a different function? If you change the value, other tests will fail, so there’s hardly an alternative.
As you can see, this is hardly scalable. So, while we don’t create an explicit Builder, Kotlin enables us to get the same functionality in a much more extensible way.
The correct way
If we use the Builder Pattern, we will have a Fixture, that looks like this:
class AccountFixture {
companion object {
fun account(
id: UUID = UUID.randomUUID(),
name: String = "name",
description: String = "description",
currency: Currency = Currency.getInstance("USD"),
amount: Double = 1000.0,
addedOn: LocalDate = LocalDate.now()
): Account {
return Account(id = id, name = name, description = description, currency = currency, amount = amount, addedOn = addedOn)
}
}
}
The similarity to before is that still, if we just want an account, we can call AccountFixture.account()
. The difference is now
though, if we want to have an Account with a different name, we do not need to create a second function, nor do we need to change
the value. We can now simply call the fixture like so: AccountFixture.account(name = "A different name")
. See how easy that is?
Kotlin really keeps on amazing me with its simplicity…
Service Layer
The first test will be for the AccountService
. You may be surprised that we’re not testing the AccountRepository
, but since
we do not yet have special methods in there, we don’t really need to test it. If we did, we’d not really be testing our code,
but instead spring, and that seems kind of unnecessary…
So now, as you may remember from the previous article, I’m a big fan of mocking. So now we’ll see the first usages of MockK in our tests.
Let’s start with the getAccount
function test.
Get Account Positive test
@ExtendWith(MockKExtension::class)
internal class AccountServiceTest {
@MockK
lateinit var accountRepository: AccountRepository
@InjectMockKs
lateinit var accountService: AccountService
@Test
fun getAccount() {
val id = UUID.randomUUID()
every { accountRepository.findById(any()) } returns Optional.of(AccountFixture.account(id = id))
accountService.getAccount(id)
assertAll(
{verify(exactly = 1) { accountRepository.findById(id) }}
)
}
}
You can see the @ExtendWith annotation on top of the class - this is a JUnit 5 feature. It is the new way of declaring the runner, essentially.
@Test is then used to define the Test.
Next, we need of course an instance of the class AccountService
which we want to test. This has a dependency however to the
AccountRepository
, which we do not want to instantiate. We also do not want to spin up a full spring context, as that would make
the test a lot slower. So we can instantiate a mocked bean for this dependency with the @MockK annotation. This is similar
to the @Mock
annotation from Mockito.
Next, we tell JUnit that this mocked bean belongs in the AccountService
with @InjectMockKs. No usage of spring, yet still somehow,
the wiring works correctly.
The test in itself is quite simple. We don’t really do much in the function. So it’s only responsibility is to call the AccountRepository
with the input it has received.
Note: If this does not compile correctly for you, ensure that the import for assertAll
is the import org.junit.jupiter.api.assertAll
,
and not import org.junit.jupiter.api.Assertions.assertAll
.
Get Account Negative test
Now you may say: Hold on, the function does more than this - it also throws an Exception if the Account is not found. So let’s also add a negative test here:
@Test
fun getAccount_notFound_exceptionIsThrown() {
every { accountRepository.findById(any()) } returns Optional.empty()
assertThrows<AccountNotFoundException> { accountService.getAccount(UUID.randomUUID()) }
}
Create Account Tests
This function currently does even less, as it really only forwards the account to the repository layer. Yet, it should still be tested:
@Test
internal fun createAccount() {
val account = AccountFixture.account()
every { accountRepository.save(any()) } returns account
accountService.createAccount(account)
assertAll(
{verify(exactly = 1) { accountRepository.save(account) }}
)
}
Controller Layer
The Controller layer can be a bit more challenging to test. You need to know whether you want these tests to be a real unit test, or rather an integration test, which would require spinning up a full spring context.
I tend towards integration tests on the Controller layer. This will enable me to also verify headers, the actual endpoint (if a servlet path is set for example) and so on. The drawback is the execution speed, but I believe this is worth it.
Now, since we will be spinning up an actual Spring context, the bean instantiation works a little differently. We cannot simply
use the @MockK
annotation, but instead need @MockkBean
. This MockK-Spring extension should be added into our pom.xml
:
<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>springmockk</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
GET Tests
Let’s start with the testing of retrieval of accounts:
@WebMvcTest(AccountController::class)
@ActiveProfiles("test")
internal class AccountControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@MockkBean
private lateinit var accountService: AccountService
@Test
fun getAccount() {
val id = UUID.randomUUID()
every { accountService.getAccount(any()) } returns AccountFixture.account(id = id)
mockMvc.perform(
MockMvcRequestBuilders.get("/accounts/$id")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(MockMvcResultMatchers.jsonPath("$.name", CoreMatchers.`is`("name")))
.andExpect(MockMvcResultMatchers.jsonPath("$.description", CoreMatchers.`is`("description")))
.andExpect(MockMvcResultMatchers.jsonPath("$.addedOn", CoreMatchers.`is`(LocalDate.now().toString())))
.andExpect(MockMvcResultMatchers.jsonPath("$.currency", CoreMatchers.`is`("USD")))
.andExpect(MockMvcResultMatchers.jsonPath("$.id", CoreMatchers.`is`(id.toString())))
assertAll(
{ verify(exactly = 1) { accountService.getAccount(id) } }
)
}
@Test
fun getAccount_accountNotFound_404Returned() {
val id = UUID.randomUUID()
every { accountService.getAccount(any()) } throws AccountNotFoundException()
mockMvc.perform(
MockMvcRequestBuilders.get("/accounts/$id")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNotFound)
}
There is a bit more going on here than previously. The @ExtendWith is replaced with @WebMvcTest(AccountController::class). This will spin up a minimal Spring context where it only instantiates the AccountController.
Next, we have the creation of the AccountService
, which is expected. However, we also need to @Autowire
the MockMvc,
which is a mock object that can create incoming REST calls.
In the positive test, I have some assertions on the returned HTTP status, as well as on the response body. I don’t usually do it on every field, as I will forget to keep these up to date anyway. However, this is simply a personal preference.
Please note that this also tests the extension functions already. The returned object is indeed the DTO, and not the domain object. Feel free to run the test with coverage to verify. However, this should not excuse you from having any tests for those. In fact, you should have them, as those should be the tests that break if something goes wrong in the mapping, and NOT the Controller tests!
In the negative test, I also check that throwing an error in the (mocked) AccountService
is correctly resolved into a status 404.
POST Tests
Now, since we need to send a DTO, we first create the Fixture for the DTO:
class AccountDtoFixture {
companion object {
fun accountDto(
id: UUID? = null,
name: String = "name",
description: String? = null,
currency: Currency = Currency.getInstance("USD"),
amount: Double = 1000.0,
addedOn: LocalDate? = null
): AccountDto {
return AccountDto(id = id, name = name, description = description, currency = currency, amount = amount, addedOn = addedOn)
}
}
}
It looks fairly similar to the previous Fixture, but there are some more nullable fields in here.
Now, we need to add also the test for the creation of an account. Since we start with a serialised object, but we don’t want to
post an actual String, we need to add the following field in the class:
private val objectMapper = jacksonObjectMapper() // another awesome utility function in Kotlin
@Test
fun createAccount() {
val capturedAccount = slot<Account>()
val accountDto = AccountDtoFixture.accountDto(amount = 100.0)
every { accountService.createAccount(capture(capturedAccount)) } returns AccountFixture.account(amount = 100.0)
mockMvc.perform(MockMvcRequestBuilders.post("/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(accountDto)))
.andExpect(MockMvcResultMatchers.status().isCreated)
.andExpect(MockMvcResultMatchers.jsonPath("currency", CoreMatchers.`is`("USD")))
.andExpect(MockMvcResultMatchers.jsonPath("amount", CoreMatchers.`is`(100.0)))
assertAll(
{ assertThat(capturedAccount.captured.amount).isEqualTo(100.0) },
{ assertThat(capturedAccount.captured.description).isNull() }
)
}
Small note regarding this test: I added the slot
to capture the account that is passed to the AccountService
. In fact, there’s
no real point to have any assertions on the returned object in regards to the input, since we decide ourselves what will
be returned. However, there is some value in checking what is passed to the accountService
, as this is done by the application,
and not defined in the test - hence the capturing.
Extension methods
While they were technically already tested previously, it won’t hurt to have separate tests for the extension methods. For now, we only have Mappings in here, but let’s test them anyway, for completeness:
internal class DtoMappingsKtTest {
@Test
fun account_toDto() {
val account = AccountFixture.account()
val accountDto = account.toDto()
assertAll(
{ assertThat(accountDto.id).isEqualTo(account.id) },
{ assertThat(accountDto.name).isEqualTo(account.name) },
{ assertThat(accountDto.amount).isEqualTo(account.amount) },
{ assertThat(accountDto.description).isEqualTo(account.description) },
{ assertThat(accountDto.addedOn).isEqualTo(account.addedOn) },
{ assertThat(accountDto.currency).isEqualTo(account.currency) },
)
}
@Test
fun accountDto_toDomain() {
val accountDto = AccountDtoFixture.accountDto(id = null, addedOn = null)
val account = accountDto.toDomain()
assertAll(
{ assertThat(account.id).isNotNull() },
{ assertThat(account.name).isEqualTo(accountDto.name) },
{ assertThat(account.amount).isEqualTo(accountDto.amount) },
{ assertThat(account.description).isEqualTo(accountDto.description) },
{ assertThat(account.addedOn).isEqualTo(LocalDate.now()) },
{ assertThat(account.currency).isEqualTo(accountDto.currency) },
)
}
}
The most notable assertions here are those from the DTO to domain, where we check that indeed an id
and an addedOn
date are set.
As a little check - running all the tests will show that we have some pretty impressive line coverage of 97% right now!
(The setup to run all tests is shown below)
This is only a small introduction to BE testing of course. There is a lot more to cover, but even with only the tools that are mentioned in this post, you will already be able to cover a huge chunk of your code.