Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Integration Testing with Ktor Client and Server in Kotlin Multiplatform #550

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

kez-lab
Copy link

@kez-lab kez-lab commented Nov 28, 2024

Hello,

I’ve been delayed in submitting this Pull Request due to other pressing tasks. However, I am now submitting a PR inspired by the excellent advice from the Ktor team regarding integration testing.

The current test cases are kept at a basic level for simplicity, but I am more than willing to add more detailed tests if necessary.

I appreciate your time and consideration in reviewing this PR and welcome any feedback you might have.

Thank you!

Description

This PR provides a guide on performing integration testing in Kotlin Multiplatform (KMP) projects using Ktor. The guide includes:

  • Project structure and Gradle configuration for client-server integration tests
  • Example code snippets to validate client-server interactions and improve test coverage
  • Strategies to identify and resolve potential issues during simultaneous client-server development

This pull request focuses on enhancing test coverage, ensuring system stability, and reducing unexpected issues in production environments.

Changes

  • Explained project structure and Gradle configuration for integration tests
  • Included example tests to validate request-response flows between the client and server
  • Shared best practices for increasing test coverage

Testing

  • All example code snippets were verified in a local KMP project
  • Integration tests confirmed the proper handling of request/response flows between the server and client
  • Stability was further validated using JUnit 5-based testing, ensuring expanded test coverage

Additional Information

This guide proposes methods to go beyond unit testing by verifying system-wide interactions, thereby enhancing test coverage and building a more reliable application.

References

@vnikolova
Copy link
Collaborator

Thanks for your contribution @kez-lab ! 🫶
Could you please update the README.md to include a "Test" section?

@vnikolova
Copy link
Collaborator

@bjhham @osipxd please help to verify the code.

Comment on lines 9 to 22
val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json {
encodeDefaults = true
isLenient = true
coerceInputValues = true
ignoreUnknownKeys = true
})
}
defaultRequest {
host = "1.2.3.4"
port = 8080
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to introduce an extension function here instead. This way you can avoid the static instantiation and reuse the configuration in your test code.

Something like:

fun HttpClient.configuredForApi() = config {
    install(ContentNegotiation) {
        json(Json {
            encodeDefaults = true
            isLenient = true
            coerceInputValues = true
            ignoreUnknownKeys = true
        })
    }
    defaultRequest {
        host = "1.2.3.4"
        port = 8080
    }
}

Copy link
Author

@kez-lab kez-lab Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion! I've updated the code to use an extension function as you recommended. By introducing the configuredForApi extension function, the configuration logic is now reusable across both the main application code 😄

However, in the test code, I utilized testApplication, which does not require the use of the defaultRequest block. Therefore, I created a separate extension function specifically tailored for the test environment.

}

android {
namespace = "io.github.kez_lab.multipatform.full.integrationtest"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep a namespace similar to the one used in composeApp module:

Suggested change
namespace = "io.github.kez_lab.multipatform.full.integrationtest"
namespace = "org.example.ktor.integrationtest"

defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
sourceSets["test"].java.srcDirs("src/test/kotlin")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modern versions of AGP add kotlin/ directory to source sets themself, so we can drop this line

ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-server-content-negotiation-jvm = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-jvm suffix could be dropped when we use Gradle as a build system:

Suggested change
ktor-server-content-negotiation-jvm = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" }
ktor-server-content-negotiation= { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }

import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

fun HttpClient.configuredForApi() = config {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function could both, create and configure an HttpClient, since you don't reuse this extension from tests:

Suggested change
fun HttpClient.configuredForApi() = config {
fun createHttpClient() = HttpClient {

In real life this method would be called in some DI framework providing a singleton HttpClient.

@@ -0,0 +1,42 @@
import core.extension.createConfiguredClient
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a package to tests. You don't have to put the files into physical packages, just add a correct package directive on the top of each file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants