Kotlin Spring Boot Tutorial Part 5: Creating REST endpoints for a task app

Habibi Coding | حبيبي كودنق
Geek Culture
Published in
7 min readJan 21, 2023
tutorial banner

Here is a quick summary of Part 4:

We set up a local development database based on a Postgres Docker image. We added a volume on a folder so the data does not get lost when we turn off the Docker container or turn off our entire machine. Here is a link to part 4.

Time to write Unit and Integration Tests — Yalla | يلا

Let us start with setting up entries for an SQL file. Go to src -> test -> right click -> New -> Directory. Then select the resources folder.

new resources folder

Right-click -> New -> File and name it test-data.sql. Then we will add dummy data with insert statements.

INSERT INTO task
(`id`, `description`, `is_reminder_set`, `is_task_open`, `created_on`, `priority`)
VALUES (111, 'first test todo', false, false, CURRENT_TIME(), 'LOW');

INSERT INTO task
(`id`, `description`, `is_reminder_set`, `is_task_open`, `created_on`, `priority`)
VALUES (112, 'second test todo', true, false, CURRENT_TIME(), 'MEDIUM');

INSERT INTO task
(`id`, `description`, `is_reminder_set`, `is_task_open`, `created_on`, `priority`)
VALUES (113, 'third test todo', true, true, CURRENT_TIME(), 'HIGH');

Remove the file `TaskAppBeMediumcomApplicationTests` because we don’t need it.

delete TaskAppBeMediumcomApplicationTests

Then create the folder repository and then the file TaskRepositoryTestEmbedded inside it. Annotate it with:

@DataJpaTest(properties = ["spring.jpa.properties.javax.persistence.validation.mode=none"])
internal class TaskRepositoryTestEmbedded {
}

The annotation allows us to use dependency injection inside the test class and the persistence validation is turned off. Basically, the entered object will not be validated because we don’t need to persist it. The internal qualifier tells this class is only in this module accessible.

The next thing we want is an instance of TaskRepository to run the tests on. Three fields one which stores the number of rows inside the test-data.sql one which stores the number of closed tasks, and one which the open ones.

@DataJpaTest(properties = ["spring.jpa.properties.javax.persistence.validation.mode=none"])
internal class TaskRepositoryTestEmbedded {

@Autowired
private lateinit var objectUnderTest: TaskRepository

private val numberOfRecordsInTestDataSql = 3
private val numberOfClosedTasksInTestDataSql = 2
private val numberOfOpenTasksInTestDataSql = 1

@Test
@Sql("classpath:test-data.sql")
fun `when task saved through SQL file then check if it is not null`() {
val task: Task = objectUnderTest.findTaskById(111)
assertThat(task).isNotNull
}

}

Now write your first method, and annotate it with @Test and @Sql, the SQL annotation will tell which SQL file to use. The first test will be an easy one which just checks if a task with a specific ID is in the SQL file.

The next test will fetch all tasks from our test-data.sql and checks them against our field numberOfRecordsInTestDataSql.

@Test 
@Sql(“classpath:test-data.sql”)
fun `when task saved through SQL file then check for the number of tasks`() {
val tasks: List<Task> = objectUnderTest.findAll()
assertThat(tasks.size).isEqualTo(numberOfRecordsInTestDataSql)
}

This test will first delete the task with id 112 and then fetch all tasks. Then check if the list size decreased from three to two.

    @Test
@Sql("classpath:test-data.sql")
fun `when task saved through SQL file then remove it by id`() {
objectUnderTest.deleteById(112)
val tasks: List<Task> = objectUnderTest.findAll()
assertThat(tasks.size).isEqualTo(numberOfRecordsInTestDataSql - 1)
}

After that write a test that queries all open tasks and checks it against our field numberOfOpenTasksInTestDataSql.

@Test
@Sql("classpath:test-data.sql")
fun `when task saved through SQL file then check for the number of open tasks`() {
val tasks: List<Task> = objectUnderTest.queryAllOpenTasks()
assertThat(tasks.size).isEqualTo(numberOfOpenTasksInTestDataSql)
}

I would say it is time to check the opposite of all closed tasks and checks it against our numberOfClosedTasksInTestDataSql.

@Test
@Sql("classpath:test-data.sql")
fun `when task saved through SQL file then check for the number of closed tasks`() {
val tasks: List<Task> = objectUnderTest.queryAllClosedTasks()
assertThat(tasks.size).isEqualTo(numberOfClosedTasksInTestDataSql)
}

We can then check if the method doesDescriptionExist() properly works, by querying a description that already exists in test-data.sql and one which is not.

  @Test
@Sql("classpath:test-data.sql")
fun `when description is queried then check if descriptions already exists`() {
val isDescriptionAlreadyGiven1 = objectUnderTest.doesDescriptionExist("first test todo")
val isDescriptionAlreadyGiven2 = objectUnderTest.doesDescriptionExist("any todo")

assertThat(isDescriptionAlreadyGiven1).isTrue
assertThat(isDescriptionAlreadyGiven2).isFalse
}

Unit tests for TaskServiceTest

Open the TaskService class on Mac: Option + Enter -> Create test

(FYI: This is something we couldn’t do for TaskRepository because it is an interface)

create test for TaskService

Add the checkmark forsetUp/@Before and make sure your testing library is Junit5 and click OK

create test for TaskService

Add the annotation `@ExtendWith(MockKExtension::class)`

@ExtendWith(MockKExtension::class)
internal class TaskServiceTest {

@BeforeEach
fun setUp() {
}
}

Then add an instance from TaskRepository via dependency injection by using the annotation @RelaxedMockK. For an instance of TaskService we need the annotation @InjectMockKs. Which injects TaskRepository into TaskService. Then instantiate an instance of Task and a lateinint variable of TaskCreateRequest.

@RelaxedMockK
private lateinit var mockRepository: TaskRepository

@InjectMockKs
private lateinit var objectUnderTest: TaskService

private val task = Task()
private lateinit var createRequest: TaskCreateRequest

Move to the setUp() method first to initiate the MockKAnnotations and then the createRequest object.

@BeforeEach
fun setUp() {
MockKAnnotations.init(this)
createRequest = TaskCreateRequest(
"test task",
isReminderSet = false,
isTaskOpen = false,
createdOn = LocalDateTime.now(),
priority = Priority.LOW
)
}

Time for the first test, create a list of tasks, mock the response for the findAll() method inside the repository returns the tasks list you created. The every {} method allows us to say for every method call of repo.findAll() which happens when service.getAllTasks() gets called, give this specific response. Then call from the service object the getAllTasks() method and save the response in a local variable. Check your two local variables against their size.

@Test
fun `when all tasks get fetched then check if the given size is correct`() {
// GIVEN
val expectedTasks = listOf(Task(), Task())

// WHEN
every { mockRepository.findAll() } returns expectedTasks.toMutableList()
val actualList: List<TaskDto> = objectUnderTest.getAllTasks()

// THEN
assertThat(actualList.size).isEqualTo(expectedTasks.size)
}

Move on to the next test method. Add the property values of the request object to the task object. Mock with every{} method that every time the repo saves anything our task object gets returned. Then try to create a new task with our request object. Assert the returned object from createTask() method with our task object.

    @Test
fun `when task gets created then check if it gets properly created`() {
task.description = createRequest.description
task.isReminderSet = createRequest.isReminderSet
task.isTaskOpen = createRequest.isTaskOpen
task.createdOn = createRequest.createdOn

every { mockRepository.save(any()) } returns task
val actualTaskDto: TaskDto = objectUnderTest.createTask(createRequest)

assertThat(actualTaskDto.description).isEqualTo(task.description)
}

Let us now test the exception case for createTask() method. Use every{} method again to mock doesDescriptionExist(any()) to true. We know that when we call objectUnderTest.createTask(createRequest) an exception should be thrown. It should be a BadRequestException and we store this exception in a local variable. At last, we make sure with verify{} method that the repo.save() call was never executed.

@Test
fun `when task gets created with non unique description then check for bad request exception`() {
every { mockRepository.doesDescriptionExist(any()) } returns true

val exception = assertThrows<BadRequestException> { objectUnderTest.createTask(createRequest) }

assertThat(exception.message).isEqualTo("There is already a task with description: test task")
verify { mockRepository.save(any()) wasNot called }
}

I would say we should test now another exception. We have the class TaskNotFoundException, so we need to call a task ID that does not exist. The method every{} allows throwing false for any given ID for the method existsById(). When calling objectUnderTest.getTaskById(123) the TaskNotFoundException should be thrown and stored in a local variable. Assert the actual exception message with the expected exception message. Lastly, verify findTaskById() method from the repo was never executed.

@Test
fun `when get task by id is called then expect a task not found exception`() {
every { mockRepository.existsById(any()) } returns false

val exception = assertThrows<TaskNotFoundException> { objectUnderTest.getTaskById(123) }

assertThat(exception.message).isEqualTo("Task with ID: 123 does not exist!")
verify { mockRepository.findTaskById(any()) wasNot called }
}

There are other test methods to add to TaskServiceTest for instance deleteById() or updateTask(), but I would say give it a try to implement the test methods on your own. In case you get stuck just watch my YouTube video where I explain every step. You can also just check out the source code by checking out branch part_five. Both are linked below.

Okay, that’s it for the first part. If you enjoyed this article give it a clap. Here is Part 6:

Here is the completed project, check out the branch part_five

By the way here is the link for the article as YouTube series: https://www.youtube.com/watch?v=ZKMGMZqnmOk&list=PLjuEK3Ez60n2dTFL7-KETl1yl04kOo-rM

--

--