Skip to content

DenisBronx/NetMock

Repository files navigation

NetMock

NetMock is a powerful testing library that makes it incredibly easy to unit test your network requests. It is compatible with Java, Kotlin, Android, and Kotlin-Multiplatform making it a versatile option for developers working on different platforms.

The library offers a variety of features that can help you test your network requests against any network library, including OkHttp, Ktor, and Retrofit. You can use a mock-like API to test your requests, making the test code much simpler and more readable.

NetMock comes in two different flavors: netmock-server and netmock-engine.

The netmock-server flavor is compatible with Java, Kotlin, and Android, and it is library independent as it allows you to mock network requests by redirecting requests to a localhost web server using MockWebServer. This flavor is perfect for developers that work on non-Multiplatform projects and that want to test their network requests without having to worry about setting up a separate server.

The netmock-engine flavor, on the other hand, is designed specifically for developers using Ktor or working with Kotlin Multiplatform. It allows you to use MockEngine instead of a localhost server, making it a more lightweight and multiplatform option for those working with Ktor.

If your project is not a Kotlin multiplatform project, and you are using a variety of libraries, and you don't want to import both flavors, just use netmock-server as it is compatible with all the libraries including Ktor.

Install

Netmock is available on Maven Central.

For Gradle users, add the following to your module’s build.gradle

dependencies {
    //compatible with all libraries
    testImplementation "io.github.denisbronx.netmock:netmock-server:0.4.0" 
    //mutliplatform and lighter weight option for ktor only library users
    testImplementation "io.github.denisbronx.netmock:netmock-engine:0.4.0"
}

Examples

Initialization

netmock-server initialization

netmock-engine initialization

Mock requests and responses

@Test
fun `my test`() {
    netMock.addMock(
        request = {
            //exact method
            method = Method.Post
            //exact request url: scheme, base url, path, params...
            requestUrl = "https://google.com/somePath?paramKey1=paramValue1"
            //must-have headers, as some clients add extra headers you may not want to check them all
            //if you are using ktor and your response body is a json, you must have "Content-Type: application/json" as header
            mandatoryHeaders = mapOf("a" to "b", "b" to "c")
            //request body, must be a String (this allows you to test your parsing)
            body = """{"id": "2"}"""
            //or, you can read a file in "test/resources"
            //body = readFromResources("requests/request_body.json")
        },
        response = {
            //status code
            code = 200
            //must-have headers
            //if you are using ktor and your response body is a json, you must have "Content-Type: application/json" as header
            mandatoryHeaders = mapOf("a" to "b", "b" to "c")
            //response body, must be a String (this allows you to test your parsing)
            body = """{"data": "text"}"""
            //or, you can read a file in "test/resources"
            //body = readFromResources("responses/response_body.json")
        }
    )
    
    //...
}

Reuse requests and responses

private val request = NetMockRequest(
    method = Method.Post,
    requestUrl = "https://google.com/somePath?paramKey1=paramValue1",
    mandatoryHeaders = mapOf("a" to "b", "b" to "c"),
    body = readFromResources("requests/request_body.json")
)
private val response = NetMockResponse(
    code = 200,
    mandatoryHeaders = mapOf("a" to "b", "b" to "c"),
    body = readFromResources("responses/response_body.json")
)

@Test
fun `my test`() {
    netMock.addMock(request, response)
    
    //...
}

Templates

private val templateRequest = NetMockRequest(
    method = Method.Post,
    requestUrl = "https://google.com/somePath?paramKey1=paramValue1",
    mandatoryHeaders = mapOf("a" to "b", "b" to "c"),
    body = readFromResources("requests/request_body.json")
)
private val templateResponse = NetMockResponse(
    code = 200,
    mandatoryHeaders = mapOf("a" to "b", "b" to "c"),
    body = readFromResources("responses/response_body.json")
)

@Test
fun `my test`() {
    netMock.addMock(
        request = {
            fromRequest(templateRequest)
            method = Method.Put
        },
        response = {
            fromResponse(templateResponse)
            code = 201
        }
    )
    
    //...
}

Mock the same request multiple times

Each mock intercepts only 1 request, once a request is intercepted NetMock will remove the mock from the queue. This allows you to test what your code exactly does. If your code is making the same request multiple times (i.e polling) you would need to add a mock for each expected request:

@Test
fun `my test`() {
    netMock.addMock(request, response)
    netMock.addMock(request, response)
    netMock.addMock(request, response)
    
    //...
}

Verify intercepted requests

If you want to verify that a request has been intercepted you can use netMock.interceptedRequests, which will return a list of all the interceptedRequests by NetMock:

@Test
fun `my test`() {
    netMock.addMock(request, response)
    
    //...
    
    assertEquals(listOf(request), netMock.interceptedRequests)
}

You can also check the requests that have not been intercepted yet with:

netMock.allowedMocks

This will return a NetMockRequestResponse which is the pair of the NetMockRequest:NetMockResponse you previously mocked.

Not mocked requests

By default, requests that are not mocked will produce a 400 Bad Request response and will be logged as errors in the console. You can override this behaviour by setting a default response:

netMock.defaultResponse = NetMockResponse(
    code = 200,
    mandatoryHeaders = mapOf("a" to "b", "b" to "c"),
    body = readFromResources("responses/response_body.json")
)

by doing so, all the not mocked requests will return the specified response and no logs will be printed in the console.

Resources

When working with request and response bodies, it may not be ideal to create string constants in your tests (i.e. long JSONs that compromise tests readability, sharing bodies between test classes...). You can use instead read from a local file in your test resources folder:

//Reads the text from the module's "src/test/resources/responses/products_response_body.json" file
val responseBody = readFromResources("responses/products_response_body.json")

By doing so, the IDE will properly format the file for you and you can even copy/paste HTTP request and response bodies from your real web server to create representative test cases.

Multiplatform Resources

If you are working on a multiplatform project, your resources folder will be located in a different path. Use the following methods for reading the correct files:

Method Resolved path
readFromCommonResources src/commonTest/resources
readFromJvmResources src/jvmTest/resources
readFromNativeResources src/nativeTest/resources