Introducing NetMock: Simplifying HTTP Request Testing in Java, Android, and Kotlin Multiplatform

Let’s get rid of the complexities of simulating requests and responses in testing environments

Denis Brandi
Better Programming

--

NetMock code

I am thrilled to introduce NetMock, a powerful and user-friendly library designed to streamline the mocking of HTTP requests and responses.

Testing HTTP requests can often present challenges for developers due to the complexity of simulating requests and responses in testing environments. This can result in increased time and resources spent on manual testing. Having faced similar struggles with existing HTTP libraries and cumbersome mocking tools, I took it upon myself to create a solution: NetMock.

NetMock is designed to integrate with Java, Android, and Kotlin Multiplatform seamlessly. With minimal setup, you can effortlessly create mocks that accurately mimic client requests and real endpoint responses. One of the key advantages of NetMock is its consistent approach to mocking HTTP requests and responses. This proves especially beneficial when working on projects employing different libraries or considering a transition from one library to another.

Another noteworthy aspect of NetMock is its user-friendly design. Unlike many other HTTP mocking tools, NetMock prioritizes ease of use. The library offers an intuitive interface and a straightforward setup process, catering to developers needing to be better-versed in HTTP libraries.

Whether you are a seasoned developer or just starting, NetMock is the perfect solution for simplifying your workflow and reducing the time and effort required to create robust, reliable software.

If you want to explore NetMock further, you can find the NetMock repository on GitHub at https://github.com/DenisBronx/NetMock.

Improving Testing Strategy for HTTP-Related Code

As a professional software engineer, I firmly believe in the critical importance of comprehensive unit testing for all code. However, I have observed a common practice among developers to neglect unit testing in the network layer due to the challenges involved in mocking and testing this area. Instead, they often rely on end-to-end automated tests or manual testing.

In various projects, I have encountered an approach where HTTP-related code is isolated into separate components to enable independent testing of the remaining system. While this approach can lead to leaner repositories and test classes, it often results in neglecting the testing of the HTTP code itself, as it is considered challenging to test. As a consequence, crucial logic related to HTTP requests and JSON parsing still needs to be validated.

This issue is exemplified in projects that utilize the Retrofit library, which abstracts the HTTP-related logic through interfaces. Consider the following code snippet:

interface GitHubService {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): List<RepoDto>
}

class GitHubRepositoryImpl(
private val githubService: GitHubService,
private val repoMapper: RepoMapper
): GitHubRepository {
override suspend fun listRepos(user: String): List<Repo> {
val repoDtos = githubService.listRepos(user)
return repoMapper.map(repoDtos)
}
}

While this approach can simplify repositories and test classes, it often omits crucial tests for the HTTP-related logic. Additionally, developers may create unnecessary components solely to simplify unit testing of repositories.

Unit testing aims to prevent and identify mistakes made by developers, ensuring that the code functions as intended. Without adequate tests, even simple errors, such as changing the path from users/{user}/repos to user/{user}/repos or renaming a field in RepoDto may go unnoticed, which is unacceptable in a professional project.

My critique is in no way targeted at the Retrofit library itself.

On the contrary, I consider it an exceptional and highly readable library.

Furthermore, I recognize the value of isolating HTTP-related code into separate components for improved organization and maintainability.

Rather, I intend to shed light on a flawed testing strategy I have observed in numerous projects. This strategy often needs to pay more attention to the comprehensive testing of the HTTP code, resulting in potential issues going unnoticed. By highlighting this concern, I encourage developers to prioritize thorough testing of their HTTP-related logic, thus ensuring their projects' overall quality and reliability.

Mocking Requests and Responses with NetMock

@Test
fun `your test`() = runTest {
val user = "some_user"
netMock.addMock(
request = {
method = Method.Get
requestUrl = "https://api.github.com/users/$user/repos"
},
response = {
code = 200
body = readFromResources("responses/repo_list.json")
}
)

val result = sut.listRepos(user)

//...
}

To mock requests and responses, you can utilize the simple API provided by NetMock.

By adding a mock to the queue of expected requests and responses, you can intercept and control the behavior of HTTP interactions during testing.

Creating a NetMock instance

NetMock offers two flavors: netmock-server and netmock-engine.

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

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

Working with NetMockServer

To add netmock-server to your project, add the following to your dependencies in build.gradle file:

dependencies {
testImplementation "io.github.denisbronx.netmock:netmock-server:0.4.0"
}

Next, create a NetMockServer instance as follows:

@get:Rule
val netMock = NetMockServerRule()

NetMockServer starts a localhost server that will intercept your requests using MockWebServer. The NetMockServerRule handles the server’s lifecycle, automatically starting and shutting it down for each test.

Once the server is up and running, you can configure your code to point to the localhost base URL using netmock.baseUrl.
Here are examples of Retrofit and Ktor:

Retrofit example that uses localhost:

@get:Rule
val netMock = NetMockServerRule()

private val service = Retrofit.Builder()
.baseUrl(netMock.baseUrl)
.build()
.create(GitHubService::class.java)

Ktor example that uses localhost:

@get:Rule
val netMock = NetMockServerRule()

private val client = HttpClient(OkHttp) {
defaultRequest {
url(netMock.baseUrl)
}
}

Alternatively, you can add an interceptor using netmock.interceptor, which automatically redirects requests to localhost:

Retrofit example that uses a real URL:

@get:Rule
val netMock = NetMockServerRule()

private val service = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.client(OkHttpClient.Builder().addInterceptor(netMock.interceptor).build())
.build()
.create(GitHubService::class.java)

Ktor example that uses a real URL:

@get:Rule
val netMock = NetMockServerRule()

private val client = HttpClient(OkHttp) {
engine {
addInterceptor(netMock.interceptor)
}
}

An interceptor is recommended, as it allows you to work with real and dynamic URLs.

However, note that you’ll need a library compatible with OkHttp interceptors, such as OkHttp itself, Retrofit, or Ktor.

Final Thoughts

Apart from the initialization process, both flavors of NetMock are essentially identical. This means that transitioning between netmock-server and netmock-engine requires minimal modifications. This flexibility allows you to switch seamlessly between different libraries, such as transitioning from Retrofit to Ktor or vice versa, without modifying your tests.

Decoupling your testing framework from the underlying libraries can enhance your confidence in refactoring. When making changes, you only need to modify your production code while your tests remain intact. This approach significantly reduces the risk of introducing unintended side effects during refactoring and ensures that your tests accurately reflect the behavior of your code.

In summary, NetMock’s versatility empowers you to adapt your testing setup to different libraries and facilitates refactoring with ease, thereby bolstering the reliability and maintainability of your codebase.

Finally, I want to express my sincere gratitude to all the readers who have taken the time to explore this article. I hope the insights provided have shed light on the importance of comprehensive testing in the network layer.

Thank you once again for your attention, and happy testing with NetMock!

--

--