11.3测试响应式接口

11.3测试响应式Controller

在测试响应式Controller时,Spring 5并没有让我们陷入困境。实际上,Spring 5引入了WebTestClient,这是一个新的测试程序,它让用Spring WebFlux编写的响应式Controller变得容易测试。让我们首先使用它测试在第11.1.2节中编写的DesignTacoController中的recentTacos()方法,来了解如何使用WebTestClient编写测试用例。

11.3.1测试GET请求

对于recentTacos()方法,我们想声明的一件事是,如果为 /design/recent 路径发出了HTTP GET请求,那么响应将包含一个不超过12tacosJSON数据。程序清单11.1中的测试类是一个很好的开始。

程序清单11.1使用WebTestClient测试DesignTacoController

package tacos;

import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import tacos.Ingredient.Type;
import tacos.data.TacoRepository;
import tacos.web.api.DesignTacoController;

public class DesignTacoControllerTest {
    @Test
    public void shouldReturnRecentTacos() {
        Taco[] tacos = {
            testTaco(1L), testTaco(2L), testTaco(3L), testTaco(4L),
            testTaco(5L), testTaco(6L), testTaco(7L), testTaco(8L),
            testTaco(9L), testTaco(10L), testTaco(11L), testTaco(12L),
            testTaco(13L), testTaco(14L), testTaco(15L), testTaco(16L)
        };

        Flux<Taco> tacoFlux = Flux.just(tacos);
        TacoRepository tacoRepo = Mockito.mock(TacoRepository.class);

        when(tacoRepo.findAll()).thenReturn(tacoFlux);
        WebTestClient testClient = WebTestClient.bindToController(
            new DesignTacoController(tacoRepo)).build();

        testClient.get().uri("/design/recent")
            .exchange().expectStatus().isOk().expectBody()
            .jsonPath("$").isArray()
            .jsonPath("$").isNotEmpty()
            .jsonPath("$[0].id").isEqualTo(tacos[0].getId().toString())
            .jsonPath("$[0].name").isEqualTo("Taco 1")
            .jsonPath("$[1].id").isEqualTo(tacos[1].getId().toString())
            .jsonPath("$[1].name").isEqualTo("Taco 2")
            .jsonPath("$[11].id").isEqualTo(tacos[11].getId().toString())
            ...
            .jsonPath("$[11].name").isEqualTo("Taco 12")
            .jsonPath("$[12]").doesNotExist()
            .jsonPath("$[12]").doesNotExist();
    }

    ...
}

shouldReturnRecentTacos()方法做的第一件事是以Flux的形式设置测试数据。然后,这个Flux作为模拟TacoRepositoryfindAll()方法的返回值。

对于将由Flux发布的Taco对象,它们是用一个名为testTaco()的方法创建的,当给定一个数字时,该方法将生成一个Taco对象,其ID和名称基于该数字。testTaco()方法实现如下:

private Taco testTaco(Long number) {
    Taco taco = new Taco();
    taco.setId(UUID.randomUUID());
    taco.setName("Taco " + number);
    List<IngredientUDT> ingredients = new ArrayList<>();
    ingredients.add(
        new IngredientUDT("INGA", "Ingredient A", Type.WRAP));
    ingredients.add(
        new IngredientUDT("INGB", "Ingredient B", Type.PROTEIN));
    taco.setIngredients(ingredients);
    return taco;
}

为了简单起见,所有的测试tacos都有相同的两种成分。但它们的ID和名字将由给定的号码决定。

同时,在shouldReturnRecentTacos()方法中,实例化了一个DesignTacoController,将模拟的TacoRepository注入构造函数。Controller被赋予WebTestClient.bindToController()以创建WebTestClient的实例。

完成所有设置后,现在可以使用WebTestClient/design/recent 提交GET请求,并验证响应是否满足预期。调用get().uri(“/design/recent”)描述要发出的请求。然后调用exchange()提交请求,该请求将由绑定到DesignTacoControllerController进行处理。

最后,可以确认响应与预期一致。通过调用expectStatus(),可以断言响应具有HTTP 200(OK)状态代码。之后,将看到对jsonPath()的几个调用,这些调用断言响应体中的JSON具有它应该具有的值。最后的断言检查第12个元素(在基于零的数组中)是否不存在,因为结果不应超过12个元素。

如果JSON返回很复杂,包含大量数据或高度嵌套的数据,那么使用jsonPath()可能会很无聊。实际上,为了节省空间,清单11.1中已经省略了对jsonPath()的许多调用。对于那些使用jsonPath()可能很笨拙的情况,WebTestClient提供了json(),它接受包含jsonString参数来对响应进行响应。

例如,假设在一个名为recent-tacos.JSON的文件中创建了完整的响应JSON,并将其放在路径 /tacos 下的类路径中。然后重写WebTestClient断言,如下所示:

ClassPathResource recentsResource = new ClassPathResource("/tacos/recent-tacos.json");
String recentsJson = StreamUtils.copyToString(
    recentsResource.getInputStream(), Charset.defaultCharset());

testClient.get().uri("/design/recent")
    .accept(MediaType.APPLICATION_JSON)
    .exchange().expectStatus().isOk().expectBody()
    .json(recentsJson);

因为json()接受String,所以必须首先将类路径资源加载到String对象中。谢天谢地,Spring中的StreamUtils使copyToString()的使用变得简单。copyToString()返回的String将包含响应请求时预期的整个JSON。将它赋给json()方法可以确保Controller产生正确的输出。

WebTestClient提供的另一个选项允许将响应体与值列表进行比较。expectBodyList()方法接受指示列表中元素类型的类或参数化类型引用,并返回要针对其进行断言的istBodySpec对象。使用expectBodyList(),可以重写测试以使用用于创建模拟TacoRepository的相同测试数据的子集:

testClient.get().uri("/design/recent")
    .accept(MediaType.APPLICATION_JSON)
    .exchange()
    .expectStatus().isOk()
    .expectBodyList(Taco.class)
    .contains(Arrays.copyOf(tacos, 12));

在这里,断言响应体包含的列表,与在测试方法开始时创建的原始Taco数组的前12个元素,具有相同的元素。

11.3.2测试POST请求

WebTestClient可以做的不仅仅是针对ControllerGET请求进行测试。它还可以用于测试任何类型的HTTP方法,包括GET、POST、PUT、PATCH、DELETEHEAD请求。表11.1HTTP方法映射到WebTestClient方法。

11.1 WebTestClient针对Spring WebFlux控制器测试任何类型的请求

HTTP方法 WebTestClient方法
GET .get()
POST .post()
PUT .put()
PATCH .patch()
DELETE .delete()
HEAD .head()

作为针对Spring WebFlux Controller中的另一个HTTP方法请求示例的测试,让我们看看针对DesignTacoController的另一个测试。这次,将通过向 /design 提交POST请求来编写针对创建taco端点API的测试:

@Test
public void shouldSaveATaco() {
    TacoRepository tacoRepo = Mockito.mock(TacoRepository.class);
    Mono<Taco> unsavedTacoMono = Mono.just(testTaco(null));
    Taco savedTaco = testTaco(null);
    savedTaco.setId(1L);
    Mono<Taco> savedTacoMono = Mono.just(savedTaco);
    when(tacoRepo.save(any())).thenReturn(savedTacoMono);
    WebTestClient testClient = WebTestClient.bindToController(
        new DesignTacoController(tacoRepo)).build();

    testClient.post()
        .uri("/design")
        .contentType(MediaType.APPLICATION_JSON)
        .body(unsavedTacoMono, Taco.class)
        .exchange()
        .expectStatus().isCreated()
        .expectBody(Taco.class)
        .isEqualTo(savedTaco);
}

与前面的测试方法一样,shouldSaveATaco()首先设置一些测试数据,模拟TacoRepository,然后构建一个WebTestClient,并绑定到Controller。然后,使用WebTestClient/design 提交POST请求,请求的body类型为application/json,有效负载是未保存MonoTacojson序列化形式。在执行exchange()之后,测试断言响应具有HTTP 201(CREATED)状态,并且正文中的有效负载等于保存的Taco对象。

11.3.3使用线上服务器进行测试

到目前为止,编写的测试依赖于Spring WebFlux框架的模拟实现,因此不需要真正的服务器。但可能需要在NettyTomcat等服务器的上下文中测试WebFlux Controller,并且可能需要使用repository或其他依赖项。也就是说,可能需要编写一个集成测试。

要编写WebTestClient集成测试,首先使用@RunWith@SpringBootTest对测试类进行注解,就像其他任何Spring Boot集成测试一样:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class DesignTacoControllerWebTest {
    @Autowired
    private WebTestClient testClient;
}

通过将webEnvironment属性设置为webEnvironment.RANDOM_PORT,将要求Spring启动正在运行的服务器来监听随机选择的端口。

WebTestClient还将自动连接到测试类中。这不仅意味着将不再需要在测试方法中创建一个URL,而且在发出请求时也不需要指定完整的URL。这是因为WebTestClient将被装配成知道测试服务器在哪个端口上运行。现在可以将shouldReturnRecentTacos()重写为使用自动连线WebTestClient的集成测试:

@Test
public void shouldReturnRecentTacos() throws IOException {
    testClient.get().uri("/design/recent")
        .accept(MediaType.APPLICATION_JSON).exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$[?(@.id == 'TACO1')].name").isEqualTo("Carnivore")
        .jsonPath("$[?(@.id == 'TACO2')].name").isEqualTo("Bovine Bounty")
        .jsonPath("$[?(@.id == 'TACO3')].name").isEqualTo("Veg-Out");
}

毫无疑问,你已经注意到这个新版本的shouldReturnRecentTacos()的代码要少得多。因为将使用自动注入的实例,因此不再需要创建WebTestClient。而且没有必要模拟TacoRepository,因为Spring将创建DesignTacoController的一个实例,并为它注入一个真正的TacoRepository。在这个新版本的测试方法中,使用JSONPath表达式来验证从数据库提供的值。

在测试过程中,当需要使用WebFlux Controller公开的API时,WebTestClient非常有用。但是,当应用程序本身使用其他API时呢?让我们把注意力转向Spring的响应式web的客户端,看看WebClient是如何提供REST客户端来处理诸如MonoFlux之类的响应式类型的。

上一页
下一页