vlambda博客
学习文章列表

读书笔记《cloud-native-applications-in-java》测试云本地应用程序

Chapter 5. Testing Cloud-Native Applications

在本章中,我们将深入测试云原生应用程序。使用各种测试工具、策略和模式,测试从手动测试到自动化测试已经成熟了很多。这种方法的好处是可以以对云开发很重要的故障保护方式频繁地进行测试。

我们将在本章中介绍以下主题:

  • Testing concepts, such as behavior-driven development (BDD) and test-driven development (TDD)
  • Testing patterns, such as A/B testing and test doubles
  • Testing tools, such as JUnit, Cucumber, JaCoCo, and Spring Test
  • Types of testing, such as unit, integration, performance, and stress testing
  • Applying the concepts of BDD and integration testing to the Product service that we developed in Chapter 2Writing Your First Cloud-Native Application, and enhanced in Chapter 4, Extending Your Cloud-Native Application

Writing test cases before development


在本书中,我们开始第 2 章, 在 Spring Boot 中编写您的第一个云原生应用程序,让您对云开发感到兴奋。然而,真正的开发遵循不同风格的最佳实践。

TDD

项目从了解需求 并编写验证需求的测试用例开始。由于此时代码不存在,因此测试用例将失败。然后,编写通过测试用例的代码。这个过程迭代直到测试用例和所需的代码完成以实现业务功能。 Kent Beck 有一本关于这个主题的优秀书籍 Test Driven Development by Example。在下一节中,我们将重做 Chapter 4 中的产品服务, 使用本章中的原则扩展您的云原生应用程序。但在此之前,让我们看看另一个重要的概念,BDD。

BDD

借鉴敏捷开发原则和用户故事,BDD 鼓励我们将开发视为一组场景,其中,在特定条件下,系统会以特定的、可预测的方式对设定的刺激进行行为。如果这些场景、条件和动作可以在业务和 IT 团队之间以一种易于理解的通用语言来捕捉,这将为开发带来很多清晰性,并减少犯错的机会。这是一种编写易于测试的规范的方法。

在本章中继续前进,我们将使用 Cucumber 工具获取我们的产品服务并对其应用 BDD。

Testing patterns


为云测试大型互联网应用程序需要一种规范的方法,其中一些模式可以派上用场。

A/B testing

A/B 测试(也称为 拆分测试)的初衷是实验 找出几个选定用户对 same 的两个不同网页的用户响应 功能。如果与另一组相比,用户对某个模式的反应良​​好,则选择该模式。

这个概念可以扩展到分阶段引入新功能。将功能、活动、布局或新服务介绍给一组受控用户并测量响应:

读书笔记《cloud-native-applications-in-java》测试云本地应用程序

测试窗口结束后,将汇总结果以计划更新功能的有效性。

这种测试的策略是,对于选定的一组用户,使用 HTTP 302(临时重定向)将用户从常规网站切换到新设计的网站。这将需要在测试期间运行网站或功能服务的变体。一旦测试成功,该功能会慢慢扩展到更多用户,并合并到主网站/代码库中。

Test doubles

通常,被测功能依赖依赖于其他团队独立开发的组件和API,具有以下缺点:

  • They may not be available for testing at the time of development of your functionality
  • They may not be always available and be set up with the data required for testing various cases
  • Using the actual components each time may be slower

因此,test double 的概念变得流行起来。测试替身(如电影中的特技替身)是替换实际组件并模仿其行为的组件/API。测试双重组件通常是轻量级且易于更改的组件,由构建功能的团队控制,与可能是依赖项或外部流程的真实组件不同。

Note

有许多类型的测试替身,例如 Dummy、Fakes、Test Stubs 和 Mocks。

Test Stubs

Test 当下游组件返回改变被测系统行为的响应时,存根很有用;例如,如果我们的产品服务要调用参考数据服务,其输出决定产品服务的行为。参考数据服务的测试存根可以模拟导致产品服务行为变化的各种响应类型:

读书笔记《cloud-native-applications-in-java》测试云本地应用程序

Mock objects

下一种类型的测试替身是 Mock 对象,它记录 system 的行为方式,然后呈现记录以供验证.例如,一个 Mock 数据库组件可以检查是否没有为应该从缓存层而不是数据库响应的产品调用它。

这是 Mocks 周围生态系统的基本图表表示:

读书笔记《cloud-native-applications-in-java》测试云本地应用程序

Mock APIs

在云开发中,您将构建一个服务,该服务依赖于其他服务或主要依赖于访问服务的API。通常,其他服务将无法立即用于测试。但是你不能阻止你的发展。这就是模拟或添加虚拟服务是测试服务的有用模式的地方。

服务模拟模拟真实服务的所有合同和行为。一些示例,例如 WireMock.orgMockable.io 帮助我们模拟 API 并测试主要情况、边缘情况和失败情况。

Types of testing


我们在本章后面讨论的各种类型的测试甚至在云计算流行之前就已经为人所知。使用 持续集成 (CI) 和持续开发CD)使得自动化这些类型变得很重要的测试,以便在每次代码签入和构建发生时执行

Unit testing

单元测试的目的是测试每个类或代码 component 并确保它执行<一个 id="id288371778" class="indexterm"> 符合预期。 JUnit 是用于单元测试的流行 Java 框架。

使用 Mock 对象模式和测试存根,可以隔离被测试服务的依赖组件,以便测试集中在被测系统,也就是服务上。

JUnit 是最流行的单元测试工具。

Integration testing

component 测试的目的是检查产品服务等组件是否按预期执行。

 spring-boot-test 等组件有助于运行测试套件并在 whole 组件。我们将在本章中看到这一点。

Load testing

负载测试包括在一段时间内向被测系统推送大量并发请求并观察效果,​​如响应系统上的时间和错误率。如果添加更多服务实例使其能够处理 额外负载,则称该系统具有水平可扩展性。

JMeter 和 Gatling 是覆盖这个维度的流行工具。

Regression testing

在引入 new 功能时,不应破坏现有功能。回归测试涵盖了这一点。

Selenium 是一个基于 Web 浏览器的开放 source 工具,在这个领域很流行,用于执行回归测试。

Ensuring code review and coverage

代码的人工审查由自动代码审查工具增强。这有助于识别代码中任何可能的错误,并确保覆盖范围完整且所有路径都经过测试。

稍后我们将查看代码覆盖率工具 JaCoCo。

Testing the Product service


让我们将我们学到的测试原则应用到我们目前正在构建的 Product 服务中。我们从用户的角度开始,因此从验收测试开始。

BDD through Cucumber

第一步是回忆我们产品服务的规范。在第 4 章中, 扩展您的云原生应用程序< /span>,我们在产品服务上构建了一些功能,允许我们获取、添加、修改和删除产品,并获取 product 给定产品类别的 ID。

让我们将其表示为 Cucumber 中的特征。

Why Cucumber?

Cucumber 允许用类似简单英语的语言表达行为小黄瓜。这使得从领域驱动的设计术语中可以使用一种无​​处不在的语言,以便业务、开发和测试之间的通信无缝且易于理解。

How does Cucumber work?

让我们了解 Cucumber 的工作原理:

  1. The first step in Cucumber is to express the user story as features with scenarios, and Given-When-Then conditions:
    • Given: Sets the preconditions for the behavior
    • When: Trigger that changes the state of the system, for example, making a request to the service
    • Then: How the service should respond
  2. These are translated to automated test cases using the cucumber-spring translation layer so that they can be executed.

让我们从一个简单的 getProduct 验收测试用例开始。我们将在 Gherkin 中编写一个简单的功能,如果产品 ID 存在则获取产品,如果找不到产品 ID,则返回错误。

让我们以真正的 BDD 风格实现以下功能。产品服务上的 get API 返回产品详细信息,例如描述和给定产品 ID 的类别 ID。如果找不到产品,它也可能返回错误,例如,404。让我们在 Gherkin 特征文件中将这两种行为表示为两个独立的场景。

功能getProduct

获取给定产品 ID 的产品详细信息。

场景一:产品ID有效且存在。将返回其所属的产品名称和类别:

  1. Given the product service is running
  2. When the get product service is called with existing product ID 1
  3. Then we should get a response with HTTP status code 200
  4. And return product details with, name Apples and category 1

场景 2:产品 ID 无效或不存在。应该返回一个错误:

  1. Given product service is running
  2. When the get product service is called with a non-existing product ID 456
  3. Then return a 404 not found status
  4. And return the error message No product for ID 456

场景 1 是一个成功的场景,其中返回并验证了数据库中存在的产品 ID。

方案 2 检查数据库中不存在的 ID 的故障情况。

每个场景分为多个部分。对于快乐路径场景:

  • Given sets a precondition. In our case, it is simple enough: that the product service should be running.
  • When changes the state of the system and, in our case, it is making the request to the service by giving a product ID.
  • Then and And are the results that are expected on the system. In this case, we expect the service to return a 200 success code and the valid description and category codes for the given product.

您可能已经注意到,这是我们的服务文档,业务和测试团队以及开发人员都可以理解。它与技术无关;也就是说,如果实现是通过 Spring Boot、Ruby 或 .NET 微服务完成的,它不会改变。

在下一节中,我们会将服务映射到我们开发的 Spring Boot 应用程序。

Spring Boot Test

Spring Boot Test 扩展并简化了 Spring Framework 提供的Spring-test 模块。让我们看看编写 acceptance 测试的基本要素,然后我们可以在本章后面重新审视细节:

  1. Copy the project that we created in Chapter 4Extending Your Cloud-Native Application with HSQLDB and Hazelcast, as a new project for this chapter.
  1. Include the dependency on Spring in the Maven POM file:
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-test</artifactId> 
            <scope>test</scope> 
        </dependency> 

您可能已经注意到,scope 更改为 test。这意味着我们定义的依赖项对于正常运行时不是必需的,仅用于编译和测试执行。

  1. Add two more dependencies to Maven. We are downloading the libraries for Cucumber and its Java translation, along with spring-boot-starter-test:
        <dependency> 
            <groupId>info.cukes</groupId> 
            <artifactId>cucumber-spring</artifactId> 
            <version>1.2.5</version> 
            <scope>test</scope> 
        </dependency> 
        <dependency> 
            <groupId>info.cukes</groupId> 
            <artifactId>cucumber-junit</artifactId> 
            <version>1.2.5</version> 
            <scope>test</scope> 
        </dependency> 

CucumberTest class 是启动 Cucumber 测试的主类:

@RunWith(Cucumber.class) 
@CucumberOptions(features = "src/test/resources") 
public class CucumberTest { 
    
} 

RunWith 告诉 JUnit 使用 Spring 的测试支持,然后使用 Cucumber。我们给出了 .feature 文件的路径,该文件包含前面讨论的 Gherkin 中的测试用例。

Productservice.feature 文件 是包含 Gherkin 语言场景的文本文件,如前所述。我们将在这里展示两个测试用例。该文件位于 src/test/resources 文件夹中。

CucumberTestSteps 类包含将 Gherkin 中的步骤翻译成等效的 Java 代码。每个步骤对应一个方法,这些方法是根据 Gherkin 文件中的场景构建来调用的。让我们讨论与一个用例相关的所有步骤:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 
@ContextConfiguration 
public class CucumberTestSteps { 
  
    @Autowired 
    private TestRestTemplate restTemplate; 
  
    private ResponseEntity<Product> productResponse; 
    private ResponseEntity<String> errResponse; 
  
    @Given("(.*) Service is running") 
    public void checkServiceRunning(String serviceName) { 
         ResponseEntity<String> healthResponse = restTemplate.getForEntity("/health",String.class, new HashMap<>()); 
         Assert.assertEquals(HttpStatus.OK, healthResponse.getStatusCode()); 
    } 
  
    @When("get (.*) service is called with existing product id (\d+)$") 
    public void callService(String serviceName, int prodId) throws Throwable { 
         productResponse = this.restTemplate.getForEntity("/"+serviceName+"/" + prodId, Product.class, new HashMap<>()); 
    } 
  
    @Then("I should get a response with HTTP status code (.*)") 
    public void shouldGetResponseWithHttpStatusCode(int statusCode) { 
         Assert.assertEquals(statusCode, productResponse.getStatusCodeValue()); 
    } 
     
    @And("return Product details with name (.*) and category (\d+)$") 
    public void theResponseShouldContainTheMessage(String prodName, int categoryId) { 
         Product product = productResponse.getBody() ; 
         Assert.assertEquals(prodName, product.getName()); 
         Assert.assertEquals(categoryId, product.getCatId());       
    } 

@SpringBootTest 注解告诉 Spring Boot Framework 它是一个测试类。 RANDOM_PORT 表示测试服务在随机端口启动Tomcat进行测试。

我们注入一个自动连接的 restTemplate ,这将有助于访问 HTTP/REST 服务并接收将被测试的响应。

现在,注意带有注释的方法 @Given@When@Then< /代码>。每个方法都使用正则表达式来提取变量(从特征文件中)并将其用于方法中的断言。我们通过执行以下操作系统地测试了这一点:

  1. Checking whether the service is running first by accessing the /health (as we did for Spring Boot Actuator in Chapter 2, Writing Your First Cloud-Native Application).
  2. Calling the service with the product ID.
  3. Checking whether the return code is 200 and the description and category of the response match the expected result.
  4. Running the tests.
  5. Right-clicking the CucumberTest.java file and selecting Run As | JUnit Test:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序

您将看到控制台启动并显示启动消息。最后,JUnit 将测试结果反映如下:

读书笔记《cloud-native-applications-in-java》测试云本地应用程序

作为一个练习,尝试将测试用例添加到插入、更新和删除 产品方法中"literal">ProductService 类。

Code coverage using JaCoCo

JaCoCo 是 EclEmma 团队开发的一个代码 coverage 库。 JaCoCo 在 JVM 中嵌入了一个代理,它扫描遍历的代码路径并创建报告。

该报告可以导入更广泛的 DevOps 代码质量工具,例如 SonarQube。 SonarQube 是一个平台,它通过众多插件帮助管理代码质量,并与 DevOps 流程很好地集成(我们将在后面的章节中看到)。它是开源的,但也有商业版本。它是一个平台,因为它具有多个组件,例如服务器(计算引擎服务器、Web 服务器和 Elasticsearch)、数据库和特定于语言的扫描仪。

Integrating JaCoCo

让我们将 JaCoCo 集成到我们现有的项目中:

  1. First, include the plugin that includes JaCoCo in the POM file:
<plugin> 
   <groupId>org.jacoco</groupId> 
   <artifactId>jacoco-maven-plugin</artifactId> 
   <version>0.7.9</version> 
</plugin> 

Note

第二步和第三步是在前面的插件中包含预执行和后执行。

  1. The pre-execution prepares the agent to be configured and added to the command line.
  2. The post-execution ensures that the reports get created in the output folders:
<executions> 
   <execution> 
         <id>pre-unit-test</id> 
         <goals> 
               <goal>prepare-agent</goal> 
         </goals> 
         <configuration> 
               <destFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</destFile> 
               <propertyName>surefireArgLine</propertyName> 
         </configuration> 
   </execution> 
   <execution> 
         <id>post-unit-test</id> 
         <phase>test</phase> 
         <goals> 
               <goal>report</goal> 
         </goals> 
         <configuration> 
               <dataFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</dataFile> 
   <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory> 
         </configuration> 
   </execution> 
</executions> 
  1. Finally, the created command-line change has to be inserted into the maven-surefire-plugin as follows:
<plugin> 
   <groupId>org.apache.maven.plugins</groupId> 
   <artifactId>maven-surefire-plugin</artifactId> 
   <configuration> 
         <!-- Sets the VM argument line used when unit tests are run. --> 
         <argLine>${surefireArgLine}</argLine> 
         <excludes> 
               <exclude>**/IT*.java</exclude> 
         </excludes>        
   </configuration> 
</plugin> 
  1. Now, we are all set to run the coverage report. Right-click on the project and select Run As | Maven test to test the program, as shown in the following screenshot:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序
  1. As the console gets filled with the Spring Boot initiation, you will find the following lines:
2 Scenarios ([32m2 passed[0m) 
8 Steps ([32m8 passed[0m) 
0m0.723s 
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 26.552 sec - in com.mycompany.product.CucumberTest......Results :Tests run: 10, Failures: 0, Errors: 0, Skipped: 0[INFO] [INFO] --- jacoco-maven-plugin:0.7.9:report (post-unit-test) @ product ---[INFO] Loading execution data file D:AppswkNeonch5-producttargetcoverage-reportsjacoco-ut.exec[INFO] Analyzed bundle 'product' with 6 classes 
  1. This tells us that two scenarios were executed with 8 Steps (as before). But in addition, coverage-reports got generated and placed in the target directory:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序
  1. In the site folder, click on index.html; you will see the coverage report as follows:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序
  1. On investigation of the product package, you can see that the ProductService is only 24% covered, as shown in the following screenshot:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序
  1. The reason for this is that we have covered only the getProduct API in the service. The insertProduct and updateProduct have not been covered. This is showcased in the drill-down report in the following screenshot:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序
  1. On the getProduct method, the coverage is complete. This is because, in two scenarios, we have covered the happy path as well as the error condition:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序
  1. On the other hand, you will find that we have missed covering the branches in the ExceptionHandler class as follows:
读书笔记《cloud-native-applications-in-java》测试云本地应用程序

Summary


在接下来的章节中,我们会将覆盖率报告与 DevOps 管道集成,并在 CI 和 CD 期间查看它的工作情况。但是,首先,让我们看一下部署机制。