article banner

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.

Summary

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.