Traits for testing in Kotlin
Using traits for testing is a fairly popular pattern in Kotlin. Think of integration tests in backend development: They need to simulate calling this application endpoints. We like to extract that into functions, like this one:
fun TestApplicationEngine.requestRegisterUser(
token: String = aUserToken,
request: RegisterUserRequest = aRegisterUserRequest
): UserJson? =
handleRequest(HttpMethod.Post, "/api/user/google") {
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
addHeader("token", token)
setBody(request.toJson())
}.let {
assertTrue(it.requestHandled)
it.response.content?.fromJson<UserJson>()
}
Since we might have many endpoints, we like to group them together. A popular way is to define them in interfaces. We will call them traits, and suffix their names with "Trait".
By definition, a trait is a concept representing a set of methods that can be used to extend the functionality of a class. Tere you can find a deeper explanation of this pattern.
interface UserApiTrait {
fun TestApplicationEngine.requestRegisterUser(
token: String = aUserToken,
request: RegisterUserRequest = aRegisterUserRequest
): UserJson? = TODO() // ...
fun TestApplicationEngine.requestGetUserSelf(
token: String
): UserJson? = TODO() // ...
...
}
interface CourseTrait {
fun TestApplicationEngine.getCourse(
courseKey: String,
token: String = aUserToken
): UserCourseJson? = TODO() // ...
fun TestApplicationEngine.getCourses(
token: String = aUserToken
): List<UserCourseJson>? = TODO() // ...
}
Now each test class can define what traits it needs to use. For example, ArticlesApiTests
might need ArticlesTrait
and UserApiTrait
, where CourseApiTests
might need CourseTrait
and UserApiTrait
.
The patterns work perfectly well, as long as those traits do not need to access any test objects. But what if they do? For instance, when we login with Google, a good practice is to verify the data with Google. This is what I defined here as GoogleAccountVerifier
. For tests, it needs to be mocked. Although, some data needs to be set there, if we want to register a user on our tests. Therefore, the method requestRegisterUser
should have access to the fake GoogleAccountVerifier used in the project. It is even more important, if we want to define methods like userExists
or adminExists
that are used to setup situation for other integration tests.
@Test
fun `should create an Effective Kotlin coupon as a course step`() = withTestApplication(::integrationModule) {
// given
randomStringProvider.textToReturn = "ABCDEFGH"
val user = userExists(aUserToken)
adminExists(aUserToken2)
requestPutUser(aUserToken2, user.id, PutUserRequest(tags = listOf(Tag.KOTLIN_WORKSHOP_ATTENDEE)))
// when
val course = getCourse("effective-kotlin", aUserToken)
// then
assertNotNull(course)
val firstCourseStep = course.steps.first()
val expectedCourseStep = UserCourseStep(
LINK,
"https://leanpub.com/effectivekotlin/c/ABCDEFGH",
"Effective Kotlin book personal coupon",
READY
)
assertEquals(expectedCourseStep, firstCourseStep)
}
A common practice is that such fake objects (like googleAccountVerifier
or randomStringProvider
) are defined on some base integration test class (in this case, I called it IntegrationTest
).
abstract class IntegrationTest {
protected val googleAccountVerifier = FakeGoogleAccountVerifier()
protected val randomStringProvider = FakeRandomStringProvider()
// ...
}
So, how can we let trait access these fakes? Time for a trick, that every Kotlin developer using this pattern should know. We need to define an interface with the property we need to access. We can name it FakeGoogleAccountVerifierProvider
, but it might as well hold more values and have more generic name. We will implement it both by IntegrationTest
and by UserApiTrait
.
UserApiTrait
can use the property, because interface makes sure it will be present in the final object. IntegrationTest
overrides it, so tests do not need to. This way traits can use objects defined by the base class.
interface FakeGoogleAccountVerifierProvider {
val googleAccountVerifier: FakeGoogleAccountVerifier
}
abstract class IntegrationTest: FakeGoogleAccountVerifierProvider {
override val googleAccountVerifier = FakeGoogleAccountVerifier()
// ...
}
interface UserApiTrait: FakeGoogleAccountVerifierProvider {
fun TestApplicationEngine.requestRegisterUser(
token: String = aUserToken,
request: RegisterUserRequest = aRegisterUserRequest,
googleUserData: GoogleUserData = aGoogleUserData,
): UserJson? {
googleAccountVerifier.register(request.googleToken, googleUserData)
// ...
}
}
class UserApiTests : IntegrationTest(), UserApiTrait {
// ...
}
This is the last puzzle needed when we use traits in Kotlin.
The traits pattern is popular in more dynamic languages, like Groovy. In Kotlin, I often see it used for testing, especially integration testing on backend applications. In Allegro, where I work, it is used widely and seems to work really well for us. I hope you know how to use it as well, and it might help you organize requests on your integration tests.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, is known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.