Testing OkHttp with MockWebServer and MockResponse with a Buffer body

6.1k Views Asked by At

I am trying to test some download code involving OkHttp3 and failing miserably. Goal: test downloading an image file and verify it worked. Platform: Android. This code is working in production but the test code just isn't making any sense.

Prod code

class FileDownloaderImpl internal constructor(
    private val ioScheduler: Scheduler,
    private val logger: LoggingInterceptor,
    private val parser: ((String) -> HttpUrl)? // for testing only
) : FileDownloader {

    @Inject constructor(logger: LoggingInterceptor) : this(Schedulers.io(), logger, null)

    override fun downloadFile(url: String, destination: File): Single<File> {
        Logger.d(TAG, "downloadFile\nurl = $url\nfile = $destination")

        val client = OkHttpClient.Builder()
            .addInterceptor(logger)
            .build()

        val call = client.newCall(newRequest(url))
        return Single.fromCallable { call.execute() }
            .doOnDispose { call.cancel() }
            .subscribeOn(ioScheduler)
            .map { response ->
                Logger.d(TAG, "Successfully downloaded board: $response")
                return@map response.body()!!.use { body ->
                    Okio.buffer(Okio.sink(destination)).use { sink ->
                        sink.writeAll(body.source())
                    }
                    destination
                }
            }
    }

    /**
     * Creates the request, optionally parsing the URL into an [HttpUrl]. The primary (maybe only)
     * use-case for that is for wrapping the URL in a `MockWebServer`.
     */
    private fun newRequest(url: String): Request {
        val httpUrl = parser?.invoke(url)
        val builder = Request.Builder()
        httpUrl?.let { builder.url(it) } ?: builder.url(url)
        return builder.build()
    }
}

Test code (JUnit5)

@ExtendWith(TempDirectory::class)
internal class FileDownloaderImplTest {

    private val mockWebServer = MockWebServer()
    private val logger = LoggingInterceptor(HttpLoggingInterceptor.Level.BODY) { msg -> println(msg) }
    private val fileDownloader = FileDownloaderImpl(Schedulers.trampoline(), logger) {
        mockWebServer.url("/$it")
    }

    @BeforeEach fun setup() {
        mockWebServer.start()
    }

    @AfterEach fun teardown() {
        mockWebServer.shutdown()
    }

    @Test fun downloadFile(@TempDir tempDirectory: Path) {
        // Given
        val res = javaClass.classLoader.getResource("green20.webp")
        val f = File(res.path)
        val buffer = Okio.buffer(Okio.source(f)).buffer()
        mockWebServer.enqueue(MockResponse().setBody(buffer))
        val destFile = tempDirectory.resolve("temp.webp").toFile()

        // Verify initial condition
        destFile.exists() shouldBe false

        // When
        fileDownloader.downloadFile("test.html", destFile)

            // Then
            .test()
            .assertValue { file ->
                file.exists() shouldBe true
                file.length() shouldEqualTo 66 // FAIL: always 0
                true
            }
    }
}

More detail

"green20.webp" is a file that exists in app/test/resources. When I debug, all indications are that it exists. On the subject of debugging, I have breakpoints in the prod code and it looks like the Response object (presumably a MockResponse) has no body. I have no idea why that would be.

Current ideas:

  1. I'm not adding a mock response body correctly
  2. The file is somehow "open" and so its length is always 0 even though it is not actually empty.

EDIT

I tried removing the MockWebServer from the test and initiated a real download, and my test actually passed. So, I think I'm doing something wrong with the MockResponse and its body. Any help would be much appreciated.

3

There are 3 best solutions below

1
On

Here is how I fixed the issue:

File file = testFile();
Buffer buffer = Okio.buffer(Okio.source(file)).getBuffer();
Okio.use(buffer, (Function1<Buffer, Object>) buffer1 -> {
     try {
         return buffer1.writeAll(Okio.source(file));
     } catch (IOException e) {
         throw new RuntimeException(e);
     }
});
resourceServiceServer.enqueue(new MockResponse().setResponseCode(200).setHeader(HttpHeaders.CONTENT_TYPE,
    ResourceType.MP3.getMimeType()).setBody(buffer));
0
On

For reasons that are unclear to me, Okio.buffer(Okio.source(file)).buffer() always returned an empty Buffer. The following, however, works:

mockWebServer.enqueue(MockResponse().setBody(Buffer().apply {
    writeAll(Okio.source(file))
}))

What I'm doing now is creating a new buffer manually and writing the entire file into it. Now my MockResponse has a real body.

I would still love for someone to explain the why of this....

1
On

The buffer() method on BufferedSource doesn’t read the entire stream into that buffer to return it. Instead, it just lets you access the bytes it has preloaded from the file, which will be returned on the next read.

This is the code to load a file into a Buffer:

val buffer = Buffer()
file.source().use {
  buffer.writeAll(it)
}