Search code examples
kotlinktorkoin

TestApplication in Ktor


So i have built my Ktor backend and im watching a tutorial from philip lackner Tutorial He starts of with making a withTestApplication call within his testfunction... The thing is that this has been deprecated and it would be nice to use the newer version of it which i understand is

@Test
fun `Create user, no body attached, responds with BadRequest`() = testApplication{
   application {
       install(Routing) {
           createUserRoute(userRepository)
       }
   }
}

And so i use this code but i have no clue how to get the scope so i can use handleRequest? please can somebody help me with this?

This is the code i wanted to test

package com.PapperSeller.routes

import com.PapperSeller.controller.user.UserRepository
import com.PapperSeller.data.models.User
import com.PapperSeller.data.requests.CreateAccountRequest
import com.PapperSeller.data.responses.BasicApiResponse
import com.PapperSeller.util.ApiResponseMessages
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject


fun Route.createUserRoute(repository: UserRepository){

    route("/api/user/create"){
        post {
            kotlin.runCatching { call.receive<CreateAccountRequest>()}.onSuccess {request ->
                val userExist = repository.getUserByEmail(request.email) != null
                if(userExist){
                    call.respond(BasicApiResponse(ApiResponseMessages.USER_ALREADY_EXIST, false))
                    return@post
                }else{
                    if(request.email.isBlank() || request.username.isBlank() || request.password.isBlank()){
                        call.respond(BasicApiResponse(ApiResponseMessages.FIELDS_EMPTY, false))
                        return@post
                    }else{
                        repository.createUser(User(email = request.email, username = request.username, password = request.password))
                        call.respond(BasicApiResponse("", true))
                        return@post
                    }
                }

            }.onFailure {
                call.respond(BasicApiResponse(ApiResponseMessages.NO_USER_REGISTERED, false))
                return@post


            }
        }
    }
}

Solution

  • Here is an example of how you can cover the Route.createUserRoute with tests:

    import io.ktor.client.call.*
    import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
    import io.ktor.client.request.*
    import io.ktor.http.*
    import io.ktor.serialization.kotlinx.json.*
    import io.ktor.server.application.*
    import io.ktor.server.request.*
    import io.ktor.server.response.*
    import io.ktor.server.routing.*
    import io.ktor.server.testing.*
    import kotlinx.serialization.Serializable
    import kotlin.test.Test
    import kotlin.test.assertEquals
    
    class KtorTest {
        @Test
        fun userNotRegistered() = testApplication {
            application {
                module(MemoryUserRepository())
            }
    
            val client = createClient {
                install(ContentNegotiation) {
                    json()
                }
            }
    
            assertEquals(
                BasicApiResponse(ApiResponseMessages.NO_USER_REGISTERED, false),
                client.post("/api/user/create").body()
            )
        }
    
        @Test
        fun userAlreadyExist() = testApplication {
            application {
                module(MemoryUserRepository(mapOf("john@doe" to User("", "", ""))))
            }
    
            val client = createClient {
                install(ContentNegotiation) {
                    json()
                }
            }
    
            client.post("/api/user/create") {
                setBody(CreateAccountRequest("john@doe", "", ""))
                contentType(ContentType.Application.Json)
            }.body<BasicApiResponse>().let { response ->
                assertEquals(BasicApiResponse(ApiResponseMessages.USER_ALREADY_EXIST, false), response)
            }
        }
    
        @Test
        fun emptyRequestFields() = testApplication {
            application {
                module(MemoryUserRepository())
            }
    
            val client = createClient {
                install(ContentNegotiation) {
                    json()
                }
            }
    
            client.post("/api/user/create") {
                setBody(CreateAccountRequest("", "", ""))
                contentType(ContentType.Application.Json)
            }.body<BasicApiResponse>().let { response ->
                assertEquals(BasicApiResponse(ApiResponseMessages.FIELDS_EMPTY, false), response)
            }
        }
    
        @Test
        fun successfulRegistration() = testApplication {
            application {
                module(MemoryUserRepository())
            }
    
            val client = createClient {
                install(ContentNegotiation) {
                    json()
                }
            }
    
            client.post("/api/user/create") {
                setBody(CreateAccountRequest("john@doe", "john", "123"))
                contentType(ContentType.Application.Json)
            }.body<BasicApiResponse>().let { response ->
                assertEquals(BasicApiResponse("", true), response)
            }
        }
    }
    
    fun Application.module(repository: UserRepository) {
        install(io.ktor.server.plugins.contentnegotiation.ContentNegotiation) {
            json()
        }
        routing {
            createUserRoute(repository)
        }
    }
    
    fun Route.createUserRoute(repository: UserRepository) {
        route("/api/user/create") {
            post {
                kotlin.runCatching { call.receive<CreateAccountRequest>() }.onSuccess { request ->
                    val userExist = repository.getUserByEmail(request.email) != null
                    if (userExist) {
                        call.respond(BasicApiResponse(ApiResponseMessages.USER_ALREADY_EXIST, false))
                        return@post
                    } else {
                        if (request.email.isBlank() || request.username.isBlank() || request.password.isBlank()) {
                            call.respond(BasicApiResponse(ApiResponseMessages.FIELDS_EMPTY, false))
                            return@post
                        } else {
                            repository.createUser(
                                User(
                                    email = request.email,
                                    username = request.username,
                                    password = request.password
                                )
                            )
                            call.respond(BasicApiResponse("", true))
                            return@post
                        }
                    }
                }.onFailure {
                    call.respond(BasicApiResponse(ApiResponseMessages.NO_USER_REGISTERED, false))
                    return@post
                }
            }
        }
    }
    
    class ApiResponseMessages {
        companion object {
            val USER_ALREADY_EXIST = "User does already exist"
            val FIELDS_EMPTY = "Something missing"
            val NO_USER_REGISTERED = "User isn't registered"
        }
    }
    
    interface UserRepository {
        fun getUserByEmail(email: String): User?
        fun createUser(user: User)
    }
    
    class MemoryUserRepository(initialUsers: Map<String, User> = mapOf()): UserRepository {
        private val users = initialUsers.toMutableMap()
    
        override fun getUserByEmail(email: String): User? {
            return users[email]
        }
    
        override fun createUser(user: User) {
            users[user.email] = user
        }
    }
    
    @Serializable
    data class CreateAccountRequest(val email: String, val username: String, val password: String)
    
    data class User(val email: String, val username: String, val password: String)
    
    @Serializable
    data class BasicApiResponse(val message: String, val success: Boolean)