vlambda博客
学习文章列表

读书笔记《building-a-restful-web-service-with-spring》测试REST风格的Web服务

Chapter 8. Testing RESTful Web Services

测试是构建商业软件解决方案的固有方面。事实上,在开发过程的每一步都应该考虑测试,只有当完整的测试套件成功时,才应该认为软件包已经准备就绪。

在本章中,我们将对示例 RESTful Web 服务应用以下测试策略:

  • 单元测试 Spring 控制器

  • 嘲笑

  • 测试安全性

  • 整合策略

  • 要考虑的其他测试形式

Unit testing Spring controllers


由于我们将 RESTful 端点声明为带有注释的Java 方法,因此可以使用正常的单元测试技术。 Java 的实际单元测试 库是 JUnit。 (http://www.junit.org)。 JUnit 是一个用于编写可重复测试的简单框架。以下代码片段说明了如何测试 RESTful 端点:

public class AvailabilityResourceTest {
  @Test
  public void testGetAvailability() throws Exception {
    AvailabilityService service = ...
    AvailabilityResource resource = new AvailabilityResource(service);
    WebRequest request = ...
    // invalid from date
    ApiResponse response = resource.getAvailability(null, "2017-01-02", "1", request);
    assertEquals(Status.ERROR, response.getStatus());
    assertEquals(17, response.getError().getErrorCode());
    // from is after until
    response = resource.getAvailability("2017-01-03", "2017-01-02", "1", request);
    assertEquals(Status.ERROR, response.getStatus());
    assertEquals(17, response.getError().getErrorCode());
  }
}

大多数读者都会熟悉这些类型的测试。在这里,我们验证了示例物业管理系统的可用性组件 不接受无效日期。此代码片段中未涵盖的是 com.packtpub.springrest.availability.AvailabilityServiceorg.springframework.web 的实例。获取到 context.request.WebRequest。这两个接口是依赖关系,我们并不想使用真正的实现。我们只对测试资源类中的代码感兴趣。下一节将讨论处理这种情况的模拟技术。

Mocking


我们通过使用 Java 接口将 RESTful 层与服务实现分离,构建了我们的示例 属性管理系统。除了帮助构建代码库和防止紧密耦合之外,此过程还有益于单元测试。实际上,在测试应用程序的 Web 层时,我们可以使用模拟的实现,而不是使用具体的服务实现。例如,考虑以下单元测试:

public class RoomsResourceTest {
  @Test
  public void testGetRoom() throws Exception {
    RoomsResource resource = new RoomsResource();
    InventoryService inventoryService = ...
    ApiResponse response = resource.getRoom(1);
    assertEquals(Status.OK, response.getStatus());
    assertNotNull(response);
    RoomDTO room = (RoomDTO) response.getData();
    assertNotNull(room);
    assertEquals("Room1", room.getName());
  }
}

当为现有房间调用 RoomResource.getRoom() 时,我们期望包含表示该房间的有效负载的非空成功响应。我们可以创建一个模拟实现,而不是使用依赖于数据库的真正的 com.packtpub.springrest.inventory.InventoryService 实现。这个模拟必须表现出预定义的行为,允许我们测试由 com.packtpub.springrest.inventory.web.RoomsResource 处理的不同用例。这可以通过两种不同的方式实现,如 将在下一节中说明。

Simple mocking

为了提供 必要的行为,我们可以简单地创建 Inventory Service 的匿名实现:

InventoryService inventoryService = new InventoryService() {
  @Override
  public Room getRoom(long roomId) {
    if (roomId == 1) {
      Room room = new Room();
      room.setName("Room1");
      room.setDescription("Room description");
      RoomCategory category = new RoomCategory();
      category.setName("Category1");
      category.setDescription("Category description");
      room.setRoomCategory(category);
      return room;
    } else {
      throw new RecordNotFoundException("");
    }
  }
  // rest of code omitted
};

使用这种方法,我们手动实现 InventoryService 接口并插入我们需要的代码。此外,我们为 roomId 值返回 Room 值,1 ,否则抛出 RecordNotFoundException

为了便于阅读,我们省略了此代码片段中公开的所有其他方法的存根。但这就是挑战在于简单模拟的地方。开发人员必须编写大量代码,这些代码对单元测试没有任何价值,但每当要向服务添加新功能时都需要维护。这就是模拟库 发挥作用的地方。下一节将讨论这样一个库的使用。

Implementation stubbing with a mocking library

在 Java 中模拟库时,有几个 选项,例如 jMock (http://www.jmock.org) 或 EasyMock (http://easymock.org)。最受欢迎的一种是 Mockito。 Mockito 是一个模拟框架,可让开发人员编写干净的测试并提供 简单的 API。开发人员可以通过向项目描述符添加 以下依赖项来将 Mockito 添加到他们的 Maven 项目中:

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>1.9.5</version>
  <scope>test</scope>
</dependency>

为了说明如何使用 Mockito,请看以下测试:

@Test
public void testUpdateRoom() {
  InventoryService service = mock(InventoryService.class);
  RoomCategory category = ... // skipped for clarity
  Room room = ... // skipped for clarity
  when(service.getRoom(1)).thenReturn(room);
  when(service.getRoomCategory(anyLong()))
  .thenReturn(category);
  Room updatedRoom = ... // skipped for clarity
  updatedRoom.setDescription("It's an awesome room!");
  ApiResponse response = resource.updateRoom(1, new RoomDTO(updatedRoom));

  assertEquals(Status.OK, response.getStatus());
  assertEquals("It's an awesome room!", ((RoomDTO) response.getData()).getDescription());
}

该测试验证可以更新现有房间。我们不是自己实现 InventoryService,而是使用 Mockito 创建一个带有 mock(InventoryService.class) 的模拟实现。下一步是指示我们的模拟在调用时返回房间和房间类别。这是通过 when(...).thenReturn(...) 实现的。这种模式为我们的模拟添加了行为。例如, when(service.getRoom(1)).thenReturn(room) 将使我们的模拟服务返回一个房间 当为房间 ID 1 调用 InventoryService.getRoom() 时。

Mockito and Spring

Spring 提供 一种机制,使用 @org.springframework.beans.factory.annotation 自动将 bean 注入到类中。自动接线。此注解可用作 如下:

@RestController
@RequestMapping("/bookings")
public class BookingsResource {

    @Autowired
    private BookingService bookingService;
}

在启动时,Spring 将查找类型为 InventoryService 的任何已声明 bean,并将其注入。这种机制减少了需要实现的样板代码量。但是,它使测试变得有点棘手,因为对于这段代码,我们无法直接访问 bookingService 实例变量。值得庆幸的是,Mockito 提供了几个有用的注释来规避这个限制。我们可以如下声明我们的测试类:

@RunWith(MockitoJUnitRunner.class)
public class BookingsResourceTest {

    @InjectMocks
    private BookingsResource resource;

    @Mock
    private BookingService bookingService;
}

第一个注释 (@org.junit.runner.RunWith) 允许我们用标准的 JUnit runner 代替 Mockito 的。使用 @org.mockito.InjectMocks,我们标记要注入的字段。此外,我们用 @org.mockito.Mock 注释 bookingService。这将自动生成一个模拟,该模拟将被注入到我们的 BookingsResource 实例中。然后我们可以使用为我们实例化的资源和服务类来运行我们的测试。

Note

在他们的 网站上阅读更多关于 Mockito 的信息:http ://mockito.org

Testing security


Spring 对单元测试有很好的支持。开发人员可以向他们的 Maven 项目添加新的依赖项以包括 Spring 测试支持,如下所示:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.1.6.RELEASE</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>4.0.1.RELEASE</version>
    <scope>test</scope>
</dependency>

因为我们不希望在发布的产品中包含这些依赖项,所以我们指定 test 作为范围。这样做只会使库在测试期间在类路径上可用。

第7章中,处理安全,我们为我们的示例物业管理系统的预订组件增加了安全性。作为参考,我们限制了对 RESTful 端点的访问以检索预订,如下所示:

@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value = "/{bookingId}", method = RequestMethod.GET)
public ApiResponse getBooking(@PathVariable("bookingId") long bookingId) {
    // omitted
}

使用标准单元测试,我们无法验证只有管理员才能访问此端点。使用 Spring 的测试支持,我们可以为这个端点编写单元测试,如下所示:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:booking-test.xml")
public class BookingsResourceTest {

  @Autowired
  private BookingService bookingService;
  @Autowired
  private BookingsResource resource;

  @Test(expected = AuthenticationCredentialsNotFoundException.class)
  public void testGetBookingNotLoggedIn() throws Exception {
    resource.getBooking(1);
  }

  @Test(expected = AccessDeniedException.class)
  @WithMockUser
  public void testGetBookingNotAdmin() throws Exception {
    resource.getBooking(1);
  }

  @Test
  @WithMockUser(roles = {"ADMIN"})
  public void testGetBookingValidUser() throws Exception {
    when(bookingService.getBooking(1)).thenReturn(new Booking());
    assertNotNull(resource.getBooking(1));
  }
}

通过@org.junit.runner.RunWith 注释测试类,我们替换了标准的 JUnit runner与春天的。

因为在 testGetBookingNotLoggedIn() 中没有定义身份验证,所以我们预计会抛出异常。

感谢@org.springframework.security.test.context.support.WithMockUser,我们的第二个测试方法声明了一个用户。但是,由于用户具有角色 USER,默认情况下,我们也期望抛出异常。

最后,第三个测试方法 (testGetBookingValidUser()) 声明了一个具有正确角色的用户,因此可以继续检索 ID 为 1 的预订

Integration testing


单元测试有助于确保各个类表现出正确的行为;集成测试侧重于系统不同组件之间的交互。系统越大,这种形式的测试就越重要。以下两节提供了创建有效集成策略的技术。

Continuous delivery

持续交付是一种软件工程实践,提倡在短周期内生产可以可靠发布的软件。传统上,一旦将更改提交到代码库,服务器就会构建软件并运行完整的测试套件。如果成功,软件可以自动部署到登台环境。然后可以针对新版本的软件执行集成测试。

使用 Maven,我们可以定义一种简单的方法,通过它们的命名约定将单元测试与集成测试分开;集成测试应该有后缀,IntegrationTest。然后可以将以下内容添加到项目描述符中:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <excludes>
      <exclude>**/*IntegrationTest.java</exclude>
    </excludes>
  </configuration>
  <executions>
    <execution>
      <id>integration-test</id>
      <goals>
        <goal>test</goal>
      </goals>
      <phase>integration-test</phase>
      <configuration>
        <excludes>
          <exclude>none</exclude>
        </excludes>
        <includes>
          <include>**/*IntegrationTest.java</include>
        </includes>
      </configuration>
    </execution>
  </executions>
</plugin>

使用此配置,可以首先运行单元测试。将软件部署到登台环境后,就可以运行集成测试。

Tip

在软件项目的整个生命周期中,部署 非常频繁。因此,花时间自动化部署过程是一项非常好的投资。它将节省开发人员的时间并促进持续交付。

Integration tests with Spring Boot

如以下代码片段:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = WebApplication.class)
@WebAppConfiguration
@IntegrationTest("integration_server:9000")
public class BookingsResourceIntegrationTest {

  @Test
  public void runTests() {
    // ...
  }
}

如本章前面所述,@org.junit.runner.RunWith 注解允许使用替代测试运行器。在这种情况下,我们将 JUnit 的默认运行器替换为 Spring 的。另外,我们通过@org.springframework.boot.test.IntegrationTest指定要连接的地址。使用这些注释,我们可以连接到运行我们的示例预订服务的服务器,并针对它运行一套集成测试。

Tip

您应该参考下一章的代码示例来远程调用 RESTful Web 服务。 相同的技术可以 用于实现集成测试。

Postman

正如我们在第5章中简要讨论的REST 中的 CRUD 操作,我们可以利用 Postman (https://www.getpostman.com) 编写测试套件以验证我们的 RESTful Web 服务。这种类型的测试实际上是集成测试。例如,如果我们编写一个通过 REST API 检查 房间可用性的测试,我们将测试系统的可用性、库存和预订组件。 Postman 提供了定义测试集合的能力,如以下屏幕截图所示:

读书笔记《building-a-restful-web-service-with-spring》测试REST风格的Web服务

我们可以快速运行对暂存环境的整个调用集合,并确保返回预期的响应。

Note

在其免费版本中,Postman 不提供对测试的完整支持。开发人员必须获得许可才能将断言添加到他们的调用中。

Postman and security

对于安全调用,例如 Chapter 7 中描述的调用,处理安全性< /em>,我们需要指示 Postman 发送正确的标头。对于 HTTP Basic 身份验证,我们首先需要生成 Base64 编码的用户名和密码对(例如 cmVzdDpyb2Nrcw== for rest:rocks< /code>),并添加 Authorization 标头以保护调用:

读书笔记《building-a-restful-web-service-with-spring》测试REST风格的Web服务

添加此标头后,调用将在服务器上进行身份验证并能够继续。

Other forms of testing


除了 unit 和集成测试之外,还应考虑其他形式的测试。它们将在下一节中描述。

User Acceptance Testing

用户验收测试UAT)着眼于用户的测试观点看法。对于 API,用户是使用服务的软件。无论用户类型如何,这种测试形式对于确保 RESTful Web 服务公开一致且功能完整的 API 都很重要。 UAT 的自动化程度往往低于其他类型的测试。但是,UAT 测试经理最终应该对软件解决方案是否已准备好通用 可用性拥有最终决定权。

Load testing

衡量 RESTful Web 服务的生产准备情况的另一个重要标准是将按照预期的服务水平协议SLA ) 在负载下。例如,在高峰时段,该服务可能预计每秒处理 1,000 个请求,平均响应时间不超过 250 毫秒。

有许多商业 和开源产品来测试Web 服务是否可以处理这种负载。最常见的是开源软件 Apache JMeter (http://jmeter.apache.org)。使用 JMeter,开发人员可以创建可以以定义的速率执行并捕获响应时间的测试计划。下面的屏幕截图显示了运行测试计划的结果,该计划包含对我们的示例物业管理系统的一次调用,以检索 ID 为 1 的房间:

读书笔记《building-a-restful-web-service-with-spring》测试REST风格的Web服务

我们同时执行了 http://localhost:8080/rooms/1 1000 次(10 个线程),平均响应时间为 11 毫秒。通过增加线程数,我们可以模拟更多的服务负载。

Note

模拟真实的生产负载并不容易实现。因此,服务设计人员可能会看到模拟负载和实际负载下的性能差异。这个事实并没有带走负载测试的价值。它只是建议服务设计人员不应仅仅依靠负载测试结果来确保满足 SLA。

Summary


在本章中,我们讨论了开发人员如何彻底测试 RESTful Web 服务。除了传统的单元测试,我们还涵盖了安全测试、集成测试,并简要介绍了其他类型的测试,例如 UAT 和负载测试。

到目前为止,在本书中,我们已经从服务器端查看了 Web 服务。然而,对于任何这样的服务,甚至任何软件,要想有用,就需要使用它。在我们的上下文中,RESTful Web 服务只有在客户端使用时才有用。

在下一章中,我们将了解如何使用 Spring 构建客户端。