Hiroaki
Write idiomatic API integration tests using Kotlin (Unit and Instrumentation)
Install / Use
/learn @JorgeCastilloPrz/HiroakiREADME
Hiroaki 
<img src="https://drive.google.com/uc?id=1dUvJF0sBQCncUdLJ7z6Q7ZVHdD5WKLS0" width="256" height="256" />
Japanese: 'spreading brightness'. Derived from the words 'hiro', which means 'large or wide', and 'aki',
which means 'bright or clear'.
The intention of Hiroaki is to achieve clarity on your API integration tests in an idiomatic way by leveraging the power of Kotlin.
It uses MockWebServer to provide a mock server as a target for your HTTP requests that you'll use to mock your backend.
That enables you to assert over how your program reacts to some predefined server & API behaviors.
Dependency
For Android, add the following dependencies to your build.gradle. Both dependencies are available in Maven Central.
dependencies{
testImplementation 'me.jorgecastillo:hiroaki-core:0.2.3'
androidTestImplementation 'me.jorgecastillo:hiroaki-android:0.2.3' // Android instrumentation tests
}
Note that Hiroaki only targets AndroidX. It does not provide support for Android support libraries anymore.
If you do plain Java or Kotlin you'll just need the core artifact on its 0.2.3 version.
Setup
To work with Hiroaki you must extend MockServerSuite on your test class, which takes care of running and shutting
down the server for you. If you can't do that, there's also a JUnit4 Rule called MockServerRule with the same goal.
To target the mock server with your requests, you'll need to request the URL from it and pass it to your endpoint creation system / collaborator / entity.
Here you have a plain OkHttp sample.
class GsonNewsNetworkDataSourceTest : MockServerSuite() {
private lateinit var dataSource: GsonNewsNetworkDataSource
@Before
override fun setup() {
super.setup()
val mockServerUrl = server.url("/v2/news")
dataSource = NewsDataSource(mockServerUrl)
}
/*...add tests here!...*/
}
/*Some random data source, probably on a different file*/
class NewsDataSource(var baseUrl: HttpUrl) {
fun getNews(): String? {
val client = OkHttpClient()
val request = Request.Builder()
.url(baseUrl)
.build()
val response = client.newCall(request).execute()
return response.body()?.string()
}
}
If you have an endpoint factory, or even a DI system providing injected endpoints, you'll need to have a good design on your app to pass the mock server url to it. That's on you and is different for every project.
Syntax for Retrofit
However, Hiroaki provides syntax for waking up mock Retrofit services in case you need one for writing some unit
tests for your api client / data source as the subject under test.
class GsonNewsNetworkDataSourceTest : MockServerSuite() {
private lateinit var dataSource: GsonNewsNetworkDataSource
@Before
override fun setup() {
super.setup()
// Use server.retrofitService() to build the service targeting the mock URL
dataSource = GsonNewsNetworkDataSource(server.retrofitService(
GsonNewsApiService::class.java,
GsonConverterFactory.create()))
}
/*...*/
}
This will use a default OkHttpClient instance created for you with basic configuration. For more detailed
configuration, retrofitService() function offers an optional parameter to pass a custom OkHttpClient:
val customClient = OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS)
.writeTimeout(2, TimeUnit.SECONDS)
.build()
dataSource = GsonNewsNetworkDataSource(server.retrofitService(
GsonNewsApiService::class.java,
GsonConverterFactory.create(),
okHttpClient = customClient))
JUnit4 Rule
As mentioned before, here you have the alternative JUnit4 rule to avoid using extension if that's your need:
@RunWith(MockitoJUnitRunner::class)
class RuleNetworkDataSourceTest {
private lateinit var dataSource: JacksonNewsNetworkDataSource
@get:Rule val rule: MockServerRule = MockServerRule()
@Before
fun setup() {
dataSource = JacksonNewsNetworkDataSource(rule.server.retrofitService(
JacksonNewsApiService::class.java,
JacksonConverterFactory.create()))
}
@Test
fun sendsGetNews() {
// you'll need to call the server through the rule
rule.server.whenever(GET, "v2/top-headlines")
.thenRespond(success(jsonBody = fileBody("GetNews.json")))
// Can also inline a body or use the json DSL
runBlocking { dataSource.getNews() }
/*...*/
}
}
Mocking Responses
With Hiroaki, you can mock request responses as if it was mockito:
@Test
fun chainResponses() {
server.whenever(Method.GET, "v2/top-headlines")
.thenRespond(success(jsonBody = fileBody("GetNews.json")))
// Can also inline a body or use the json DSL
val news = runBlocking { dataSource.getNews() }
/*...*/
}
This ensures that whenever the endpoint v2/top-headlines is called with the given conditions the server will
respond with the mocked response we're providing.
These are all the supported params for whenever that you can match to. All of them are optional except sentToPath:
server.whenever(method = Method.GET,
sentToPath = "v2/top-headlines",
queryParams = params("sources" to "crypto-coins-news",
"apiKey" to "21a12ef352b649caa97499bed2e77350"),
jsonBody = fileBody("GetNews.json"), // (file, inline, or JsonDSL)
headers = headers("Cache-Control" to "max-age=640000"))
.thenRespond(success(jsonFileName = "GetNews.json"))
Also note in the previous snippets the success() function when mocking the response. function success() is a
shortcut to provide a mocked successful response. You can also use error() and response(). All of them are mocking
functions that allow you to pass the following optional arguments:
codeInt return http status code for the mocked response.jsonBodyJsonBody, JsonFileBody, Json or JsonArray: json for your mocked response body.headersIs a Map<String,String> headers to attach to the mocked response.
If you don't want to use the succes(), error() or response() shortcut functions, you can still pass your own
custom MockResponse.
Chaining Mocked Responses
You can also chain a bunch of mocked responses:
server.whenever(Method.GET, "v2/top-headlines")
.thenRespond(success(jsonBody = fileBody("GetNews.json")))
.thenRespond(success(jsonBody = fileBody("GetSingleNew.json")))
.thenRespond(success(jsonBody = fileBody("GetNews.json")))
Each time the endpoint is called under the given conditions, the server will return the next mocked response from the list, following the same order.
Dynamic dispatch
Sometimes you want a response to depend on the request sent. For that reason, Hiroaki provides the thenDispatch
method:
server.whenever(Method.GET, "v2/top-headlines")
.thenDispatch { request -> success(jsonBody = inlineBody("{\"requestPath\" : ${request.path}}")) }
You can combine as many thenRespond() and thenDispatch() calls as you want.
server.whenever(Method.GET, "v2/top-headlines")
.thenRespond(success())
.thenDispatch { request -> success(jsonBody = inlineBody("{\"requestPath\" : ${request.path}}")) }
.thenRespond(error())
Delay Responses
Mimic server response delays with delay(), an extension function for MockResponse to pass a delay in
millis: response.delay(millis):
server.whenever(Method.GET, "v2/top-headlines")
.thenRespond(success(jsonBody = fileBody("GetNews.json")).delay(250))
.thenRespond(success(jsonBody = fileBody("GetSingleNew.json")).delay(500))
.thenRespond(success(jsonBody = fileBody("GetNews.json")).delay(1000))
// Also for dispatched responses
server.whenever(Method.GET, "v2/top-headlines")
.thenDispatch { request -> success().delay(250) }
Throttle response bodies
Sometimes you want to emulate bad network conditions, so you can throttle your response body like:
server.whenever(GET, "v2/top-headlines").thenRespond(error().throttle(64, 1000))
Here, you are asking the server to throttle and write chunks of 64 bytes per second (1000 millis).
Verifying Requests
Hiroaki provides a highly configurable verify() function to perform verification over executed HTTP requests.
Its arguments are optional so you're free to configure the assertion in a way that matches your needs.
@Test
fun verifiesCall() {
server.whenever(Method.GET, "v2/top-headlines")
.thenRespond(success(jsonBody = fileBody("GetNews.json")))
.thenRespond(success(jsonBody = fileBody("GetSingleNew.json")))
.thenRespond(success(jsonBody = fileBody("GetNews.json")))
runBlocking {
dataSource.getNews()
dataSource.getSingleNew()
dataSource.getNews()
}
server.verify("v2/top-headlines").called(
times = times(2),
order = order(1, 3),
method = Method.POST,
headers = headers("Cache-Control" to "max-age=640000"),
queryParams = params(
"sources" to "cr
