The ASRR approved TDD development flow
Setting up your service class
@Service
class BookService(val bookRepository: BookRepository) {
fun addBook(dto: BookDto): Book {
TODO()
}
}Given the following model class
@Entity
data class Book(
@Id
val id: String,
val title: String,
val author: String = ""
)and the following DTO
data class BookDto(
val title: String,
val author: String = ""
)Mocking
For mocking, we will use MockK (opens in a new tab)
To test our code in units, we need to mock the repository. We don't want to test the repository, we want to test the service.
Add MockK dependency
testImplementation("io.mockk:mockk:1.9.3")Basic sample of mocking a repository
@Test
fun `should add a book`() {
val repository = mockk<BookRepository>()
every { repository.save(any()) } returnsArgument 0
val service = BookService(repository)
val dto = BookDto("title", "author")
val result = service.addBook(dto)
assertEquals(dto.title, result.title)
}What we see here is that we are mocking the repository and we are telling it to return the argument that was passed to it. This is a very simple example, but it shows how we can mock a repository.
The notation mockk<T>() ensures we create a mocked instance of the class we pass to it. In this case, we are mocking the BookRepository class. We can then specify how the mocked class should respond to specific methods with specific inputs.
We can do this using the every function. This function takes a lambda as an argument. The lambda specifies what should happen when the mocked method is called. In this case, we are telling the mocked method to return the argument that was passed to it.
In the lambda, we can specify which input is matched for in the repository.save() function, in this case we use any() to match any input. We can also specify which output should be returned.
Set up your test file
- Generate test class using IntelliJ. Shortcut:
Ctrl + Shift + TorCmd + Shift + T - Make sure your class ends with "Tests"
- Set up a private function to get a instance of your service class
private fun getService(val repository: BookRepository = mockk()): BookService {
return BookService(repository = repository)
}The iteration
Write a failing test (failing when running, not compiling)
@Test
fun `should add a book`() {
val repository = mockk<BookRepository>()
every { repository.save(any()) } returnsArgument 0
val service = getService(repository = repository)
val dto = BookDto("title", "author")
val result = service.addBook(dto)
assertEquals(dto.title, result.title)
}If we run this against our code from before, we will get a NotImplementedException due to the TODO() in the service class.
Write code so that the test is passing
@Service
class BookService(val bookRepository: BookRepository) {
fun addBook(dto: BookDto): Book {
val book = Book(UUID.randomUUID().toString(), dto.title, dto.author)
return bookRepository.save(book)
}
}Write a failing test
@Test
fun `should throw exception if book with same name already exists`() {
val service = getService()
every { repository.save(any()) } returnsArgument 0
val dto = BookDto("title", "author")
service.addBook(dto)
assertThrows<IllegalArgumentException> {
service.addBook(dto)
}
}Write code so that the test is passing
@Service
class BookService(val bookRepository: BookRepository) {
fun addBook(dto: BookDto): Book {
val book = Book(UUID.randomUUID().toString(), dto.title, dto.author)
if (bookRepository.existsByTitle(book.title)) {
throw IllegalArgumentException("Book with title ${book.title} already exists")
}
return bookRepository.save(book)
}
}Repeat until all functionality is covered.