Learn how to test Retrofit APIs using MockWebServer: fake HTTP responses, request paths, headers, JSON bodies, successful and error responses, malformed JSON, timeouts, schemas, consumer/provider contracts, backward compatibility and serialization.

API models

data class TaskDto(
    val id: Long,
    val title: String,
    val completed: Boolean
)

data class CreateTaskRequest(
    val title: String
)

data class ApiErrorDto(
    val code: String,
    val message: String
)

Retrofit service

interface TaskApi {

    @GET("v1/tasks/{id}")
    suspend fun getTask(
        @Path("id") id: Long,
        @Header("Authorization") token: String
    ): Response<TaskDto>

    @POST("v1/tasks")
    suspend fun createTask(
        @Header("Authorization") token: String,
        @Body request: CreateTaskRequest
    ): Response<TaskDto>
}

API integration boundary

flowchart LR
    TEST["JUnit test"]
    API["Retrofit interface"]
    CONVERTER["JSON converter"]
    CLIENT["OkHttp client"]
    SERVER["MockWebServer"]

    TEST --> API --> CONVERTER --> CLIENT --> SERVER

Real components:

  • Retrofit
  • OkHttp
  • Converter such as Moshi
  • Interceptors, when included
  • Request annotations

Replaced component:

  • Production backend


Test Setup

class TaskApiTest {

    private lateinit var server: MockWebServer
    private lateinit var api: TaskApi

    @Before
    fun setUp() {
        server = MockWebServer()
        server.start()

        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        api = Retrofit.Builder()
            .baseUrl(server.url("/"))
            .addConverterFactory(
                MoshiConverterFactory.create(moshi)
            )
            .build()
            .create(TaskApi::class.java)
    }

    @After
    fun tearDown() {
        server.shutdown()
    }
}

The Retrofit base URL must end with /.

---

23. Retrofit API Testing

Retrofit API testing verifies the Android client’s HTTP integration.

It checks:

* HTTP method

* Request URL and path

* Query parameters

* Headers

* Request body

* Response conversion

* Error handling

* Timeout handling

* API contract assumptions

What is not being tested?

A MockWebServer test does not verify:

* Production backend logic

* Production DNS

* Production TLS certificates

* Actual production network performance

* Server database behaviour

It verifies that the Android client creates and handles HTTP messages correctly.

Recommended test matrix

ScenarioExpected result
HTTP 200 with valid JSONParsed DTO
HTTP 201 after POSTCreated DTO
HTTP 204Empty successful response
HTTP 400Validation error
HTTP 401Unauthorized error
HTTP 404Not-found error
HTTP 500Server error
Invalid JSONSerialization failure
Slow responseTimeout
Missing optional fieldDefault/null behaviour
Missing required fieldControlled parsing failure
Unexpected extra fieldRemains compatible if allowed

---

24. MockWebServer

MockWebServer is a local programmable HTTP server from OkHttp.

It allows tests to:

* Enqueue responses

* Record requests

* Verify request details

* Simulate errors

* Simulate slow responses

* Return malformed content

* Return responses in sequence

* Dispatch responses by path

sequenceDiagram
    participant Test
    participant Retrofit
    participant MockServer

    Test->>MockServer: Enqueue fake response
    Test->>Retrofit: Call getTask()
    Retrofit->>MockServer: GET /v1/tasks/10
    MockServer-->>Retrofit: HTTP 200 + JSON
    Retrofit-->>Test: Response<TaskDto>
    Test->>MockServer: Inspect recorded request

MockWebServer provides MockResponse for controlled responses and RecordedRequest for checking method, path, body and headers. Official MockWebServer guide.

---

25. Creating Fake HTTP Responses

Successful response

server.enqueue(
    MockResponse.Builder()
        .code(200)
        .addHeader(
            "Content-Type",
            "application/json"
        )
        .body(
            """
            {
              "id": 10,
              "title": "Learn API testing",
              "completed": false
            }
            """.trimIndent()
        )
        .build()
)

Error response

server.enqueue(
    MockResponse.Builder()
        .code(400)
        .addHeader(
            "Content-Type",
            "application/json"
        )
        .body(
            """
            {
              "code": "INVALID_TITLE",
              "message": "Title is required"
            }
            """.trimIndent()
        )
        .build()
)

No-content response

server.enqueue(
    MockResponse.Builder()
        .code(204)
        .build()
)

Depending on the OkHttp version, older projects may use:

MockResponse()
    .setResponseCode(200)
    .setBody("{}")

Keep test APIs consistent with the project’s installed OkHttp version.

---

26. Verifying Request Path

Use takeRequest() to obtain the request received by MockWebServer.

@Test
fun getTask_sendsCorrectMethodAndPath() = runTest {
    server.enqueue(successfulTaskResponse())

    api.getTask(
        id = 10,
        token = "Bearer test-token"
    )

    val request = server.takeRequest()

    assertEquals("GET", request.method)
    assertEquals("/v1/tasks/10", request.url.encodedPath)
}

Query parameter example

@GET("v1/tasks")
suspend fun getTasks(
    @Query("page") page: Int,
    @Query("completed") completed: Boolean
): Response<List<TaskDto>>
val request = server.takeRequest()

assertEquals("1", request.url.queryParameter("page"))
assertEquals(
    "true",
    request.url.queryParameter("completed")
)

Test:

* HTTP method

* Encoded path

* Path parameters

* Query parameters

* Optional query omission

* URL encoding

* Dynamic @Url behaviour

---

27. Verifying Headers and Body

Header verification

@Test
fun createTask_sendsAuthorizationAndJsonBody() = runTest {
    server.enqueue(successfulTaskResponse(code = 201))

    api.createTask(
        token = "Bearer test-token",
        request = CreateTaskRequest(
            title = "Learn MockWebServer"
        )
    )

    val recorded = server.takeRequest()

    assertEquals(
        "Bearer test-token",
        recorded.headers["Authorization"]
    )

    assertTrue(
        recorded.headers["Content-Type"]
            ?.startsWith("application/json") == true
    )
}

Body verification

Avoid comparing raw JSON strings because whitespace and field order may vary.

Parse the body:

val rawBody = recorded.body?.utf8().orEmpty()

val adapter =
    moshi.adapter(CreateTaskRequest::class.java)

val requestBody = adapter.fromJson(rawBody)

assertEquals(
    CreateTaskRequest("Learn MockWebServer"),
    requestBody
)

Important banking example

Verify that a payment request contains:

* Amount

* Currency

* Beneficiary ID

* Idempotency key

* Request ID

* Authorization header

Do not use real tokens or personal information in tests.

---

28. Successful-Response Testing

Test that Retrofit correctly converts a valid HTTP response into the expected model.

@Test
fun getTask_http200_parsesTaskDto() = runTest {
    server.enqueue(
        MockResponse.Builder()
            .code(200)
            .addHeader(
                "Content-Type",
                "application/json"
            )
            .body(
                """
                {
                  "id": 10,
                  "title": "Retrofit testing",
                  "completed": false
                }
                """.trimIndent()
            )
            .build()
    )

    val response = api.getTask(
        id = 10,
        token = "Bearer test-token"
    )

    assertTrue(response.isSuccessful)

    assertEquals(
        TaskDto(
            id = 10,
            title = "Retrofit testing",
            completed = false
        ),
        response.body()
    )
}

Test successful cases such as:

* 200 OK

* 201 Created

* 202 Accepted

* 204 No Content

* Empty list

* Optional fields

* Pagination metadata

* Response headers

Do not assume every successful response contains a body.

---

29. HTTP Error-Response Testing

For a Retrofit function returning `Response<T>`, HTTP error responses are represented by unsuccessful `Response` objects.
@Test
fun getTask_http404_returnsNotFoundResponse() = runTest {
    server.enqueue(
        MockResponse.Builder()
            .code(404)
            .addHeader(
                "Content-Type",
                "application/json"
            )
            .body(
                """
                {
                  "code": "TASK_NOT_FOUND",
                  "message": "Task does not exist"
                }
                """.trimIndent()
            )
            .build()
    )

    val response = api.getTask(
        id = 999,
        token = "Bearer test-token"
    )

    assertFalse(response.isSuccessful)
    assertEquals(404, response.code())
    assertNull(response.body())
    assertNotNull(response.errorBody())
}

Return-type difference

Returning Response<TaskDto>

suspend fun getTask(): Response<TaskDto>

The caller inspects:

response.isSuccessful
response.code()
response.body()
response.errorBody()

Returning TaskDto

suspend fun getTask(): TaskDto

Non-successful HTTP responses normally cause Retrofit to throw HttpException.

@Test
fun getTask_http401_throwsHttpException() = runTest {
    server.enqueue(
        MockResponse.Builder()
            .code(401)
            .build()
    )

    val error = runCatching {
        apiReturningBody.getTask()
    }.exceptionOrNull()

    assertTrue(error is HttpException)
    assertEquals(401, (error as HttpException).code())
}

Test important statuses:

StatusPossible mapping
400Invalid request
401Session expired
403Forbidden
404Not found
409Conflict
422Validation failure
429Rate limited
500–599Server error

Do not treat every non-200 response as the same error.

---

30. Malformed JSON Testing

Malformed JSON verifies that the client handles invalid server content safely.

@Test
fun getTask_malformedJson_failsParsing() = runTest {
    server.enqueue(
        MockResponse.Builder()
            .code(200)
            .addHeader(
                "Content-Type",
                "application/json"
            )
            .body(
                """
                {
                  "id": 10,
                  "title":
                }
                """.trimIndent()
            )
            .build()
    )

    val error = runCatching {
        api.getTask(
            id = 10,
            token = "Bearer test-token"
        )
    }.exceptionOrNull()

    assertNotNull(error)
}

Malformed-response cases include:

* Invalid JSON syntax

* Wrong data type

* Missing required field

* Unexpected null

* Invalid enum

* Invalid date

* Empty response body

* HTML error page returned as JSON

* Truncated body

Repository tests should verify conversion into a stable application error:

assertEquals(
    DataError.InvalidResponse,
    result.error
)

Avoid asserting a converter’s exact internal exception class unless the application depends on it.

---

31. Timeout Testing

A timeout test verifies behaviour when the server responds too slowly.

Client with short timeout

val client = OkHttpClient.Builder()
    .readTimeout(500, TimeUnit.MILLISECONDS)
    .build()

Slow fake response

server.enqueue(
    MockResponse.Builder()
        .code(200)
        .body(
            """
            {
              "id": 10,
              "title": "Slow task",
              "completed": false
            }
            """.trimIndent()
        )
        .throttleBody(
            1,
            1,
            TimeUnit.SECONDS
        )
        .build()
)

Test

@Test
fun getTask_slowResponse_throwsTimeout() = runTest {
    val error = runCatching {
        apiWithShortTimeout.getTask(
            id = 10,
            token = "Bearer test-token"
        )
    }.exceptionOrNull()

    assertTrue(
        error is SocketTimeoutException ||
            error is InterruptedIOException
    )
}

MockWebServer supports slow responses, including throttled bodies, for testing timeout behaviour. Official MockWebServer guide.

Test:

* Connect timeout

* Read timeout

* Write timeout

* Call timeout

* Repository error mapping

* Retry policy

* Cancellation

* Partial response

Payment warning

After a payment timeout, do not immediately assume failure or blindly resubmit. The request may have reached the server. Check transaction status using an idempotency key or transaction ID.

---

32. Contract-Testing Fundamentals

An API contract defines the agreement between client and server.

It includes:

* HTTP method

* Endpoint

* Request headers

* Query/path parameters

* Request schema

* Response schema

* Status codes

* Nullability

* Field types

* Error format

* Authentication

* Versioning rules

flowchart LR
    APP["Android consumer"]
    CONTRACT["API contract"]
    SERVER["Backend provider"]

    APP --> CONTRACT
    SERVER --> CONTRACT

A contract test answers:

> Does the Android application’s expectation match what the backend promises?

Example contract

GET /v1/tasks/{id}

200:
{
  "id": number, required
  "title": string, required
  "completed": boolean, required
}

404:
{
  "code": string, required
  "message": string, required
}

OpenAPI provides a machine-readable standard for describing endpoints, operations, parameters, authentication and schemas. OpenAPI Specification.

---

33. Consumer and Provider Contracts

Consumer

The system calling the API:

Android application

The consumer specifies what it requires:

* Fields it reads

* Data types

* Status codes it handles

* Required headers

* Error structures

Provider

The system serving the API:

Backend service

The provider verifies that its implementation satisfies the consumer’s expectation.

sequenceDiagram
    participant Android as Android consumer
    participant Contract
    participant Backend as API provider

    Android->>Contract: Define required interaction
    Backend->>Contract: Verify implementation
    Contract-->>Android: Compatible
    Contract-->>Backend: Compatible

Example

Consumer expectation:

Given task 10 exists
When GET /v1/tasks/10 is requested
Then return:
Status 200
Content-Type application/json
Body includes id, title and completed

Provider verification should execute this interaction against the provider implementation.

Contract tests complement, rather than replace:

* Android MockWebServer tests

* Backend unit tests

* Full integration tests

* End-to-end tests

---

34. Request-Schema Testing

Request-schema testing verifies that the Android client sends the correct structure.

Expected request

{
  "title": "Learn contract testing"
}

Test

@Test
fun createTask_sendsExpectedRequestSchema() = runTest {
    server.enqueue(
        successfulTaskResponse(code = 201)
    )

    api.createTask(
        token = "Bearer test-token",
        request = CreateTaskRequest(
            title = "Learn contract testing"
        )
    )

    val recorded = server.takeRequest()
    val json = recorded.body?.utf8().orEmpty()

    val parsed =
        moshi.adapter(CreateTaskRequest::class.java)
            .fromJson(json)

    assertNotNull(parsed)
    assertEquals(
        "Learn contract testing",
        parsed?.title
    )
}

Verify:

* Required fields exist

* Field names are correct

* Types are correct

* Null fields follow the contract

* Optional fields are omitted or included correctly

* Enum values are supported

* Date format is correct

* Numeric precision is preserved

* Sensitive values are not accidentally included

For payments, verify money uses the agreed representation, such as minor units:
{
  "amountMinor": 10050,
  "currency": "INR"
}

This avoids floating-point ambiguity.

---

35. Response-Schema Testing

Response-schema testing verifies that the client correctly handles the provider’s promised response.

Expected response

{
  "id": 10,
  "title": "Learn testing",
  "completed": false
}

Test required fields

@Test
fun response_withRequiredFields_isDeserialized() = runTest {
    server.enqueue(
        jsonResponse(
            """
            {
              "id": 10,
              "title": "Learn testing",
              "completed": false
            }
            """
        )
    )

    val response = api.getTask(
        id = 10,
        token = "Bearer test-token"
    )

    assertEquals(10, response.body()?.id)
    assertEquals(
        "Learn testing",
        response.body()?.title
    )
}

Test:

* Required field present

* Optional field missing

* Nullable field is null

* Wrong field type

* Unknown enum

* Extra field

* Empty collection

* Nested model

* Error-response schema

* Pagination structure

A test that parses only the happy-path fixture is not enough.

---

36. Backward Compatibility

A backend change is backward-compatible when older application versions can continue working.

Usually compatible changes

* Add an optional response field

* Add a new endpoint

* Add an optional request parameter

* Add a new optional header

* Extend behaviour without changing existing meanings

Common breaking changes

* Remove a field

* Rename a field

* Change a field type

* Make an optional field required

* Change nullability

* Change date format

* Change endpoint path

* Change status-code meaning

* Add an enum value when the client cannot handle unknown values

* Change error-body structure

Compatibility test

@Test
fun responseWithAdditionalField_stillParses() = runTest {
    server.enqueue(
        jsonResponse(
            """
            {
              "id": 10,
              "title": "Testing",
              "completed": false,
              "priority": "HIGH"
            }
            """
        )
    )

    val response = api.getTask(
        id = 10,
        token = "Bearer test-token"
    )

    assertEquals(10, response.body()?.id)
}

Safer enum handling

Backend:

{
  "status": "REVERSED"
}
If the app understands only `SUCCESS` and `FAILED`, parsing may fail.

Use a safe fallback where supported:

enum class TransactionStatus {
    SUCCESS,
    FAILED,
    PENDING,
    UNKNOWN
}

Backward compatibility matters because users do not all update Android applications immediately.

---

37. Serialization and Deserialization Testing

Serialization

Converts a Kotlin object into JSON.

CreateTaskRequest → JSON

Deserialization

Converts JSON into a Kotlin object.

JSON → TaskDto
flowchart LR
    REQUEST["Kotlin request"]
    SER["Serialize"]
    JSON1["Request JSON"]
    JSON2["Response JSON"]
    DES["Deserialize"]
    DTO["Kotlin DTO"]

    REQUEST --> SER --> JSON1
    JSON2 --> DES --> DTO

Serialization test

@Test
fun createTaskRequest_serializesCorrectly() {
    val adapter =
        moshi.adapter(CreateTaskRequest::class.java)

    val json = adapter.toJson(
        CreateTaskRequest("Learn JSON")
    )

    val parsed =
        adapter.fromJson(json)

    assertEquals(
        CreateTaskRequest("Learn JSON"),
        parsed
    )
}

Deserialization test

@Test
fun validJson_deserializesToTaskDto() {
    val adapter =
        moshi.adapter(TaskDto::class.java)

    val result = adapter.fromJson(
        """
        {
          "id": 10,
          "title": "Learn JSON",
          "completed": false
        }
        """.trimIndent()
    )

    assertEquals(
        TaskDto(
            id = 10,
            title = "Learn JSON",
            completed = false
        ),
        result
    )
}

Test:

* Custom JSON field names

* Nullable fields

* Default values

* Nested objects

* Lists

* Enums

* Dates and time zones

* Numeric precision

* Unknown fields

* Missing fields

* Invalid data types

Retrofit uses converter factories to transform HTTP representations into application types. Retrofit converter API.

---

38. Integration-Testing Common Mistakes

1. Calling the production backend from automated tests

2. Using real authentication credentials

3. Hardcoding a fixed localhost port

4. Forgetting to shut down MockWebServer

5. Using the production base URL accidentally

6. Testing only HTTP 200

7. Ignoring 201, 202 and 204

8. Treating every HTTP error identically

9. Comparing raw JSON strings

10. Ignoring request headers

11. Not verifying HTTP method or path

12. Not testing query-parameter encoding

13. Ignoring empty response bodies

14. Not testing malformed JSON

15. Not testing missing required fields

16. Not testing extra response fields

17. Using long real waits for timeout tests

18. Retrying every error

19. Retrying non-idempotent requests blindly

20. Assuming timeout means the server did nothing

21. Testing converter implementation rather than application requirements

22. Making the mock response unrealistically different from the API contract

23. Duplicating outdated JSON fixtures across many tests

24. Not version-controlling contract fixtures

25. Updating consumer tests without coordinating backend changes

26. Considering MockWebServer tests to be provider tests

27. Not checking backward compatibility

28. Failing on unknown enum values without a fallback strategy

29. Using floating-point numbers for financial amounts

30. Exposing parsing or HTTP exceptions directly to the UI

31. Verifying exact implementation details of interceptors

32. Allowing tests to depend on request order unnecessarily

---

Complete Banking Use Case: Payment API

Request

{
  "beneficiaryId": "B101",
  "amountMinor": 100000,
  "currency": "INR",
  "idempotencyKey": "unique-key"
}

Recommended tests

ScenarioVerification
Valid paymentCorrect POST path and body
AuthenticationAuthorization header
Request tracingRequest ID header
Duplicate protectionIdempotency key present
HTTP 201Successful transaction parsed
HTTP 202Pending transaction parsed
HTTP 400Validation error mapped
HTTP 401Session-expired handling
HTTP 409Duplicate/conflict handled
HTTP 429Rate-limit error handled
HTTP 500Server error mapped
Malformed JSONInvalid-response error
TimeoutStatus inquiry, not blind resubmission
Additional response fieldOld client still parses
New unknown statusMaps safely to unknown

---

Interview Questions and Answers

1. What does a Retrofit integration test verify?

It verifies request creation, HTTP communication, conversion and response/error handling across Retrofit, OkHttp and the converter.

2. What is MockWebServer?

It is a programmable local HTTP server used to return controlled responses and record client requests.

3. Why use MockWebServer instead of mocking Retrofit?

MockWebServer exercises real HTTP request creation, OkHttp behaviour and serialization.

4. Does MockWebServer test the production backend?

No. It tests the client against a simulated server.

5. What is MockResponse?

It defines a response that MockWebServer should return, including status, headers, body and timing.

6. What is RecordedRequest?

It represents a request received by MockWebServer and exposes its method, URL, headers and body.

7. How do you verify a request path?

Call takeRequest() and assert its URL path and query parameters.

8. How should JSON request bodies be compared?

Deserialize them into a model or JSON structure instead of comparing raw strings.

9. How do you test a successful Retrofit response?

Enqueue valid JSON with a successful status, call the API and assert the parsed DTO.

10. How do you test an HTTP error?

Enqueue an error status and body, then assert the response code, error body or thrown HttpException.

11. When does Retrofit throw HttpException?

A suspend API returning a body directly normally throws it for non-successful HTTP responses.

12. What happens when a Retrofit API returns Response<T>?

The caller receives success and error responses and must inspect isSuccessful, code, body and errorBody.

13. What is the difference between an HTTP error and a network error?

An HTTP error contains a server response such as 404 or 500. A network error occurs when a valid HTTP response is not received, such as timeout or DNS failure.

14. How do you test malformed JSON?

Return invalid or contract-breaking JSON and verify a parsing failure or mapped invalid-response error.

15. How do you test a timeout?

Use a short client timeout and configure MockWebServer to delay or throttle the response.

16. Should a timed-out payment be retried immediately?

No. First check its status because the original request may have reached the server.

17. What is an API contract?

It is the agreement covering endpoints, methods, parameters, headers, schemas, status codes and errors.

18. What is a consumer in contract testing?

The consumer is the system calling the API, such as the Android application.

19. What is a provider?

The provider is the backend service implementing the API.

20. What is consumer-driven contract testing?

Consumers publish the interactions they require, and providers verify their implementations against those contracts.

21. Do contract tests replace integration tests?

No. They complement client integration, provider integration and end-to-end testing.

22. What is request-schema testing?

It verifies that the client sends required fields with correct names, types, formats and nullability.

23. What is response-schema testing?

It verifies that returned data matches the structure and types expected by the client.

24. What is OpenAPI?

It is a language-independent standard for describing HTTP API operations, parameters, security and schemas.

25. What is a backward-compatible API change?

A change that allows existing clients to continue operating correctly.

26. Is adding a response field normally backward-compatible?

Usually yes, provided clients tolerate unknown fields.

27. Is removing a response field backward-compatible?

Usually no when existing consumers depend on that field.

28. Why can adding an enum value break an app?

A client with strict enum parsing may not recognize the new value and can fail deserialization.

29. How can unknown enum values be handled?

Map unrecognized values to a safe UNKNOWN state where the serialization library and business rules allow it.

30. What is serialization?

Converting an application object into a transport representation such as JSON.

31. What is deserialization?

Converting JSON into an application object.

32. What serialization cases should be tested?

Field names, nullability, defaults, enums, dates, nested objects and numeric precision.

33. Why should financial amounts avoid floating-point representations?

Floating-point values can introduce precision errors. Prefer agreed minor-unit integers or exact decimal types.

34. Why must the Retrofit base URL end with /?

Retrofit resolves relative endpoint paths against a directory-style base URL.

35. Should authentication tokens be hardcoded in tests?

Use fake non-sensitive tokens only. Never place real credentials in source code.

36. What is the most important Retrofit test principle?

Verify the Android client’s contract and behaviour using controlled, realistic responses without depending on the production server.

---

Quick Revision

Retrofit test:
Real Retrofit + OkHttp + converter
Fake backend through MockWebServer

MockResponse:
Controls status, headers, body and timing

RecordedRequest:
Verifies method, path, query, headers and body

HTTP error:
Server returned a non-success status

Network error:
No valid HTTP response received

Contract:
Agreement between Android consumer and backend provider

Request schema:
What Android sends

Response schema:
What Android expects

Backward compatibility:
Old app versions continue to work

Serialization:
Kotlin object → JSON

Deserialization:
JSON → Kotlin object