In this chapter, we complete our web application by adding a web endpoint. We will learn how to write HTTP integration tests using the built-in Java HTTP client. We will test-drive the web adapter code that runs this endpoint, using an open source HTTP server framework. This web adapter is responsible for converting HTTP requests into commands we can execute in our domain layer. At the end of the chapter, we will assemble all the pieces of our application into a microservice. The web adapter and database adapters will be linked to the domain model using dependency injection. We will need to run a few manual database commands, install a web client called Postman, and then we can play our game.
In this chapter, we’re going to cover the following main topics:
The code for this chapter is available at https://github.com/PacktPublishing/Test-Driven-Development-with-Java/tree/main/chapter15.
Before attempting to run the final application, perform the following steps:
insert into word values (1, 'ARISE'), (2, 'SHINE'), (3, 'LIGHT'), (4, 'SLEEP'), (5, 'BEARS'), (6, 'GREET'), (7, 'GRATE');
In this section, we will test-drive a web adapter that will provide our domain model with an HTTP API. External web clients will be able to send HTTP requests to this endpoint to trigger actions in our domain model so that we can play the game. The API will return appropriate HTTP responses, indicating the score for the submitted guess and reporting when the game is over.
The following open source libraries will be used to help us write the code:
To start building, we first add the required libraries as dependencies to the build.gradle file. Then we can begin writing an integration test for our HTTP endpoint and test-drive the implementation.
We need to add the three libraries Molecule, Undertow, and Gson to the build.gradle file before we can use them:
Add the following code to the build.gradle file:
dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' testImplementation 'org.assertj:assertj-core:3.22.0' testImplementation 'org.mockito:mockito-core:4.8.0' testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' testImplementation 'com.github.database-rider:rider-core:1.35.0' testImplementation 'com.github.database-rider:rider-junit5:1.35.0' implementation 'org.postgresql:postgresql:42.5.0' implementation 'org.jdbi:jdbi3-core:3.34.0' implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'com.vtence.molecule:molecule:0.15.0' implementation 'io.thorntail:undertow:2.7.0.Final' implementation 'com.google.code.gson:gson:2.10' }
We will follow the normal TDD cycle to create our web adapter. When writing tests for objects in the adapter layer, we must focus on testing the translation between objects in our domain layer and communications with external systems. Our adapter layer will use the Molecule HTTP framework to handle HTTP requests and responses.
As we have used hexagonal architecture and started with the domain layer, we already know that the game logic is working. The goal of this test is to prove that the web adapter layer is performing its responsibility. That is to translate HTTP requests and responses to objects in our domain layer.
As ever, we begin by creating a test class:
package com.wordz.adapters.api;
public class WordzEndpointTest {
}
The reason for including this package is as part of our hexagonal architecture. Code in this web adapter is allowed to use anything from the domain model. The domain model itself is unaware of the existence of this web adapter.
Our first test will be to start a new game:
@Test void startGame() { }
@Test
void startGame() {
HttpResponse res;
assertThat(res)
.hasStatusCode(HttpStatus.NO_CONTENT.code);
}
@Test
void startGame() throws IOException,
InterruptedException {
var httpClient = HttpClient.newHttpClient();
HttpResponse res
= httpClient.send(req,
HttpResponse.BodyHandlers.discarding());
assertThat(res)
.hasStatusCode(HttpStatus.NO_CONTENT.code);
}
@Test
private static final Player PLAYER
= new Player("alan2112");
void startGame() throws IOException,
InterruptedException {
var req = HttpRequest.newBuilder()
.uri(URI.create("htp://localhost:8080/start"))
.POST(HttpRequest.BodyPublishers
.ofString(new Gson().toJson(PLAYER)))
.build();
var httpClient = HttpClient.newHttpClient();
HttpResponse res
= httpClient.send(req,
HttpResponse.BodyHandlers.discarding());
assertThat(res)
.hasStatusCode(HttpStatus.NO_CONTENT.code);
}
We see the Gson library being used to convert a Player object into its JSON representation. We also see a POST method is constructed and sent to the /start path on localhost. Eventually, we will want to move the localhost detail into configuration. But, for now, it will get the test working on our local machine.
Figure 15.1 – A failed test – no HTTP server
Unsurprisingly, this test fails because it cannot connect to an HTTP server. Fixing that is our next task.
The failing test allows us to test-drive code that implements an HTTP server. We will use the Molecule library to provide HTTP services to us:
@Test
void startGame() throws IOException,
InterruptedException {
var endpoint
= new WordzEndpoint("localhost", 8080);
The two parameters passed into the WordzEndpoint constructor define the host and port that the web endpoint will run on.
package com.wordz.adapters.api;
public class WordzEndpoint {
public WordzEndpoint(String host, int port) {
}
}
In this case, we’re not going to store the host and port details in fields. Instead, we are going to start a WebServer using a class from the Molecule library.
package com.wordz.adapters.api;
import com.vtence.molecule.WebServer;
public class WordzEndpoint {
private final WebServer server;
public WordzEndpoint(String host, int port) {
server = WebServer.create(host, port);
}
}
The preceding code is enough to start an HTTP server running and allow the test to connect to it. Our HTTP server does nothing useful in terms of playing our game. We need to add some routes to this server along with the code to respond to them.
To be useful, the HTTP endpoint must respond to HTTP commands, interpret them, and send them as commands to our domain layer. As design decisions, we decide on the following:
To add routes to the HTTP server, do the following:
public class WordzEndpoint {
private final WebServer server;
public WordzEndpoint(String host, int port) {
server = WebServer.create(host, port);
try {
server.route(new Routes() {{
post("/start")
.to(request -> startGame(request));
}});
} catch (IOException ioe) {
throw new IllegaStateException(ioe);
}
}
private Response startGame(Request request) {
return Response
.of(HttpStatus.NOT_IMPLEMENTED)
.done();
}
}
Figure 15.2 – An incorrect HTTP status
The test fails, as expected. We have made progress because the test now fails for a different reason. We can now connect to the web endpoint, but it does not return the right HTTP response. Our next task is to connect this web endpoint to the domain layer code and take the relevant actions to start a game.
Our next task is to receive an HTTP request and translate that into domain layer calls. This involves parsing JSON request data, using the Google Gson library, into Java objects, then sending that response data to the class Wordz port:
@ExtendWith(MockitoExtension.class)
public class WordzEndpointTest {
@Mock
private Wordz mockWordz;
@Test
void startGame() throws IOException,
InterruptedException {
var endpoint
= new WordzEndpoint(mockWordz,
"localhost", 8080);
public class WordzEndpoint {
private final WebServer server;
private final Wordz wordz;
public WordzEndpoint(Wordz wordz,
String host, int port) {
this.wordz = wordz;
private Response startGame(Request request) {
try {
Player player
= new Gson().fromJson(request.body(),
Player.class);
boolean isSuccessful = wordz.newGame(player);
if (isSuccessful) {
return Response
.of(HttpStatus.NO_CONTENT)
.done();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
throw new
UnsupportedOperationException("Not
implemented");
}
Figure 15.3 – An incorrect HTTP response
It failed because the return value from wordz.newGame() was false. The mock object needs to be set up to return true.
@Test
void startsGame() throws IOException,
InterruptedException {
var endpoint
= new WordzEndpoint(mockWordz,
"localhost", 8080);
when(mockWordz.newGame(eq(PLAYER)))
.thenReturn(true);
Figure 15.4 – The test passes
The integration test passes. The HTTP request has been received, called the domain layer code to start a new game, and the HTTP response is returned. The next step is to consider refactoring.
As usual, once a test passes, we consider what – if anything – we need to refactor.
It will be worthwhile to refactor the test to simplify writing new tests by collating common code into one place:
@ExtendWith(MockitoExtension.class) public class WordzEndpointTest { @Mock private Wordz mockWordz; private WordzEndpoint endpoint; private static final Player PLAYER = new Player("alan2112"); private final HttpClient httpClient = HttpClient.newHttpClient(); @BeforeEach void setUp() { endpoint = new WordzEndpoint(mockWordz, "localhost", 8080); } @Test void startsGame() throws IOException, InterruptedException { when(mockWordz.newGame(eq(player))) .thenReturn(true); var req = requestBuilder("start") .POST(asJsonBody(PLAYER)) .build(); var res = httpClient.send(req, HttpResponse.BodyHandlers.discarding()); assertThat(res) .hasStatusCode(HttpStatus.NO_CONTENT.code); } private HttpRequest.Builder requestBuilder( String path) { return HttpRequest.newBuilder() .uri(URI.create("http://localhost:8080/" + path)); } private HttpRequest.BodyPublisher asJsonBody( Object source) { return HttpRequest.BodyPublishers .ofString(new Gson().toJson(source)); } }
One of our design decisions is that a player cannot start a game when one is in progress. We need to test-drive this behavior. We choose to return an HTTP status of 409 Conflict to indicate that a game is already in progress for a player and a new one cannot be started for them:
@Test
void rejectsRestart() throws Exception {
when(mockWordz.newGame(eq(player)))
.thenReturn(false);
var req = requestBuilder("start")
.POST(asJsonBody(player))
.build();
var res
= httpClient.send(req,
HttpResponse.BodyHandlers.discarding());
assertThat(res)
.hasStatusCode(HttpStatus.CONFLICT.code);
}
Figure 15.5 – A failing test
private Response startGame(Request request) {
try {
Player player
= new Gson().fromJson(request.body(),
Player.class);
boolean isSuccessful = wordz.newGame(player);
if (isSuccessful) {
return Response
.of(HttpStatus.NO_CONTENT)
.done();
}
return Response
.of(HttpStatus.CONFLICT)
.done();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Figure 15. 6 – The test passes
The test passes when run on its own now that the implementation is in place. Let’s run all the WordzEndpointTests tests to double-check our progress.
Figure 15.7 – Test failure due to restarting the server
Unexpectedly, the tests fail when run one after the other.
When we run all of the tests, they now fail. The tests all previously ran correctly when run one at a time. A recent change has clearly broken something. We lost our test isolation at some point. This error message indicates the web server is being started twice on the same port, which is not possible.
The options are to stop the web server after each test or to only start the web server once for all tests. As this is intended to be a long-running microservice, only starting once seems the better choice here:
@BeforeAll
void setUp() {
mockWordz = mock(Wordz.class);
endpoint = new WordzEndpoint(mockWordz,
"localhost", 8080);
}
We change the @BeforeEach annotation to a @BeforeAll annotation to make the endpoint creation only happen once per test. To support this, we also must create the mock and use an annotation on the test itself to control the life cycle of objects:
@ExtendWith(MockitoExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class WordzEndpointTest {
Both tests in WordzEndpointTest now pass.
private Response startGame(Request request) {
try {
Player player = extractPlayer(request);
boolean isSuccessful = wordz.newGame(player);
HttpStatus status
= isSuccessful?
HttpStatus.NO_CONTENT :
HttpStatus.CONFLICT;
return Response
.of(status)
.done();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Player extractPlayer(Request request)
throws IOException {
return new Gson().fromJson(request.body(),
Player.class);
}
We have now completed the major part of the coding needed to start a game. To handle the remaining error condition, we can now test-drive the code to return 400 BAD REQUEST if the Player object cannot be read from the JSON payload. We will omit that code here. In the next section, we will move on to test-driving the code for guessing the target word.
In this section, we will test-drive the code to play the game. This involves submitting multiple guess attempts to the endpoint until a game-over response is received.
We start by creating an integration test for the new /guess route in our endpoint:
@Test
void partiallyCorrectGuess() {
var score = new Score("-U---");
score.assess("GUESS");
var result = new GuessResult(score, false, false);
when(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(result);
}
@Test
void partiallyCorrectGuess() {
var score = new Score("-U---");
score.assess("GUESS");
var result = new GuessResult(score, false, false);
when(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(result);
var guessRequest = new GuessRequest(player, "-U---");
var body = new Gson().toJson(guessRequest);
var req = requestBuilder("guess")
.POST(ofString(body))
.build();
}
package com.wordz.adapters.api;
import com.wordz.domain.Player;
public record GuessRequest(Player player, String guess) {
}
@Test
void partiallyCorrectGuess() throws Exception {
var score = new Score("-U---");
score.assess("GUESS");
var result = new GuessResult(score, false, false);
when(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(result);
var guessRequest = new GuessRequest(player, "-U---");
var body = new Gson().toJson(guessRequest);
var req = requestBuilder("guess")
.POST(ofString(body))
.build();
var res
= httpClient.send(req,
HttpResponse.BodyHandlers.ofString());
}
@Test
void partiallyCorrectGuess() throws Exception {
var score = new Score("-U--G");
score.assess("GUESS");
var result = new GuessResult(score, false, false);
when(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(result);
var guessRequest = new GuessRequest(player,
"-U--G");
var body = new Gson().toJson(guessRequest);
var req = requestBuilder("guess")
.POST(ofString(body))
.build();
var res
= httpClient.send(req,
HttpResponse.BodyHandlers.ofString());
var response
= new Gson().fromJson(res.body(),
GuessHttpResponse.class);
// Key to letters in scores():
// C correct, P part correct, X incorrect
Assertions.assertThat(response.scores())
.isEqualTo("PCXXX");
Assertions.assertThat(response.isGameOver())
.isFalse();
}
One API design decision here is to return the per-letter scores as a five-character String object. The single letters X, C, and P are used to indicate incorrect, correct, and partially correct letters. We capture this decision in the assertion.
package com.wordz.adapters.api;
public record GuessHttpResponse(String scores,
boolean isGameOver) {
}
public WordzEndpoint(Wordz wordz, String host,
int port) {
this.wordz = wordz;
server = WebServer.create(host, port);
try {
server.route(new Routes() {{
post("/start")
.to(request -> startGame(request));
post("/guess")
.to(request -> guessWord(request));
}});
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
We add an IllegalStateException to rethrow any problems that occur when starting the HTTP server. For this application, this exception may propagate upwards and cause the application to stop running. Without a working web server, none of the web code makes sense to run.
private Response guessWord(Request request) {
try {
GuessRequest gr =
extractGuessRequest(request);
return null ;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private GuessRequest extractGuessRequest(Request request) throws IOException {
return new Gson().fromJson(request.body(),
GuessRequest.class);
}
private Response guessWord(Request request) {
try {
GuessRequest gr =
extractGuessRequest(request);
GuessResult result
= wordz.assess(gr.player(),
gr.guess());
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Response guessWord(Request request) {
try {
GuessRequest gr =
extractGuessRequest(request);
GuessResult result = wordz.assess(gr.player(),
gr.guess());
return Response.ok()
.body(createGuessHttpResponse(result))
.done();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String createGuessHttpResponse(GuessResult result) {
GuessHttpResponse httpResponse
= new
GuessHttpResponseMapper().from(result);
return new Gson().toJson(httpResponse);
}
package com.wordz.adapters.api;
import com.wordz.domain.GuessResult;
public class GuessHttpResponseMapper {
public GuessHttpResponse from(GuessResult result) {
return null;
}
}
Figure 15.8 – The test fails
Note
The details of this are omitted but can be found on GitHub – it follows the standard approach used throughout the book.
Figure 15.9 – The endpoint test passes
As we see in the preceding image, the integration test has passed! Time for a well-earned coffee break. Well, mine’s a nice English breakfast tea, but that’s just me. After that, we can test-drive the response to any errors that occurred. Then it’s time to bring the microservice together. In the next section, we will assemble our application into a running microservice.
In this section, we will bring together the components of our test-driven application. We will form a microservice that runs our endpoint and provides the frontend web interface to our service. It will use the Postgres database for storage.
We need to write a short main() method to link together the major components of our code. This will involve creating concrete objects and injecting dependencies into constructors. The main() method exists on class WordzApplication, which is the entry point to our fully integrated web service:
package com.wordz; import com.wordz.adapters.api.WordzEndpoint; import com.wordz.adapters.db.GameRepositoryPostgres; import com.wordz.adapters.db.WordRepositoryPostgres; import com.wordz.domain.Wordz; public class WordzApplication { public static void main(String[] args) { var config = new WordzConfiguration(args); new WordzApplication().run(config); } private void run(WordzConfiguration config) { var gameRepository = new GameRepositoryPostgres(config.getDataSource()); var wordRepository = new WordRepositoryPostgres(config.getDataSource()); var randomNumbers = new ProductionRandomNumbers(); var wordz = new Wordz(gameRepository, wordRepository, randomNumbers); var api = new WordzEndpoint(wordz, config.getEndpointHost(), config.getEndpointPort()); waitUntilTerminated(); } private void waitUntilTerminated() { try { while (true) { Thread.sleep(10000); } } catch (InterruptedException e) { return; } } }
The main() method instantiates the domain model, and dependency injects the concrete version of our adapter classes into it. One notable detail is the waitUntilTerminated()method. This prevents main() from terminating until the application is closed down. This, in turn, keeps the HTTP endpoint responding to requests.
Configuration data for the application is held in class WordzConfiguration. This has default settings for the endpoint host and port settings, along with database connection settings. These can also be passed in as command line arguments. The class and its associated test can be seen in the GitHub code for this chapter.
In the next section, we will use the Wordz web service application using the popular HTTP testing tool, Postman.
To use our newly assembled web application, first ensure that the database setup steps and the Postman installation described in the Technical requirements section have been successfully completed. Then run the main() method of class WordzApplication in IntelliJ. That starts the endpoint, ready to accept requests.
Once the service is running, the way we interact with it is by sending HTTP requests to the endpoint. Launch Postman and (on macOS) a window that looks like this will appear:
Figure 15.10 – Postman home screen
We first need to start a game. To do that, we need to send HTTP POST requests to the /start route on our endpoint. By default, this will be available at http://localhost:8080/start. We need to send a body, containing the JSON {"name":"testuser"} text.
We can send this request from Postman. We click the Create a request button on the home page. This takes us to a view where we can enter the URL, select the POST method and type our JSON body data:
Figure 15.11 – Start a new game
Click the blue Send button. The screenshot in Figure 15.11 shows both the request that was sent – in the upper portion of the screen – and the response. In this case, the game was successfully started for the player named testuser. The endpoint performed as expected and sent an HTTP status code of 204 No Content. This can be seen in the response panel, towards the bottom of the screenshot.
A quick check of the contents of the game table in the database shows that a row for this game has been created:
wordzdb=# select * from game; player_name | word | attempt_number | is_game_over -------------+-------+----------------+-------------- testuser | ARISE | 0 | f (1 row) wordzdb=#
Figure 15.12 – Score returned
The endpoint returns an HTTP status code of 200 OK. This time, a body of JSON formatted data is returned. We see "scores":"PXPPC" indicating that the first letter of our guess, S, appears in the word somewhere but not in the first position. The second letter of our guess, T, is incorrect and does not appear in the target word. We got two more part-correct letters and one final correct letter in our guess, which was the letter E at the end.
The response also shows "isGameOver":false. We haven’t finished the game yet.
Figure 15.13 – A successful guess
Winner! We see "scores":"CCCCC" telling us all five letters of our guess are correct. "isGameOver":true tells us that our game has ended, on this occasion, successfully.
We’ve successfully played one game of Wordz using our microservice.
In this section, we have completed our Wordz application. We used an integration test with TDD to drive out an HTTP endpoint for Wordz. We used open source HTTP libraries – Molecule, Gson, and Undertow. We made effective use of hexagonal architecture. Using ports and adapters, these frameworks became an implementation detail rather than a defining feature of our design.
We assembled our final application to bring together the business logic held in the domain layer with the Postgres database adapter and the HTTP endpoint adapter. Working together, our application forms a small microservice.
In this final chapter, we have arrived at a small-scale yet typical microservice comprising an HTTP API and a SQL database. We’ve developed the code test first, using tests to guide our design choices. We have applied the SOLID principles to improve how our software fits together. We have learned how the ports and adapters of hexagonal architecture simplify the design of code that works with external systems. Using hexagonal architecture is a natural fit for TDD, allowing us to develop our core application logic with FIRST unit tests. We have created both a database adapter and an HTTP adapter test first, using integration tests. We applied the rhythms of TDD – Red, Green, Refactor and Arrange, Act and Assert to our work. We have applied test doubles using the Mockito library to stand in for external systems, simplifying the development.
In this book, we have covered a wide range of TDD and software design techniques. We can now create code with fewer defects, and that is safer and easier to work with.
Further work could include adding a Continuous Integration (CI) pipeline so that whenever we commit code, the application gets pulled from source control, built, and all tests run. We could consider deployment and automation of that. One example might be to package up the Wordz application and the Postgres database as a Docker image. It would be good to add database schema automation, using a tool such as Flyway.
Yes. As the web endpoint sits in our adapter layer of the hexagonal architecture, it does not affect the core functionality in the domain model. Any suitable web framework could be used.
An overview of what a REST web interface means, along with some common variations
The author’s book gives some more details on OO basics with some useful design patterns
A popular testing tool that sends HTTP requests and displays responses
A lightweight HTTP framework for Java
An HTTP server for Java that works well with the Molecule framework
Google’s library to convert between Java objects and the JSON format
Amazon’s guide to REST APIs
Official Java documentation about the test HHTP client used in this chapter
3.145.20.193