查看原文
其他

测试微服务:工具和框架

xuli SpringForAll社区 2020-10-17

我们面临的微服务架构测试存在一些关键挑战。选择正确的工具是帮助我们处理与这些挑战相关的问题的要素之一。首先,让我们确定微服务测试过程中涉及的最重要的元素。下面是其中一些:

  • 团队协调 - 由于许多独立团队管理自己的微服务,协调软件开发和测试的整个过程变得非常具有挑战性

  • 复杂性 - 有许多微服务相互通信。我们需要确保它们中的每一个都正常工作,并且能够抵抗来自其他微服务的缓慢响应或故障

  • 性能 - 由于有许多独立服务,因此在接近生产的流量下测试整个架构非常重要

让我们讨论一些有用的框架,帮助您测试基于微服务的架构。

使用Hoverfly进行组件测试

Hoverfly 仿真模式对于构建组件测试尤其有用。在组件测试期间,我们同时验证了整个微服务,不需要通过网络与其他微服务或外部数据存储进行通信。下图显示了如何对我们的样品微服务执行此类测试。

Hoverfly提供了用于创建模拟的简单DSL,以及用于在JUnit测试中使用的JUnit集成。它可以通过JUnit的@Rule注解进行编排。我们正在模拟两个服务,然后重写Ribbon属性以按客户端名称解析这些服务的地址。我们还应该通过在应用程序启动后禁用注册或获取Ribbon客户端的服务列表来禁用与Eureka发现的通信。Hoverfly通过暴露的方法passenger-management和driver-management微服务模拟PUT和GET 的响应。Controller是在我们的应用程序中实现业务逻辑的主要组件。它使用内存存储库组件存储数据,并通过@FeignClient接口与其他微服务进行通信。通过测试控制器实现的三种方法,我们在trip-management服务内部测试了整个业务逻辑。

  1. @SpringBootTest(properties = {

  2.        "eureka.client.enabled=false",

  3.        "ribbon.eureka.enable=false",

  4.        "passenger-management.ribbon.listOfServers=passenger-management",

  5.        "driver-management.ribbon.listOfServers=driver-management"

  6. })

  7. @RunWith(SpringRunner.class)

  8. @AutoConfigureMockMvc

  9. @FixMethodOrder(MethodSorters.NAME_ASCENDING)

  10. public class TripComponentTests {


  11.    ObjectMapper mapper = new ObjectMapper();


  12.    @Autowired

  13.    MockMvc mockMvc;


  14.    @ClassRule

  15.    public static HoverflyRule rule = HoverflyRule.inSimulationMode(SimulationSource.dsl(

  16.            HoverflyDsl.service("passenger-management:80")

  17.                    .get(HoverflyMatchers.startsWith("/passengers/login/"))

  18.                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Walker'}")))

  19.                    .put(HoverflyMatchers.startsWith("/passengers")).anyBody()

  20.                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Walker'}"))),

  21.            HoverflyDsl.service("driver-management:80")

  22.                    .get(HoverflyMatchers.startsWith("/drivers/"))

  23.                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'David Smith','currentLocationX': 15,'currentLocationY':25}")))

  24.                    .put(HoverflyMatchers.startsWith("/drivers")).anyBody()

  25.                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'David Smith','currentLocationX': 15,'currentLocationY':25}")))

  26.    )).printSimulationData();


  27.    @Test

  28.    public void test1CreateNewTrip() throws Exception {

  29.        TripInput ti = new TripInput("test", 10, 20, "walker");

  30.        mockMvc.perform(MockMvcRequestBuilders.post("/trips")

  31.                .contentType(MediaType.APPLICATION_JSON_UTF8)

  32.                .content(mapper.writeValueAsString(ti)))

  33.                .andExpect(MockMvcResultMatchers.status().isOk())

  34.                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.any(Integer.class)))

  35.                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("NEW")))

  36.                .andExpect(MockMvcResultMatchers.jsonPath("$.driverId", Matchers.any(Integer.class)));

  37.    }


  38.    @Test

  39.    public void test2CancelTrip() throws Exception {

  40.        mockMvc.perform(MockMvcRequestBuilders.put("/trips/cancel/1")

  41.                .contentType(MediaType.APPLICATION_JSON_UTF8)

  42.                .content(mapper.writeValueAsString(new Trip())))

  43.                .andExpect(MockMvcResultMatchers.status().isOk())

  44.                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.any(Integer.class)))

  45.                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("IN_PROGRESS")))

  46.                .andExpect(MockMvcResultMatchers.jsonPath("$.driverId", Matchers.any(Integer.class)));

  47.    }


  48.    @Test

  49.    public void test3PayTrip() throws Exception {

  50.        mockMvc.perform(MockMvcRequestBuilders.put("/trips/payment/1")

  51.                .contentType(MediaType.APPLICATION_JSON_UTF8)

  52.                .content(mapper.writeValueAsString(new Trip())))

  53.                .andExpect(MockMvcResultMatchers.status().isOk())

  54.                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.any(Integer.class)))

  55.                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("PAYED")));

  56.    }


  57. }

上面显示的测试仅验证了正常的情景。如何测试网络延迟或服务器错误等意外行为?使用Hoverfly,我们可以轻松地模拟这种行为并定义一些负面情景。在下面的代码片段中,我定义了三个场景。第一个目标服务已延迟2秒。为了模拟客户端的超时,我必须更改readTimeoutRibbon负载均衡器的默认值,然后禁用Feign客户端的Hystrix断路器。第二个测试模拟来自passenger-management服务的HTTP 500响应状态。最后一个场景假定负责搜索最近的驱动程序的方法返回空响应。

  1. @SpringBootTest(properties = {

  2.        "eureka.client.enabled=false",

  3.        "ribbon.eureka.enable=false",

  4.        "passenger-management.ribbon.listOfServers=passenger-management",

  5.        "driver-management.ribbon.listOfServers=driver-management",

  6.        "feign.hystrix.enabled=false",

  7.        "ribbon.ReadTimeout=500"

  8. })

  9. @RunWith(SpringRunner.class)

  10. @AutoConfigureMockMvc

  11. public class TripNegativeComponentTests {


  12.    private ObjectMapper mapper = new ObjectMapper();

  13.    @Autowired

  14.    private MockMvc mockMvc;


  15.    @ClassRule

  16.    public static HoverflyRule rule = HoverflyRule.inSimulationMode(SimulationSource.dsl(

  17.            HoverflyDsl.service("passenger-management:80")

  18.                    .get("/passengers/login/test1")

  19.                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Smith'}")).withDelay(2000, TimeUnit.MILLISECONDS))

  20.                    .get("/passengers/login/test2")

  21.                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Smith'}")))

  22.                    .get("/passengers/login/test3")

  23.                    .willReturn(ResponseCreators.serverError()),

  24.            HoverflyDsl.service("driver-management:80")

  25.                    .get(HoverflyMatchers.startsWith("/drivers/"))

  26.                    .willReturn(ResponseCreators.success().body("{}"))

  27.            ));

  28.    @Test

  29.    public void testCreateTripWithTimeout() throws Exception {

  30.        mockMvc.perform(MockMvcRequestBuilders.post("/trips").contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(new TripInput("test", 15, 25, "test1"))))

  31.                .andExpect(MockMvcResultMatchers.status().isOk())

  32.                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.nullValue()))

  33.                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("REJECTED")));

  34.    }


  35.    @Test

  36.    public void testCreateTripWithError() throws Exception {

  37.        mockMvc.perform(MockMvcRequestBuilders.post("/trips").contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(new TripInput("test", 15, 25, "test3"))))

  38.                .andExpect(MockMvcResultMatchers.status().isOk())

  39.                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.nullValue()))

  40.                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("REJECTED")));

  41.    }


  42.    @Test

  43.    public void testCreateTripWithNoDrivers() throws Exception {

  44.        mockMvc.perform(MockMvcRequestBuilders.post("/trips").contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(new TripInput("test", 15, 25, "test2"))))

  45.                .andExpect(MockMvcResultMatchers.status().isOk())

  46.                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.nullValue()))

  47.                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("REJECTED")));

  48.    }


  49. }

与外部微服务通信的所有超时和错误都由注释为@ControllerAdvice的bean来处理。 在这种情况下,trip-management微服务不应该返回服务器错误响应,但200 OK,包含字段状态的JSON响应等于REJECTED。

  1. @ControllerAdvice

  2. public class TripControllerErrorHandler extends ResponseEntityExceptionHandler {


  3.    @ExceptionHandler({RetryableException.class, FeignException.class})

  4.    protected ResponseEntity handleFeignTimeout(RuntimeException ex, WebRequest request) {

  5.        Trip trip = new Trip();

  6.        trip.setStatus(TripStatus.REJECTED);

  7.        return handleExceptionInternal(ex, trip, null, HttpStatus.OK, request);

  8.    }


  9. }

使用Pact进行合约测试

通常为基于微服务的架构实施的下一类测试策略是消费者驱动的合约测试。实际上,有一些专门用于此类测试的工具。其中之一是Pact。合约测试是一种确保服务可以相互通信而无需实施集成测试的方法。 通信双方签订合同:消费者和提供者。Pact假定合同代码是在消费者方面生成和发布的,而不是由提供商验证。

Pact提供了可以存储和共享消费者和提供者之间的合约的工具。它被称为Pact Broker。它公开了一个简单的RESTful API,用于发布和检索pacts,以及用于导航API的嵌入式web仪表板。我们可以使用Docker镜像在本地计算机上轻松运行Pact Broker。

我们将从运行Pact Broker开始。Pact Broker需要运行postgresql的实例,所以首先我们必须使用Docker镜像启动它,然后将我们的代理容器与该容器链接

  1. docker run -d --name postgres -p 5432:5432 -e POSTGRES_USER=oauth -e POSTGRES_PASSWORD=oauth123 -e POSTGRES_DB=oauth postgres

  2. docker run -d --name pact-broker --link postgres:postgres -e PACT_BROKER_DATABASE_USERNAME=oauth -e PACT_BROKER_DATABASE_PASSWORD=oauth123 -e PACT_BROKER_DATABASE_HOST=postgres -e PACT_BROKER_DATABASE_NAME=oauth -p 9080:80 dius/pact-broke

下一步是在消费者方面实施合约测试。 我们将使用PVM库的JVM实现。它提供PactProviderRuleMk2负责创建提供者服务的存根的对象。我们应该用JUnit的@Rule注解来注释它。Ribbon会将所有请求passenger-management的转发到存根地址 - 在这种情况下就是localhost:8180。Pact JVM支持注释并提供用于构建测试场景的DSL。负责生成合约数据的测试方法应注明@Pact。设置字段状态和提供者很重要,因为生成的合约将使用这些名称在提供者端进行验证。生成合约同样的在使用@PactVerification方法注解测试中被验证。字段片段指向负责在同一测试类中生成合约的方法的名称。合约测试使用PassengerManagementClient @FeignClient。

  1. @RunWith(SpringRunner.class)

  2. @SpringBootTest(properties = {

  3.        "driver-management.ribbon.listOfServers=localhost:8190",

  4.        "passenger-management.ribbon.listOfServers=localhost:8180",

  5.        "ribbon.eureka.enabled=false",

  6.        "eureka.client.enabled=false",

  7.        "ribbon.ReadTimeout=5000"

  8. })

  9. public class PassengerManagementContractTests {


  10.    @Rule

  11.    public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("passengerManagementProvider", "localhost", 8180, this);

  12.    @Autowired

  13.    private PassengerManagementClient passengerManagementClient;


  14.    @Pact(state = "get-passenger", provider = "passengerManagementProvider", consumer = "passengerManagementClient")

  15.    public RequestResponsePact callGetPassenger(PactDslWithProvider builder) {

  16.        DslPart body = new PactDslJsonBody().integerType("id").stringType("name").numberType("balance").close();

  17.        return builder.given("get-passenger").uponReceiving("test-get-passenger")

  18.                .path("/passengers/login/test").method("GET").willRespondWith().status(200).body(body).toPact();

  19.    }


  20.    @Pact(state = "update-passenger", provider = "passengerManagementProvider", consumer = "passengerManagementClient")

  21.    public RequestResponsePact callUpdatePassenger(PactDslWithProvider builder) {

  22.        return builder.given("update-passenger").uponReceiving("test-update-passenger")

  23.                .path("/passengers").method("PUT").bodyWithSingleQuotes("{'id':1,'amount':1000}", "application/json").willRespondWith().status(200)

  24.                .bodyWithSingleQuotes("{'id':1,'name':'Adam Smith','balance':5000}", "application/json").toPact();

  25.    }


  26.    @Test

  27.    @PactVerification(fragment = "callGetPassenger")

  28.    public void verifyGetPassengerPact() {

  29.        Passenger passenger = passengerManagementClient.getPassenger("test");

  30.        Assert.assertNotNull(passenger);

  31.        Assert.assertNotNull(passenger.getId());

  32.    }


  33.    @Test

  34.    @PactVerification(fragment = "callUpdatePassenger")

  35.    public void verifyUpdatePassengerPact() {

  36.        Passenger passenger = passengerManagementClient.updatePassenger(new PassengerInput(1L, 1000));

  37.        Assert.assertNotNull(passenger);

  38.        Assert.assertNotNull(passenger.getId());

  39.    }


  40. }

仅运行测试是不够的。我们还必须将测试期间生成的协议发布到Pact Broker。为了实现它,我们必须包含以下Maven插件到我们pom.xml,然后执行命令mvn clean install pact:publish。

  1. <plugin>

  2.    <groupId>au.com.dius</groupId>

  3.    <artifactId>pact-jvm-provider-maven_2.12</artifactId>

  4.    <version>3.5.21</version>

  5.    <configuration>

  6.        <pactBrokerUrl>http://192.168.99.100:9080</pactBrokerUrl>

  7.    </configuration>

  8. </plugin>

Pact在提供者方面为Spring提供了支持。幸好我们可以使用MockMvc控制器或通过application.yml将属性注入到测试类中。这里的依赖声明必须包含在我们的pom.xml

  1. <dependency>

  2.    <groupId>au.com.dius</groupId>

  3.    <artifactId>pact-jvm-provider-spring_2.12</artifactId>

  4.    <version>3.5.21</version>

  5.    <scope>test</scope>

  6. </dependency>

现在,合约正在提供商方面进行验证。我们需要在@Provider注解的每个验证测试中传递提供者名称,以及在@State注解的每个验证测试中传递状态的名称。这些值是在@Pact注释(字段state和provider)内的消费者端测试期间进行测试。

  1. @RunWith(SpringRestPactRunner.class)

  2. @Provider("passengerManagementProvider")

  3. @PactBroker

  4. public class PassengerControllerContractTests {


  5.    @InjectMocks

  6.    private PassengerController controller = new PassengerController();

  7.    @Mock

  8.    private PassengerRepository repository;

  9.    @TestTarget

  10.    public final MockMvcTarget target = new MockMvcTarget();

  11.    @Before

  12.    public void before() {

  13.        MockitoAnnotations.initMocks(this);

  14.        target.setControllers(controller);

  15.    }


  16.    @State("get-passenger")

  17.    public void testGetPassenger() {

  18.        target.setRunTimes(3);

  19.        Mockito.when(repository.findByLogin(Mockito.anyString()))

  20.                .thenReturn(new Passenger(1L, "Adam Smith", "test", 4000))

  21.                .thenReturn(new Passenger(3L, "Tom Hamilton", "hamilton", 400000))

  22.                .thenReturn(new Passenger(5L, "John Scott", "scott", 222));

  23.    }


  24.    @State("update-passenger")

  25.    public void testUpdatePassenger() {

  26.        target.setRunTimes(1);

  27.        Passenger passenger = new Passenger(1L, "Adam Smith", "test", 4000);

  28.        Mockito.when(repository.findById(1L)).thenReturn(passenger);

  29.        Mockito.when(repository.update(Mockito.any(Passenger.class)))

  30.                .thenReturn(new Passenger(1L, "Adam Smith", "test", 5000));

  31.    }

  32. }

合约节点的主机和端口通过application.yml 文件注入

  1. pactbroker:

  2.  host: "192.168.99.100"

  3.  port: "8090"

使用Gatling进行性能测试

在将微服务部署到生产之前测试微服务的一个重要步骤是性能测试。这个领域的一个有趣的工具是Gatling。它是用Scala编写的高性能负载测试工具。这意味着我们还必须使用Scala DSL来构建测试场景。让我们从将所需的库添加到pom.xml文件开始。

  1. <dependency>

  2.    <groupId>io.gatling.highcharts</groupId>

  3.    <artifactId>gatling-charts-highcharts</artifactId>

  4.    <version>2.3.1</version>

  5. </dependency>

现在,我们可以继续进行测试。在上面显示的场景中,我们正在测试两个端点trip-management:POST /trips和PUT /trips/payment/${tripId}。事实上,这个场景验证了我们的样本系统的整个功能,我们在那里设置行程,然后在完成后付费。

每个使用Gatling的测试类都需要扩展Simulation类。我们使用scenario方法定义场景,然后设置其名称。我们可以在单个场景中定义多个执行。每次执行POST /trips方法测试后,保存服务返回的生成的id。然后它将该id插入到用于调用方法的URL中PUT /trips/payment/${tripId}。每个测试都要求200 OK状态的响应。

Gatling提供了两个有趣的功能,值得一提的是,您可以在以下性能测试中看到它们的使用方式。首先是feeder。它用于轮询记录并将其内容注入测试。Feed rPassengers随机选择五个定义的登录中的一个。可以使用Assertions API验证最终测试结果。它负责验证全局统计信息,例如响应时间或失败请求的数量与整个模拟的预期相匹配。在下面可见的场景中,标准是最大响应时间需要低于100毫秒。

  1. class CreateAndPayTripPerformanceTest extends Simulation {


  2.  val rPassengers = Iterator.continually(Map("passenger" -> List("walker","smith","hamilton","scott","holmes").lift(Random.nextInt(5)).get))


  3.  val scn = scenario("CreateAndPayTrip").feed(rPassengers).repeat(100, "n") {

  4.    exec(http("CreateTrip-API")

  5.      .post("http://localhost:8090/trips")

  6.      .header("Content-Type", "application/json")

  7.      .body(StringBody("""{"destination":"test${n}","locationX":${n},"locationY":${n},"username":"${passenger}"}"""))

  8.      .check(status.is(200), jsonPath("$.id").saveAs("tripId"))

  9.    ).exec(http("PayTrip-API")

  10.      .put("http://localhost:8090/trips/payment/${tripId}")

  11.      .header("Content-Type", "application/json")

  12.      .check(status.is(200))

  13.    )

  14.  }


  15.  setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(5, TimeUnit.MINUTES))

  16.    .assertions(global.responseTime.max.lt(100))


  17. }

要运行Gatling性能测试,您需要将以下Maven插件包含在您的pom.xml中。您可以运行单个方案或运行多个方案。包含插件后,您只需要执行命令mvn clean gatling:test。

  1. <plugin>

  2.    <groupId>io.gatling</groupId>

  3.    <artifactId>gatling-maven-plugin</artifactId>

  4.    <version>2.2.4</version>

  5.    <configuration>

  6.        <simulationClass>pl.piomin.performance.tests.CreateAndPayTripPerformanceTest</simulationClass>

  7.    </configuration>

  8. </plugin>

以下是一些图表,说明了我们的微服务性能测试结果。由于最大响应时间大于设置内部断言(100ms),因此测试失败。

概括

正确选择工具并不是微服务测试中最重要的元素阶段。但是,正确的工具可以帮助您应对与之相关的关键挑战。Hoverfly允许创建完整的组件测试,以验证您的微服务是否能够处理来自下游服务的延迟或错误。Pact通过共享和验证独立开发的微服务之间的合同来帮助您组织团队。最后,Gatling可以帮助您为选定的方案实施负载测试,以验证系统的端到端性能。用作本文演示的源代码可在GitHub上找到:https://github.com/piomin/sample-testing-microservices. 如果您觉得这篇文章很有趣,您可能也会对与此主题相关的其他文章感兴趣:

  • 使用Hoverfly测试REST API

  • 使用Gatling进行性能测试

  • 与Jenkins,Artifactory和Spring Cloud契约的持续集成

原文链接:https://piotrminkowski.wordpress.com/2018/09/06/testing-microservices-tools-and-frameworks/

作者:Piotr Mińkowski

译者:xuli


推荐: SpringBoot WebFlux 入门案例

上一篇:何时(不)使用Java抽象类


 关注公众号

点击原文阅读更多



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存