1.简介
自从肯特·贝克 ( Kent Beck )十多年前提出了测试驱动开发 ( TDD )的想法以来,测试就成为每个旨在成功的软件项目中必不可少的一部分。 多年过去了,软件系统的复杂性已经大大增加,测试技术也得到了极大提高,但是相同的基本原理仍然存在并且仍在应用。
高效和有效的测试是一个非常大的主题,充满了意见和永无止境该做什么和不该做什么的争论所包围。 有很多理由认为测试是一门艺术 。 在本教程的这一部分中,我们将不参加任何训练营,而是专注于测试按照微服务体系结构原理实现的应用程序。 即使是在这样狭窄的主题中,也有太多话题需要讨论,因此本教程的后续部分将分别致力于性能和安全性测试。
但是在开始之前,请花一些时间仔细研究一下Marin Fowler的微服务体系结构中的测试策略 ,这是管理微服务领域中测试复杂性的方法的精妙 ,详尽且说明充分的摘要。
2.单元测试
单元测试可能是最简单但功能非常强大的测试形式,它并不是真正针对微服务的,而是任何类型的应用程序或服务。
单元测试将使用应用程序中最小的可测试软件,以确定其性能是否符合预期。 – https://martinfowler.com/articles/microservice-testing/#testing-unit-introduction
单元测试通常应该构成应用程序测试套件的最大部分(根据实际的测试金字塔 ),因为它们应该很容易编写并且执行起来很快。 在Java中, JUnit框架( JUnit 4和JUnit 5 )如今已成为事实(尽管也广泛使用了诸如TestNG或Spock之类的其他框架)。
单元测试的一个好例子是什么? 令人惊讶的是,这是一个很难回答的问题,但是要遵循一些规则:它应该单独测试一个特定的组件(“单元”),应该一次测试一件事,并且应该很快。
JCG租车平台的服务测试套件中包含许多单元测试 。 让我们选择客户服务并查看AddressToAddressEntityConverter
类的测试套件的片段,该类将Address
数据传输对象转换为相应的JPA持久实体 。
public class AddressToAddressEntityConverterTest {
private AddressToAddressEntityConverter converter;
@Before
public void setUp() {
converter = new AddressToAddressEntityConverter();
}
@Test
public void testConvertingNullValueShouldReturnNull() {
assertThat(converter.convert(null)).isNull();
}
@Test
public void testConvertingAddressShouldSetAllNonNullFields() {
final UUID uuid = UUID.randomUUID();
final Address address = new Address(uuid)
.withStreetLine1("7393 Plymouth Lane")
.withPostalCode("19064")
.withCity("Springfield")
.withStateOrProvince("PA")
.withCountry("United States of America");
assertThat(converter.convert(address))
.isNotNull()
.hasFieldOrPropertyWithValue("uuid", uuid)
.hasFieldOrPropertyWithValue("streetLine1", "7393 Plymouth Lane")
.hasFieldOrPropertyWithValue("streetLine2", null)
.hasFieldOrPropertyWithValue("postalCode", "19064")
.hasFieldOrPropertyWithValue("city", "Springfield")
.hasFieldOrPropertyWithValue("stateOrProvince", "PA")
.hasFieldOrPropertyWithValue("country", "United States of America");
}
}
该测试非常简单,易于阅读,理解和排除将来可能发生的任何故障。 在实际项目中,单元测试可能会很快失控,变得肿且难以维护。 目前尚无针对这种疾病的通用治疗方法,但一般建议是将测试用例视为主流代码。
3.集成测试
实际上,我们应用程序中的组件(或“单元”)通常依赖于其他组件,数据存储,外部服务,缓存,消息代理等……由于单元测试的重点是隔离性,因此我们需要上一层并切换完成集成测试 。
集成测试验证组件之间的通信路径和交互,以检测接口缺陷。 – https://martinfowler.com/articles/microservice-testing/#testing-integration-introduction
展示集成测试功能的最佳示例可能是提供套件来测试持久层。 这是Arquillian , Mockito , DBUnit , Wiremock , Testcontainers , REST Assured (以及许多其他框架)之类的框架所主导的领域。
让我们回到客户服务部门 ,考虑如何确保客户数据确实在数据库中永久存在。 我们有专门的RegistrationService来管理注册过程,因此我们需要提供数据库实例,连接所有依赖项并启动注册过程。
@RunWith(Arquillian.class)
public class TransactionalRegistrationServiceIT {
@Inject private RegistrationService service;
@Deployment
public static JavaArchive createArchive() {
return ShrinkWrap
.create(JavaArchive.class)
.addClasses(CustomerJpaRepository.class, PersistenceConfig.class)
.addClasses(ConversionService.class, TransactionalRegistrationService.class)
.addPackages(true, "org.apache.deltaspike")
.addPackages(true, "com.javacodegeeks.rentals.customer.conversion")
.addPackages(true, "com.javacodegeeks.rentals.customer.registration.conversion");
}
@Test
public void testRegisterNewCustomer() {
final RegisterAddress homeAddress = new RegisterAddress()
.withStreetLine1("7393 Plymouth Lane")
.withPostalCode("19064")
.withCity("Springfield")
.withCountry("United States of America")
.withStateOrProvince("PA");
final RegisterCustomer registerCustomer = new RegisterCustomer()
.withFirstName("John")
.withLastName("Smith")
.withEmail("john@smith.com")
.withHomeAddress(homeAddress);
final UUID uuid = UUID.randomUUID();
final Customer customer = service.register(uuid, registerCustomer);
assertThat(customer).isNotNull()
.satisfies(c -> {
assertThat(c.getUuid()).isEqualTo(uuid);
assertThat(c.getFirstName()).isEqualTo("John");
assertThat(c.getLastName()).isEqualTo("Smith");
assertThat(c.getEmail()).isEqualTo("john@smith.com");
assertThat(c.getBillingAddress()).isNull();
assertThat(customer.getHomeAddress()).isNotNull()
.satisfies(a -> {
assertThat(a.getUuid()).isNotNull();
assertThat(a.getStreetLine1()).isEqualTo("7393 Plymouth Lane");
assertThat(a.getStreetLine2()).isNull();
assertThat(a.getCity()).isEqualTo("Springfield");
assertThat(a.getPostalCode()).isEqualTo("19064");
assertThat(a.getStateOrProvince()).isEqualTo("PA");
assertThat(a.getCountry()).isEqualTo("United States of America");
});
});
}
}
这是一个基于Arquillian的测试套件,在该套件中,我们已通过PostgreSQL兼容模式(通过属性文件)配置了内存中的H2数据库引擎 。 即使在这种配置下,它可能也要花费15-25秒才能运行,仍然比旋转PostgreSQL数据库的专用实例要快得多。
通过替换集成组件来交易集成测试执行时间是提高反馈速度的可行技术之一。 当然,它可能不适用于所有人,因此我们将在本教程的后面部分中再次讨论该主题。
如果您的微服务建立在Spring Framework和Spring Boot之上,例如我们的预订服务 ,您肯定会从自动配置的测试片和bean 模拟中受益。 下面的代码段是ReservationController
测试套件的一部分,说明了@WebFluxTest
测试切片在实际中的用法。
@WebFluxTest(ReservationController.class)
class ReservationControllerTest {
private final String username = "b36dbc74-1498-49bd-adec-0b53c2b268f8";
private final UUID customerId = UUID.fromString(username);
private final UUID vehicleId = UUID.fromString("397a3c5c-5c7b-4652-a11a-f30e8a522bf6");
private final UUID reservationId = UUID.fromString("3f8bc729-253d-4d8f-bff2-bc07e1a93af6");
@Autowired
private WebTestClient webClient;
@MockBean
private ReservationService service;
@MockBean
private InventoryServiceClient inventoryServiceClient;
@Test
@DisplayName("Should create Customer reservation")
@WithMockUser(roles = "CUSTOMER", username = username)
public void shouldCreateCustomerReservation() {
final OffsetDateTime reserveFrom = OffsetDateTime.now().plusDays(1);
final OffsetDateTime reserveTo = reserveFrom.plusDays(2);
when(inventoryServiceClient.availability(eq(vehicleId)))
.thenReturn(Mono.just(new Availability(vehicleId, true)));
when(service.reserve(eq(customerId), any()))
.thenReturn(Mono.just(new Reservation(reservationId)));
webClient
.mutateWith(csrf())
.post()
.uri("/api/reservations")
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters
.fromObject(new CreateReservation()
.withVehicleId(vehicleId)
.withFrom(reserveFrom)
.withTo(reserveTo)))
.exchange()
.expectStatus().isCreated()
.expectBody(Reservation.class)
.value(r -> {
assertThat(r)
.extracting(Reservation::getId)
.isEqualTo(reservationId);
});
}
}
公平地说,很高兴看到Spring团队在测试支持上投入了多少精力和想法。 我们不仅能够在不旋转服务器实例的情况下涵盖大部分请求和响应处理,而且测试执行时间非常快。
您经常会遇到的另一个有趣的概念,特别是在集成测试中 ,是使用伪造品 , 存根 , 测试双打和/或模拟 。
4.测试异步流
您很可能迟早会面临测试某种依赖于异步处理的功能的需求。 老实说,由于执行流程的不确定性,因此不使用专用的调度程序或执行程序确实很困难。
如果回头讨论微服务实现的那一刻,我们将遇到依赖CDI 2.0提供的异步事件传播的客户服务流程。 我们将如何测试呢? 让我们通过剖析下面的代码片段找出解决此问题的一种可能方法。
@RunWith(Arquillian.class)
public class NotificationServiceTest {
@Inject private RegistrationService registrationService;
@Inject private TestNotificationService notificationService;
@Deployment
public static JavaArchive createArchive() {
return ShrinkWrap
.create(JavaArchive.class)
.addClasses(TestNotificationService.class, StubCustomerRepository.class)
.addClasses(ConversionService.class, TransactionalRegistrationService.class, RegistrationEventObserver.class)
.addPackages(true, "org.apache.deltaspike.core")
.addPackages(true, "com.javacodegeeks.rentals.customer.conversion")
.addPackages(true, "com.javacodegeeks.rentals.customer.registration.conversion");
}
@Test
public void testCustomerRegistrationEventIsFired() {
final UUID uuid = UUID.randomUUID();
final Customer customer = registrationService.register(uuid, new RegisterCustomer());
await()
.atMost(1, TimeUnit.SECONDS)
.until(() -> !notificationService.getTemplates().isEmpty());
assertThat(notificationService.getTemplates())
.hasSize(1)
.hasOnlyElementsOfType(RegistrationTemplate.class)
.extracting("customerId")
.containsOnly(customer.getUuid());
}
}
由于该事件是异步触发和使用的,因此我们无法预测性地断言,而是使用Awaitility库考虑了时序方面。 另外,我们实际上不需要在此测试套件中包含持久层,因此我们提供了自己的(相当愚蠢的) StubCustomerRepository
实现以加快测试执行速度。
@Singleton
public static class StubCustomerRepository implements CustomerRepository {
@Override
public Optional<CustomerEntity> findById(UUID uuid) {
return Optional.empty();
}
@Override
public CustomerEntity saveOrUpdate(CustomerEntity entity) {
return entity;
}
@Override
public boolean deleteById(UUID uuid) {
return false;
}
}
即使采用这种方法,仍然存在不稳定的机会。 专用的测试调度程序和执行程序可能会产生更好的结果,但并非每个框架都提供它们或支持将其插入的简便方法。
5.测试计划任务
从测试的角度来看,应该在特定时间完成(或计划)的工作提出了一个有趣的挑战。 我们如何确保时间表符合期望? 让测试套件运行数小时或数天以等待任务被触发是不切实际的(但不管您相信与否,这都是现实的)。 幸运的是,几乎没有其他选择。 对于使用Spring Framework的应用程序和服务,例如,最简单但相当可靠的方法是使用CronTrigger
和模拟(或存根) TriggerContext
。
class SchedulingTest {
private final class TestTriggerContext implements TriggerContext {
private final Date lastExecutionTime;
private TestTriggerContext(LocalDateTime lastExecutionTime) {
this.lastExecutionTime = Date.from(lastExecutionTime.atZone(ZoneId.systemDefault()).toInstant());
}
@Override
public Date lastScheduledExecutionTime() {
return lastExecutionTime;
}
@Override
public Date lastActualExecutionTime() {
return lastExecutionTime;
}
@Override
public Date lastCompletionTime() {
return lastExecutionTime;
}
}
@Test
public void testScheduling(){
final CronTrigger trigger = new CronTrigger("0 */30 * * * *");
final LocalDateTime lastExecutionTime = LocalDateTime.of(2019, 01, 01, 10, 00, 00);
final Date nextExecutionTime = trigger.nextExecutionTime(new TestTriggerContext(lastExecutionTime));
assertThat(nextExecutionTime)
.hasYear(2019)
.hasMonth(01)
.hasDayOfMonth(01)
.hasHourOfDay(10)
.hasMinute(30)
.hasSecond(0);
}
}
上面的测试用例使用固定的CronTrigger
表达式并验证下一次执行时间,但也可以从属性甚至类方法注释中填充它。
除了验证时间表本身之外,您可能会发现依靠虚拟时钟非常有用,而且实际上是“实时旅行”。 例如,您可以传递Clock
抽象类的实例( Java Standard Library的一部分),并在测试中将其替换为stub或模拟。
6.测试反应流
反应式编程范式的流行对我们过去采用的测试方法产生了深远的影响。 实际上,在任何反应式框架中,测试支持都是一流的公民: RxJava , Project Reactor或Akka Streams ,您都可以命名。
我们的预订服务是完全使用Spring Reactive堆栈构建的,非常适合用来说明如何使用专用脚手架来测试React API 。
@Testcontainers
@SpringBootTest(
classes = ReservationRepositoryIT.Config.class,
webEnvironment = WebEnvironment.NONE
)
public class ReservationRepositoryIT {
@Container
private static final GenericContainer<?> container = new GenericContainer<>("cassandra:3.11.3")
.withTmpFs(Collections.singletonMap("/var/lib/cassandra", "rw"))
.withExposedPorts(9042)
.withStartupTimeout(Duration.ofMinutes(2));
@Configuration
@EnableReactiveCassandraRepositories
@ImportAutoConfiguration(CassandraMigrationAutoConfiguration.class)
@Import(CassandraConfiguration.class)
static class Config {
}
@Autowired
private ReservationRepository repository;
@Test
@DisplayName("Should insert Customer reservations")
public void shouldInsertCustomerReservations() {
final UUID customerId = randomUUID();
final Flux<ReservationEntity> reservations =
repository
.deleteAll()
.thenMany(
repository.saveAll(
Flux.just(
new ReservationEntity(randomUUID(), randomUUID())
.withCustomerId(customerId),
new ReservationEntity(randomUUID(), randomUUID())
.withCustomerId(customerId))));
StepVerifier
.create(reservations)
.expectNextCount(2)
.verifyComplete();
}
}
除了利用Spring Boot测试支持之外 ,该测试套件还以StepVerifier
的形式依赖于出色的Spring Reactor测试功能 ,其中期望是根据每个步骤的期望事件来定义的。 StepVerifier
及其系列提供的功能足以覆盖任意复杂的场景。
这里还要提到的另一件事是使用Testcontainers框架并引导专用数据存储实例(在本例中为Apache Cassandra )以实现持久性。 这样,不仅测试了无功流量 , 集成测试还使用了实际组件,并尽可能接近实际生产条件。 这样做的代价是更高的资源需求和显着增加的测试套件执行时间。
7.合同测试
在松散耦合的微服务体系结构中 ,合同是每个服务发布和使用的唯一内容。 合同可以用协议缓冲区或Apache Thrift之类的IDL表示,这使得通信,发展和使用相对容易。 但是对于基于HTTP的RESTful Web API,它更有可能是某种形式的蓝图或规范。 在这种情况下,问题就变成了:消费者如何对此类合同主张期望? 更重要的是,提供者如何在不破坏现有消费者的情况下发展合同?
这些都是棘手的问题,其中消费者驱动的合同测试可能会非常有帮助。 这个想法很简单。 提供者发布合同。 消费者创建测试以确保对合同有正确的解释。 有趣的是,消费者可能不需要使用所有API,而只需使用它真正需要完成的子集即可。 最后,消费者将这些测试传达给提供商。 最后一步非常重要,因为它可以帮助提供程序在不中断使用者的情况下改进API。
在JVM生态系统中, Pact JVM和Spring Cloud Contract是两个用于消费者驱动的合同测试的最受欢迎的库。 让我们看一下JCG租车公司客户管理门户网站如何使用Pact JVM通过发布的OpenAPI规范为其中一种客户服务 API添加消费者驱动的合同测试。
public class RegistrationApiContractTest {
private static final String PROVIDER_ID = "Customer Service";
private static final String CONSUMER_ID = "JCG Car Rentals Admin";
@Rule
public ValidatedPactProviderRule provider = new ValidatedPactProviderRule(getContract(), null, PROVIDER_ID,
"localhost", randomPort(), this);
private String getContract() {
return getClass().getResource("/contract/openapi.json").toExternalForm();
}
@Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)
public RequestResponsePact registerCustomer(PactDslWithProvider builder) {
return builder
.uponReceiving("registration request")
.method("POST")
.path("/customers")
.body(
new PactDslJsonBody()
.stringType("email")
.stringType("firstName")
.stringType("lastName")
.object("homeAddress")
.stringType("streetLine1")
.stringType("city")
.stringType("postalCode")
.stringType("stateOrProvince")
.stringType("country")
.closeObject()
)
.willRespondWith()
.status(201)
.matchHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.body(
new PactDslJsonBody()
.uuid("id")
.stringType("email")
.stringType("firstName")
.stringType("lastName")
.object("homeAddress")
.stringType("streetLine1")
.stringType("city")
.stringType("postalCode")
.stringType("stateOrProvince")
.stringType("country")
.closeObject())
.toPact();
}
@Test
@PactVerification(value = PROVIDER_ID, fragment = "registerCustomer")
public void testRegisterCustomer() {
given()
.contentType(ContentType.JSON)
.body(Json
.createObjectBuilder()
.add("email", "john@smith.com")
.add("firstName", "John")
.add("lastName", "Smith")
.add("homeAddress", Json
.createObjectBuilder()
.add("streetLine1", "7393 Plymouth Lane")
.add("city", "Springfield")
.add("postalCode", "19064")
.add("stateOrProvince", "PA")
.add("country", "United States of America"))
.build())
.post(provider.getConfig().url() + "/customers");
}
}
编写消费者驱动的合同测试的方法有很多,上面只是其中之一。 遵循哪种方法都没关系, 微服务架构的质量将得到提高 。
进一步推动它,诸如swagger-diff , Swagger Brake和assertj-swagger之类的工具对于验证合同中的更改非常有用(因为在大多数情况下它是活的东西),并确保服务正确地执行了它声称的合同至。
如果这还不够的话, Twitter上的 Diffy就是其中一种无价的工具,它可以并排运行新版本和旧版本的实例来帮助发现服务中的潜在错误。 它的行为更像是代理,它将接收到的所有请求路由到每个正在运行的实例,然后比较响应。
8.组件测试
组件测试位于单个微服务的测试金字塔的顶部。 从本质上讲,它们仅使用存根(或模拟)的外部服务来执行真实的,理想的类似于生产的部署。
让我们回到预订服务并逐步进行我们可能提出的组件测试。 由于它依赖于库存服务 ,因此我们需要模拟此外部依赖关系。 为此,我们可以受益于Spring Cloud Contract WireMock扩展,顾名思义,该扩展基于WireMock 。 除了库存服务,我们还使用@MockBean
批注模拟安全提供程序。
@AutoConfigureWireMock(port = 0)
@Testcontainers
@SpringBootTest(
webEnvironment = WebEnvironment.RANDOM_PORT,
properties = {
"services.inventory.url=http://localhost:${wiremock.server.port}"
}
)
class ReservationServiceIT {
private final String username = "ac2a4b5d-a35f-408e-a652-47aa8bf66bc5";
private final UUID vehicleId = UUID.fromString("4091ffa2-02fa-4f09-8107-47d0187f9e33");
private final UUID customerId = UUID.fromString(username);
@Autowired private ObjectMapper objectMapper;
@Autowired private ApplicationContext context;
@MockBean private ReactiveJwtDecoder reactiveJwtDecoder;
private WebTestClient webClient;
@Container
private static final GenericContainer<?> container = new GenericContainer<>("cassandra:3.11.3")
.withTmpFs(Collections.singletonMap("/var/lib/cassandra", "rw"))
.withExposedPorts(9042)
.withStartupTimeout(Duration.ofMinutes(2));
@BeforeEach
public void setup() {
webClient = WebTestClient
.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.build();
}
@Test
@DisplayName("Should create Customer reservations")
public void shouldCreateCustomerReservation() throws JsonProcessingException {
final OffsetDateTime reserveFrom = OffsetDateTime.now().plusDays(1);
final OffsetDateTime reserveTo = reserveFrom.plusDays(2);
stubFor(get(urlEqualTo("/" + vehicleId + "/availability"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody(objectMapper.writeValueAsString(new Availability(vehicleId, true)))));
webClient
.mutateWith(mockUser(username).roles("CUSTOMER"))
.mutateWith(csrf())
.post()
.uri("/api/reservations")
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters
.fromObject(new CreateReservation()
.withVehicleId(vehicleId)
.withFrom(reserveFrom)
.withTo(reserveTo)))
.exchange()
.expectStatus().isCreated()
.expectBody(Reservation.class)
.value(r -> {
assertThat(r)
.extracting(Reservation::getCustomerId, Reservation::getVehicleId)
.containsOnly(vehicleId, customerId);
});
}
}
尽管实际上发生了很多事情,但是测试用例看起来仍然很容易管理,但现在运行所需的时间接近50秒。
在设计组件测试时,请记住,不应采取任何捷径(例如,直接更改数据库中的数据)。 如果需要一些先决条件或断言内部服务状态的方式,请考虑引入仅在测试时可用的支持API(例如,使用配置文件或配置属性启用)。
9.端到端测试
端到端测试的目的是验证整个系统是否按预期工作,因此,假设是对所有组件进行全面部署。 尽管非常重要,但是端到端测试是最复杂,最昂贵,最慢的,并且正如实践所示,是最脆弱的测试。
通常, 端到端测试是在用户从头到尾执行工作流之后设计的。 因此,通常进入系统的入口是某种移动或Web前端,因此诸如Geb , Selenium和Robot Framework的测试框架在这里是非常受欢迎的选择。
10.故障注入与混沌工程
可以公平地说,大多数测试都偏向“幸福的道路”,并且不会探索错误的场景,除非那些琐碎的场景(例如,数据存储中不存在记录或输入无效)。 您有多少次看到故意引入数据库连接问题的测试套件?
正如我们在本教程的上半部分所述 ,坏事会发生,最好做好准备。 混沌工程学催生了许多不同的库,框架和工具包,用于执行故障注入和仿真。
要制造其他类型的网络问题,您可以从Blockade , Saboteur或Comcast入手,所有这些都专注于网络故障和分区注入,目的是简化弹性和稳定性测试。
Chaos Toolkit是进行混沌实验的更高级,更规范的方法。 它还可以与大多数流行的业务流程引擎和云提供商很好地集成。 同样,来自Netflix的SimianArmy是最早的(如果不是第一个)面向云的工具之一,用于产生各种类型的故障并检测异常情况。 对于在Spring Boot堆栈之上构建的服务,您可能已经听说过一个专门的项目,称为Spring Boot的Chaos Monkey 。 它虽然很年轻,但是发展很快,非常有前途。
对于大多数组织来说,这种测试是很新的,但是在微服务架构的背景下,绝对值得考虑和投资。 这些测试使您有信心,该系统能够通过逐渐降低其功能而不是着火和燃烧而幸免于故障。 许多组织(例如Netflix )会定期在生产中进行混乱的实验,主动发现问题并加以解决。
11.结论
在本教程的这一部分中,我们专注于测试。 由于存在许多不同类型的测试,因此我们的研究范围还远远不够详尽。 在许多方面,测试单个微服务没有太大差异,适用相同的良好做法。 但是这种架构的分布式特性带来了许多独特的挑战, 合同测试以及故障注入和混沌工程正在设法解决这些挑战。
最后, 测试微服务,理智的方法和生产中的测试,安全的方法这一系列文章是对有效方法以及如何在测试微服务时避免常见陷阱的深刻见解和建议的绝妙来源。
12.接下来是什么
在本教程的下一部分中,我们将继续测试主题,并讨论性能(负载和压力)测试。
翻译自: https://www.javacodegeeks.com/2019/01/microservices-for-java-developers-testing.html