( Note: You can read the full series at: https://blog.mandraketech.in/series/java-http-server )
As I started the journey down the test route, the first thought was to unit test the individual Handlers. This required the implementation of a HttpExchange
object. As I delved into the specific APIs to be implemented for a MockHttpExchange
it turned out that there is a specific set of sequence the APIs should be called. As per the documentation, the sendResponseHeaders
needs to be called before the getResponseBody
stream. But, in the actual implementation in the JRE, that need not be the case. So, my Mocks broke my implementation that works in production. Hah !!!
Then I looked at testing the registration of the endpoints, and that lead me to implement a MockHttpServer
that would accept the context registrations. Again, implementing that API felt like I was not getting any testing done really. Everything was in my mock, so the production would feel very different, and possibly break.
So, I dropped the "unit test", and worked on the integration test. My tests bring up an Http Server in the test environment, hit the 2 APIs ( ping and 404, for everything else) that have been implemented, and stop the server after that.
I am fairly satisfied with this approach for now. It allows me to move on to the next step.
The test looks like this:
class PingAndRootEndpointTest {
static App app;
static final int PORT = 9090;
static String baseAddress = "http://localhost:%d".formatted(PORT);
@BeforeAll
public static void initializeApp() throws IOException {
app = new App(PORT);
app.start();
}
@AfterAll
public static void stopApp() {
app.stop(0);
app = null;
}
@Test
void testPingHandler() throws IOException, InterruptedException {
var client = HttpClient.newHttpClient();
var uri = URI.create("%s/%s".formatted(baseAddress, "ping"));
var response = client.send(
HttpRequest.newBuilder().GET().uri(uri).build(),
BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertEquals("pong", response.body());
}
@Test
void testRootHandler() throws IOException, InterruptedException {
var client = HttpClient.newHttpClient();
var uri = URI.create("%s/%s".formatted(baseAddress, "404"));
var response = client.send(
HttpRequest.newBuilder().GET().uri(uri).build(),
BodyHandlers.ofString());
assertEquals(404, response.statusCode());
assertEquals("Not Found !!!", response.body());
}
}
The code for the tests are in this Merge Request: https://gitlab.com/mandraketech/httpserver-based-todo-app/-/merge_requests/5
Yeah, I know, Annotations. These are for Junit 5. And there is no way to avoid them. Thankfully, they do not add anything to the class files. Only used for lookup using reflection, when running them.
I did not know about IntegrationTests, or IT, plugin called Maven Failsafe at the time I merged the request. Will rename the test at an appropriate time and integrate this plugin too. Need to be able to differentiate between Unit Tests and Integration Tests.
Along the way, I got some cleanup, and simplifications on the Docker images, for dev as well as prod environments. They now use the alpine versions of the JRE, and VSCode is happy working in them. So, I just trimmed down the release image to under 300MB, down from over 600 earlier. The dev environment is down from over 900 MB earlier to under 600MB too. The changes are in the (https://gitlab.com/mandraketech/java-vscode-dev) if you want to take from there.
We have "Water Though the Pipe" now, and the learnings from these are as follows:
The HttpServer "context" can be used to wire up separate processing units, but each context will need a "Router" for itself, just like all the other frameworks. For example, creating a context handler for serving static files, and another for endpoints that server JSON, vs those that server html / templates.
This will require writing a basic router that can handle the Todo endpoints. Given that the HttpServer context exposes the very fundamental constructs, it will help if we can wrap up the request, response and some request context into the handler api. This will allow cleanly handling any error conditions, and ensuring that the response stream is not polluted in the handler.
This will also allow us to write "preprocessors" or "middleware" to handle security, sanitising the request, and similar activities.
I will look for inspiration in the implementation to the Servlet API, and the Fastify framework ( Node ecosystem ).
It has been quite a journey so far. Until next time.