Understand Java I/O Streams By Writing Your Own
Previously, I wrote an article outlining what I learned by writing my own mocks without using a mocking library. After writing even more mocks to test my HTTP server client thread, I can now add a greater understanding of Input and Output streams to that list.
Testing my server’s client thread requires mocking three things: a socket, a router, and a middleware chain. The socket links my server to a client, reading the request it receives from the client and writing a response back to it. After parsing the incoming request and creating a Request
instance, every Request
goes through a middleware chain (which includes logging and authenticating every Request
). The router then sends the final, possibly alteredRequest
to the proper Handler, which creates and returns aResponse
object. Before writing the Request
object back to the client, the Request
goes through the middleware chain, this time for appending the appropriate Cross-Origin Resource Sharing Sharing (CORS) header and for logging. After formatting the Response
object and converting it to bytes, the thread finally uses the socket to write the response to the client.
Mocking the router and middleware is a relatively simple task. For the router, we simply create a MockRouter
that extends Router
. Its constructor receives an expected Response
object, which its one public #getResponse
method can return. For the middleware, we similarly create a MockMiddleware
class which extends Middleware
. As with the MockRouter
, we can pass in an expected Response
to the constructor, which the #applyMiddleware
method can return.
These hard-coded Request
s and Response
s decouple our client thread test from the function of an actual router or any number of actual Middleware
instances. If we want to ensure that multiple middleware classes alter a Response
before being written to the client, we can create and link three instances of MockMiddleware
, each returning a completely different Response
before running assertions against the Response
returned by the last in the chain. This prevents use from using an actual implementation of Middleware
, which could be broken or — more likely — could change in the future. This client thread unit test would become too fragile if it depended on any number of Middleware
instances.
Mocking the socket isn’t quite as easy. For one, it’s a matter of mocking a core Java library. Up until this point, I had only mocked classes that I had written myself. Second, my client thread wraps the InputStream
returned by #getInputStream
in an InputStreamReader
wrapped by a BufferedReader
. I felt like I was a few levels removed from the code I wanted to mock.
My code signaled to me that I’d have to run assertions against the InputStream
and OutputStream
returned by #socket.getInputStream
and #socket.getOutputStream
respectively. Unfortunately, the #write
method in an OutputStream
does not return anything. To make matters more complicated, constructing a Socket
with a stream as a parameter was deprecated. Although I spent some time trying to avoid it, I realized that mocking a socket would also require mocking InputStream
and OutputStream
.
MockInputStream
Creating your own implementation of InputStream isn’t completely outlandish. The documentation states that:
Applications that need to define a subclass of
InputStream
must always provide a method that returns the next byte of input.
That method is the abstract method #read
. Its documentation states that:
Reads the next byte of data from the input stream. The value byte is returned as an
int
in the range0
to255
. If no byte is available because the end of the stream has been reached, the value-1
is returned. This method blocks until input data is available, the end of the stream is detected, or an exception is thrown.
In order to read
from the stream, we’d need somewhere from which to read. Just as with MockRouter
and MockMiddleware
, a mock constructor is the perfect place to inject an expected object. In this case, constructing a MockInputStream
with an array of bytes gives us precisely what we need. By saving the inputBytes
as an instance variable, the #read
method can pop and return the first byte in the array. To ensure that we meet the specification above, we can check the array length and return -1 if it’s already empty. The InputStreamReader
(wrapped in a BufferedReader
) read
ing from the MockInputStream
will take care of stopping when it receives a -1.
All together, the code looks like this:
If you’re familiar with other types of InputStream
s, then you might have noticed that we’ve essentially created our own implementation of ByteArrayInputStream.
MockOutputStream
As above, the documentation also suggests that implementing your own OutputStream is fairly standard.
Applications that need to define a subclass of
OutputStream
must always provide at least a method that writes one byte of output.
That method is the abstract #write(int b)
method. As mentioned above, this method is void, so we can’t actually run assertions against it. To get around that, we’ll create and expose a method unique to MockOutputStream
: #getWrittenBytes
. Because this method will ultimately return something, it will be possible to run assertions against it.
Because two methods (one setting and one getting) will be making use of an object, this class will also make use of an instance variable. In this case, we’ll avoid the primitive array appending and simply instantiate a ByteArrayOutputStream
to an instance variable. Now, whenever the socket write
s a byte, it can write it directly to the ByteArrayOutputStream
instance. Since the method is void, the #write
method has no other functionality.
All together, the code looks like this:
MockSocket
With the two requisite mocks out of the way, the MockSocket
seems quite simple. It’s two mocked methods will simply return mock instances of InputStream
and OutputStream
. To run assertions against whatever the socket writes, we create and expose the #getWrittenString
method, which gets MockOutputStream
‘s written bytes and converts them to a String.
All together, the code looks like this:
Do note that the type for mockInputStream
(line 9) must be MockOutputStream
and not OutputStream
. This is because line 27 calls #getWrittenBytes
— a method that exists only in the mock implementation and not any other implementation of OutputStream
.
When testing the client thread (or any class that uses an instance of the Socket
class), you’ll know be able to control what a test client “sends” the server (as an array of bytes). Similarly, you’ll be able to run assertions against what the server “sends” (conveniently as a String, but you can alter#getWrittenString
method in MockSocket
to return a ByteArrayOutputStream
or a byte array).