Test REST APIs with Kotlin, Retrofit, and JUnit5

A common concern for any developer is whether their code works. Unit tests catch many simple errors but the only way to truly know whether an application is working as intended is with end-to-end (or E2E) tests. These are tests that emulate user actions to verify aspects of your app that are key to its success.

Consider what is essential for your business: account creation, payment processing, event tracking. You want to know if these actions work - and the best way to do that is with an end to end test.

There are many end-to-end test frameworks out there but a great approach I’ve found is using Kotlin, Retrofit and JUnit5. If you’ve never used Kotlin before it’s really easy to get the hang of, especially if you’ve done Java or even Javascript before.

Goals

Before testing let’s think of an example application and the key features we want to test. For this post we’ll cover purchasing credits on an imaginary API - but you can test anything really.

Setup

Starting a Kotlin project from scratch can be a pain, so lets use Spring Initializr to bootstrap our E2E test project.

curl https://start.spring.io/starter.tgz -d language=kotlin -d type=gradle-project | tar -xzvf -

This command should request a Kotlin Spring Gradle project as a tgz and unzip it in your working directory. Open it up in your favorite editor.

Next we need to add Retrofit and a JSON parser as dependencies to build.gradle. Then we can start writing our tests.

// add to build.gradle
dependencies {
	// retrofit
	testCompile 'com.squareup.retrofit2:retrofit:2.3.0'
	// lib for parsing JSON api responses
	testCompile 'com.squareup.retrofit2:converter-gson:2.3.0'
}

Calling APIs

To test the purchasing of credits we’ll use Retrofit to call our API and JUnit to verify the results.

Retrofit is a really clever HTTP library from Square. You use interfaces and annotations describe an API and then build it into a concrete client using Retrofit reflection. Don’t worry, it’s not as scary as it sounds. Here’s how you use it.

First create an interface for your API. Then a method for an API endpoint - say, getPaymentMethod to fit with our example. Then you annotate the method with an HTTP method and path (@GET("/users/me/payment-method") for example). Lastly, add any additional query params or headers and give the method a response type. Wrap your response entity in Retrofit’s Call class to make the entire method executable.

Here’s the end result:

interface Api {
  @GET("/users/me/payment-method")
  fun getPaymentMethod(
      @Header("authorization") bearerAuth: String
  ): Call<ResponseBody>
}

Pretty neat! But how do we use it? Well we instantiate the interface with Retrofit by passing in a baseUrl. We can actually add this instantiation method to the Api interface itself as a static method.

interface Api {
  // factory methods
  companion object {
    fun create(baseUrl: String): Api {
      val retrofit = Retrofit.Builder()
          // here we set the base url of our API
          .baseUrl(baseUrl)
          // add the JSON dependency so we can handle json APIs 
          .addConverterFactory(GsonConverterFactory.create())
          .build()
      // here we pass a reference to our API interface 
      // and get back a concrete instance
      return retrofit.create(Api::class.java)
    }
  }
  
  // other functions ignored...
}

So all together now.

val api = Api.create("https://yourapi.com")
val response = api.getPaymentMethod("authToken").execute()
// how to get the response values
val body = response.body() // will be of the type you specified in the interface
val code = response.code()

Verifying results

Now its time to actually call our API’s payment method endpoint and verify the result. So in the src/test directory add a new class for our test.

class CreditTest {

    @Test
    fun `can get payment method` () {
      // call the api
      val api = Api.create("yourapi.com")
      val response = api.getPaymentMethod("authToken").execute()
      // verify the response is OK
      assertThat(response.code()).isEqualTo(200)
    }

}

What’s the catch?

Well, did you notice use of the @Header("authorization") in our method signature? That’s because many APIs require OAuth2 authentication. For your test you may want to create a test user and save their authorization token or login details in your test configuration.

This can lead to additional problems however. Auth tokens often expire so logging in with a real username and password is probably a more reliable way to get auth tokens during tests. But that means storing usernames and passwords in your code. It also means that your one test user will have an account littered with records and entities created during tests.

What can we do about this…

Emulating user sign-up

In an ideal world every test would run with a brand new user with a clean account. That way each test user would be isolated from another concurrent test actions. Setting this up manually is a big job! Luckily, MailSlurp provides an affordable API for generating unique email addresses on the fly. We can use this in our tests to:

  • create a unique email address
  • sign up as a new user
  • confirm the users email address (by reading their email)
  • running tests with a completely fresh user and verify app actions

MailSlurp is a simple REST API for sending and receiving emails. We can call it via Retrofit too!

interface MailSlurp {
    
    @POST("/inbox")
    fun createInbox(
        @Header("x-api-key") apiKey: String
    ): Call<Inbox>
    
    data class Inbox(val id: String, val emailAddress: String)
}

For sending and receiving emails, check out the documentation.

Conclusion

There you have it, a very simple E2E test that calls your real API using Retrofit. With any end-to-end tests you often come up against issues when trying to test user sign up and login. As shown above, MailSlurp is a nice addition in these cases. Enjoy!