玩游戏

在本节中,我们将测试驱动用于玩游戏的代码。这涉及向端点提交多次猜测尝试,直到收到游戏结束的响应。

我们首先为端点中的新 /guess 路由创建一个集成测试:

  1. 第一步是编写 Arrange 步骤。我们的领域模型提供了 Wordz 类中的 assess() 方法,用于评估猜测的分数,并报告游戏是否结束。为了进行测试驱动开发,我们设置了 mockWordz 存根,以便在调用 assess() 方法时返回一个有效的 GuessResult 对象。

    @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. Act 步骤将通过 web 请求提交猜测来调用我们的端点。我们的设计决策是向 /guess 路由发送一个 HTTP POST 请求,请求体将包含猜测的单词的 JSON 表示。为了创建这个请求,我们将使用 GuessRequest 记录,并使用 Gson 将其转换为 JSON。

    @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. 接下来,我们定义记录:

    package com.wordz.adapters.api;
    
    import com.wordz.domain.Player;
    
    public record GuessRequest(Player player, String guess) {
    }
  4. 然后,我们通过 HTTP 将请求发送到我们的端点,等待响应:

    @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. 然后,我们提取返回的主体数据并将其与我们的预期进行断言:

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

    这里的一个 API 设计决策是将每个字母的得分作为一个五字符的 String 对象返回。单个字母 XCP 用于表示不正确、正确和部分正确的字母。我们在断言中捕获了这一决策。

  6. 我们定义一个记录来表示我们将从端点返回的 JSON 数据结构:

    package com.wordz.adapters.api;
    
    public record GuessHttpResponse(String scores,
                                   boolean isGameOver) {
    }
  7. 由于我们决定向新的 /guess 路由发送 POST 请求,因此我们需要将此路由添加到路由表中。我们还需要将其绑定到一个将执行操作的方法,即我们将其命名为 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);
       }
    }

    我们添加 IllegalStateException 以重新抛出启动 HTTP 服务器时发生的任何问题。对于此应用程序,此异常可能会向上传播并导致应用程序停止运行。如果没有正常运行的 Web 服务器,任何 Web 代码都没有意义。

  8. 实现`guessWord()`方法 我们实现`guessWord()`方法,从POST请求体中提取请求数据:

    ```java
    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);
    }
    ```
  9. 现在我们有了请求数据,是时候调用我们的领域层来执行实际操作了。我们将捕获返回的 GuessResult 对象,以便我们可以基于它来构建来自端点的 HTTP 响应。

    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);
    }
  10. 添加`GuessHttpResponseMapper`类 我们添加一个空的`GuessHttpResponseMapper`类来完成转换:

    ```java
    package com.wordz.adapters.api;
    import com.wordz.domain.GuessResult;
    public class GuessHttpResponseMapper {
        public GuessHttpResponse from(GuessResult result) {
            return null;
        }
    }
    ```
  11. 我们添加了一个空的对象来进行转换,即 GuessHttpResponseMapper 类。在这第一步中,它将简单地返回 null

    package com.wordz.adapters.api;
    import com.wordz.domain.GuessResult;
    
    public class GuessHttpResponseMapper {
        public GuessHttpResponse from(GuessResult result) {
            return null;
        }
    }
  12. 这足以编译并能够运行 WordzEndpointTest 测试。

    image 2025 01 12 22 04 43 191
    Figure 1. Figure 15.8 – The test fails
  13. 有了失败的测试之后,我们现在可以测试驱动转换类的细节。为此,我们切换到添加一个新的单元测试,名为 GuessHttpResponseMapperTest

    这些细节省略了,但可以在 GitHub 上找到——它遵循本书中使用的标准方法。

  14. 一旦我们通过测试驱动了 GuessHttpResponseMapper 类的详细实现,就可以重新运行集成测试。

    image 2025 01 12 22 06 23 153
    Figure 2. Figure 15.9 – The endpoint test passes

正如我们在前面的图片中看到的,集成测试已经通过了!是时候享受一下美好的咖啡休息时间了。嗯,我喜欢喝一杯地道的英式早餐茶,不过那只是我个人的偏好。休息过后,我们可以开始测试驱动错误发生时的响应。接着就是将微服务集成在一起。下一节,我们将把我们的应用程序组合成一个运行中的微服务。