Step-by-step guide
Let’s learn new concepts in kotlin
Create a simple test
class DemoTest {
@Test
void `Test some rules`
{
assertThat("hello" + "test")
}
}
Create integration test
Add dependencies
- Start spring boot test like in Java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Testing controller
- Solution N°1: Create test as we do with java and Mockito
@WebMvcTest
class WebTierTests(@Autowired val mockMvc: MockMvc) {
@MockBean
private lateinit var service: MessageService
@Test
fun `find messages`() {
Mockito.`when`(service.findMessages()).thenReturn(listOf(Message("1", "First"), Message("2", "Second")))
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("\$.[0].id").value("1"))
.andExpect(MockMvcResultMatchers.jsonPath("\$.[0].text").value("First"))
.andExpect(MockMvcResultMatchers.jsonPath("\$.[1].id").value("2"))
.andExpect(MockMvcResultMatchers.jsonPath("\$.[1].text").value("Second"))
Mockito.verify(service).findMessages()
}
}
- Solution N°2
- Recreate the test using extensions (DSL) provider by spring
- use Mockk is a mocking library for kotlin supported by spring tests
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.4</version>
</dependency>
<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>springmockk</artifactId>
<version>3.0.1</version>
</dependency>
@MockkBean
private lateinit var service: MessageService
@Test
fun `find messages`() {
every { service.findMessages() } returns listOf(
Message("1", "First"),
Message("2", "Second")
)
mockMvc.get("/")
.andExpect { status { isOk() } }
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
.andExpect {
jsonPath("\$.[0].id", "") { value("1") }
jsonPath("\$.[0].text") { value("First") }
jsonPath("\$.[1].id") { value("2") }
jsonPath("\$.[1].text") { value("Second") }
}
verify(exactly = 1) { service.findMessages() }
}
Refactoring and clean Tests
Analyse tests:
- Have a business oriented name for the test
- What is a Right ? What is a valid comment ?
fun `returns a Right for valid comment`() {
val blogService = BlogService()
val article = Article(
"Lorem Ipsum",
"consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore"
)
val result = blogService.addComment(article, "Amazing article !!!", "Pablo Escobar")
assert(result.isRight)
}
- 1 test to test each comment value…
- 4 tests instead of 1 to maintain
- Tests should be behavior focus -> not data driven
fun `add a comment with the given text`() {
val blogService = BlogService()
val article = Article(
"Lorem Ipsum",
"consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore"
)
val text = "Amazing article !!!"
val result = blogService.addComment(article, text, "Pablo Escobar")
assert(result.get().comments.first().text == text)
}
fun `add a comment with the given author`() {
val blogService = BlogService()
val article = Article(
"Lorem Ipsum",
"consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore"
)
val author = "Pablo Escobar"
val result = blogService.addComment(article, "Amazing article !!!", author)
assert(result.get().comments.first().author == author)
}
- Missing assertions
fun `add a comment with the date of the day`() {
val blogService = BlogService()
val article = Article(
"Lorem Ipsum",
"consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore"
)
val author = "Pablo Escobar"
val result = blogService.addComment(article, "Amazing article !!!", author)
}
- What is inside the Left ?
fun `returns a Left when adding existing comment`() {
val blogService = BlogService()
val article = Article(
"Lorem Ipsum",
"consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore",
listOf(Comment("Amazing article !!!", "Pablo Escobar", LocalDate.now()))
)
val result = blogService
.addComment(article, "Amazing article !!!", "Pablo Escobar")
assert(result.isLeft)
}
- Isolate the technical glue in a
companion
andtop level function
BlogTestUtils.kt
// Top level Function
fun articleWithComments(comments: List<Comment>): Article =
Article(
"Lorem Ipsum",
"consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore",
comments
)
fun assertAddedComment(
article: Article,
expectedText: String,
expectedAuthor: String
) {
val addedComment = article.comments.last()
assertionJunit5.assertEquals(expectedAuthor, addedComment.author)
assertionJ.assertThat( addedComment.creationDate).isEqualTo(LocalDate.now())
// Equals operators
Assertions.assertTrue(expectedText == addedComment.text)
}
// using typealias to differentiate two libs Junit and AssertJ
typealias assertionJunit5 = org.junit.jupiter.api.Assertions
typealias assertionJ = org.assertj.core.api.Assertions
class BlogServiceRefactoredTest {
private val blogService = BlogService()
@Nested
inner class `add new comment`{
@Test
fun `in an empty Article including given text & author`() {
val updatedArticle = blogService.addComment(emptyArticle, text, author)
assert(updatedArticle.isRight)
assertAddedComment(updatedArticle.get(), text, author)
}
@Test
fun `in an Article containing existing comments`() {
val newText = "Finibus Bonorum et Malorum"
val newAuthor = "Al Capone"
val updatedArticle = blogService.addComment(articleWith1Comment, newText, newAuthor)
assert(updatedArticle.isRight)
Assertions.assertEquals(updatedArticle.get().comments.size, 2)
assertAddedComment(updatedArticle.get(), newText, newAuthor)
}
}
@Test
fun `return an error when adding existing comment`() {
val updatedArticle = blogService.addComment(articleWith1Comment, text, author)
Assertions.assertTrue(updatedArticle.isLeft)
Assertions.assertEquals(1, updatedArticle.left.size)
Assertions.assertEquals(BlogService.ErrorMessage.existingCommentError, updatedArticle.left.first().description)
}
companion object BlogRefactoredTests {
private const val text = "Amazing article !!!"
private const val author = "Pablo Escobar"
private val emptyArticle: Article = articleWithComments(emptyList())
private val articleWith1Comment: Article = articleWithComments(listOf(Comment(text, author, LocalDate.now())))
}
}
Tests integration using TestContainers
** Add Dependecy for Postgres jdbc driver
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.1</version>
</dependency>
** Add Tests containers dependecies
<!-- Integration with Junit -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.16.0</version>
<scope>test</scope>
</dependency>
<!-- Integration with Postgre -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.16.0</version>
<scope>test</scope>
</dependency>
- Update the schema definition to use postgre database specific syntax
CREATE TABLE IF NOT EXISTS messages (
id varchar(50) DEFAULT uuid_generate_v4()::text,
text VARCHAR(500),
CONSTRAINT id_messages PRIMARY KEY (id)
);
- Update application.properties to use PostgreSQL driver
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
- A simple test to start the container and verify if is up running
- To Start the container once per test class, we can delarce it in a
companion object
- Make sure that docker is running in this machine
@Testcontainers
class IntegrationTests {
companion object {
@Container
val container = postgres("13-alpine") {
withDatabaseName("db")
withUsername("user")
withPassword("password")
withInitScript("sql/schema.sql")
}
}
@Test
fun `container is up and running`() {
Assertions.assertTrue(container.isRunning)
}
}
- Create an integration test for Repository
@Testcontainers
class MessageRepositoryTest(@Autowired var messageRepository: MessageRepository) {
val message = Message("1", "My first Message")
@BeforeEach
fun cleanup() {
messageRepository.deleteAll();
}
companion object {
@Container
val postgreDBContainer = PostgreSQLContainer(DockerImageName.parse("postgres:13-alpine"))
.withDatabaseName("db")
.withUsername("user")
.withPassword("password")
.withInitScript("sql/schema.sql")
@JvmStatic
@DynamicPropertySource
fun datasourceConfig(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgreDBContainer::getJdbcUrl)
registry.add("spring.datasource.password", postgreDBContainer::getPassword)
registry.add("spring.datasource.username", postgreDBContainer::getUsername)
}
}
@Test
fun `add a new message`() {
val id = "${Random.nextInt()}".uuid()
val message = Message(id, "some message")
assertDoesNotThrow { messageRepository.save(message) }
val result = messageRepository.findById(id).get();
Assertions.assertEquals(message.id, result.id)
Assertions.assertEquals(message.text, result.text)
}
}