15

Driving the Web Layer

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:

  • Starting a new game
  • Playing the game
  • Integrating the application
  • Using the application

Technical requirements

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:

  1. Ensure the Postgres database is running locally.
  2. Ensure the database setup steps from Chapter 14 , Driving the Database Layer, have been completed.
  3. Open the Postgres pqsl command terminal and enter the following SQL command:
    insert into word values (1, 'ARISE'), (2, 'SHINE'), (3, 'LIGHT'), (4, 'SLEEP'), (5, 'BEARS'), (6, 'GREET'), (7, 'GRATE');
  4. Install Postman by following the instructions at https://www.postman.com/downloads/.

Starting a new game

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:

  • Molecule: This is a lightweight HTTP framework
  • Undertow: This is a lightweight HTTP web server that powers the Molecule framework
  • GSON: This is a Google library that converts between Java objects and JSON structured data

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.

Adding required libraries to the project

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'
}

Writing the failing test

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:

  1. First, we write our test class. We’ll call it WordzEndpointTest, and it belongs in the com.wordz.adapters.api package:
    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() {
}
  1. This test needs to capture the design decision that surrounds our intended web API. One decision is that when a game has successfully started, we will return a simple 204 No Content HTTP status code. We will start with the assert to capture this decision:
    @Test
    void startGame() {
        HttpResponse res;
        assertThat(res)
           .hasStatusCode(HttpStatus.NO_CONTENT.code);
    }
  2. Next, we write the Act step. The action here is for an external HTTP client to send a request to our web endpoint. To achieve this, we use the built-in HTTP client provided by Java itself. We arrange the code to send the request, and then discard any HTTP response body, as our design does not return a body:
    @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);
    }
  3. The Arrange step is where we capture our decisions about the HTTP request to send. In order to start a new game, we need a Player object to identify the player. We will send this as a Json object in the Request body. The request will cause a state change on our server, so we choose the HTTP POST method to represent that. Finally, we choose a route whose path is /start:
    @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.

  1. We can run our integration test and confirm that it fails:
Figure 15.1 – A failed test – no HTTP server

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.

Creating our HTTP server

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:

  1. Add an endpoint class, which we will call class WordzEndpoint:
        @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.

  1. Using the IDE, we generate the class:
    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.

  1. Create a WebServer using 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.

Adding routes to the HTTP server

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:

  • That a /start route must be called to start the game
  • That we will use the HTTP POST method
  • That we will identify which player the game belongs to as JSON data in the POST body

To add routes to the HTTP server, do the following:

  1. Test-drive the /start route. To work in small steps, initially, we will return a NOT_IMPLEMENTED HTTP response code:
    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();
      }
    }
  2. We can run the WordzEndpointTest integration test:
Figure 15.2 – An incorrect HTTP status

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.

Connecting to the domain layer

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:

  1. Add the code to call the domain layer port implemented as class Wordz. We will use Mockito to create a test double for this object. This allows us to test only the web endpoint code, decoupled from all other code:
    @ExtendWith(MockitoExtension.class)
    public class WordzEndpointTest {
        @Mock
        private Wordz mockWordz;
        @Test
        void startGame() throws IOException,
                                InterruptedException {
            var endpoint
            = new WordzEndpoint(mockWordz,
                                "localhost", 8080);
  2. We need to provide our class Wordz domain object to the class WordzEndpoint object. We use dependency injection to inject it into the constructor:
    public class WordzEndpoint {
        private final WebServer server;
        private final Wordz wordz;
        public WordzEndpoint(Wordz wordz,
                             String host, int port) {
            this.wordz = wordz;
  3. Next, we need to add the code to start a game. To do that, we first extract the Player object from the JSON data in the request body. That identifies which player to start a game for. Then we call the wordz.newGame() method. If it is successful, we return an HTTP status code of 204 No Content, indicating success:
    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");
    }
  4. Now, we can run the test, however, it fails:
Figure 15.3 – An incorrect HTTP response

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.

  1. Return the correct value from the mockWordz stub:
       @Test
    void startsGame() throws IOException,
                             InterruptedException {
        var endpoint
             = new WordzEndpoint(mockWordz,
                                 "localhost", 8080);
        when(mockWordz.newGame(eq(PLAYER)))
              .thenReturn(true);
  2. Then, run the test:
Figure 15.4 – The test passes

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.

Refactoring the start game code

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));
    }
}

Handling errors when starting a game

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:

  1. Write the test to return a 409 Conflict if the game is already in progress:
        @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);
        }
  2. Next, run the test. It should fail, as we have yet to write the implementation code:
Figure 15.5 – A failing test

Figure 15.5 – A failing test

  1. Test-drive the code to report that the game cannot be restarted:
    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);
        }
    }
  2. Run the test again:
Figure 15. 6 – The test passes

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.

  1. Run all WordzEndpointTests:
Figure 15.7 – Test failure due to restarting the server

Figure 15.7 – Test failure due to restarting the server

Unexpectedly, the tests fail when run one after the other.

Fixing the unexpectedly failing tests

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:

  1. Add a @BeforeAll annotation to only start the HTTP server once:
    @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.

  1. With all tests passing again, we can consider refactoring the code. A readability improvement will come from extracting an extractPlayer() method. We can also make the conditional HTTP status code more concise:
    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.

Playing the game

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:

  1. The first step is to code the Arrange step. Our domain model provides the assess() method on class Wordz to assess the score for a guess, along with reporting whether the game is over. To test-drive this, we set up the mockWordz stub to return a valid GuessResult object when the assess() method is called:
    @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);
    }
  2. The Act step will call our endpoint with a web request submitting the guess. Our design decision is to send an HTTP POST request to the /guess route. The request body will contain a JSON representation of the guessed word. To create this, we will use record GuessRequest and use Gson to convert that into JSON for us:
    @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();
    }
  3. Next, we define the record:
    package com.wordz.adapters.api;
    import com.wordz.domain.Player;
    public record GuessRequest(Player player, String guess) {
    }
  4. Then, we send the request over HTTP to our endpoint, awaiting the response:
    @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());
    }
  5. Then, we extract the returned body data and assert it against our expectations:
    @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.

  1. We define a record to represent the JSON data structure we will return as a response from our endpoint:
    package com.wordz.adapters.api;
    public record GuessHttpResponse(String scores,
                                    boolean isGameOver) {
    }
  2. As we have decided to POST to a new /guess route, we need to add this route to the routing table. We also need to bind it to a method that will take action, which we will call guessWord():
    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.

  1. We implement the guessWord() method with code to extract the request data from the POST body:
    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);
    }
  2. Now we have the request data, it’s time to call our domain layer to do the real work. We will capture the GuessResult object returned, so we can base our HTTP response from the endpoint on it:
    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);
        }
    }
  3. We choose to return a different format of data from our endpoint compared to the GuessResult object returned from our domain model. We will need to transform the result from the domain model:
    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);
    }
  4. We add an empty version of the object doing the transformation, which is class GuessHttpResponseMapper. In this first step, it will simply return null:
    package com.wordz.adapters.api;
    import com.wordz.domain.GuessResult;
    public class GuessHttpResponseMapper {
        public GuessHttpResponse from(GuessResult result) {
            return null;
        }
    }
  5. This is enough to compile and be able to run the WordzEndpointTest test:
Figure 15.8 – The test fails

Figure 15.8 – The test fails

  1. With a failing test in place, we can now test-drive the details of the transform class. To do this, we switch to adding a new unit test called class GuessHttpResponseMapperTest.

Note

The details of this are omitted but can be found on GitHub – it follows the standard approach used throughout the book.

  1. Once we have test-driven the detailed implementation of class GuessHttpResponseMapper, we can rerun the integration test:
Figure 15.9 – The endpoint test passes

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.

Integrating the application

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.

Using the application

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

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:

  1. Create a POST request to start the game:
Figure 15.11 – Start a new game

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=#
  1. We can now make our first guess at the word. Let’s try a guess of "STARE". The POST request for this and the response from our endpoint appears, as shown in the following screenshot:
Figure 15.12 – Score returned

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.

  1. We will make one more guess, cheating slightly. Let’s send a POST request with a guess of "ARISE":
Figure 15.13 – A successful guess

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.

Summary

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.

Questions and answers

  1. What further work could be done?

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.

  1. Could we replace the Molecule library and use something else for our web endpoint?

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.

Further reading

An overview of what a REST web interface means, along with some common variations

  • Java OOP Done Right, Alan Mellor, ISBN 9781527284449

The author’s book gives some more details on OO basics with some useful design patterns

  • https://www.postman.com/

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.20.193