在企业测试中,测试软件的方式应该与软件在生产环境中运行的方式相同,以便确保软件能够按预期的方式运行。常见的挑战是微服务应用程序直接或间接依赖需要在测试场景中编排的其他服务。
本文展示了容器编排如何在服务实例之上提供抽象,并使用桩实例来替代真实实例。此外,服务网格让我们能够重新路由流量,并通过注入错误响应或延迟来验证服务的弹性。
我们将使用一个 咖啡店示例应用程序 ,这个应用程序被部署在一个容器和服务网格集群中。我们选择Kubernetes和Istio作为实例运行环境。
假设我们想要在不考虑其他外部服务的情况下测试应用程序的行为。应用程序的运行方式和配置方式应该与生产环境相同,以便确保以后它在生产环境中的行为是一致的。在测试中,我们将使用定义好的通信接口连接应用程序。
但是,外部服务不应成为测试场景的一部分。通常,在测试时我们应该关注被测试的对象,并忽略掉其他对象。因此,我们使用模拟服务器来替代外部服务。
使用模拟服务器而不是真实实例与以与生产环境相同的方式运行微服务的想法相矛盾,因为到了生产环境配置会发生改变。但是,如果我们的应用程序部署到容器编排集群(例如Kubernetes),就可以将抽象的服务名称用作配置,并让集群自己去解析后端服务实例。
以下示例是一个网关类,它是咖啡店应用程序的一部分,连接到端口8080上的coffee-processor。
public class OrderProcessor { // definitions omitted ... @PostConstruct private void initClient() { final Client client = ClientBuilder.newClient(); target = client.target("http://coffee-processor:8080/processes"); } @Transactional(Transactional.TxType.REQUIRES_NEW) public void processOrder(Order order) { OrderStatus status = retrieveOrderStatus(order); order.setStatus(status); entityManager.merge(order); } // ... private JsonObject sendRequest(final JsonObject requestBody) { Response response = target.request() .buildPost(Entity.json(requestBody)) .invoke(); // ... return response.readEntity(JsonObject.class); } // definitions omitted ... }
主机名通过Kubernetes群集DNS解析,将流量引导到其中一个正在运行的处理器实例。然而,coffee-processor的实例将成为一个模拟服务器,在我们的示例中使用了 WireMock 。这种替换对我们的应用来说是透明的。
在测试场景中,不仅会连接到应用程序来调用业务逻辑,还会与模拟服务器发生通信,在单独的管理界面上控制响应行为,并验证应用程序是否以正确的方式调用模拟服务器。这与类级别的单元测试类似,通常使用JUnit和Mockito实现。
上述的设置可以让我们模拟和控制在容器编排集群内运行的服务。那么那些在集群之外的外部服务该怎么办呢?
通常,我们可以创建一个 不带有选择器的Kubernetes服务 ,让它指向一个外部IP,并重写我们的应用程序,让它始终使用由群集解析的服务名。这样一来,我们定义了一个单一的点,服务将被路由到这个点上。
以下的代码片段显示了一个外部Kubernetes服务和端点定义,它将coffee-shop-db路由到外部IP地址1.2.3.4:
kind: Service apiVersion: v1 metadata: name: coffee-shop-db spec: ports: - protocol: TCP port: 5432 --- kind: Endpoints apiVersion: v1 metadata: name: coffee-shop-db subsets: - addresses: - ip: 1.2.3.4 ports: - port: 5432
在不同的环境中,服务可能会被路由到不同的数据库实例。
服务网格能够帮助我们处理微服务间的通信问题。目前,Istio是最常用的服务网格技术之一。它增加了与应用程序容器共存的边车代理容器,可以解决微服务间的通信问题,并且还可以用来操纵或减慢连接,以便进行弹性测试。
在端到端测试中,我们可以引入错误或缓慢的响应来验证应用程序是否能够正确处理这些问题场景。
以下的代码片段显示了一个Istio虚拟服务的定义,其中到coffee-processor的路由有50%的延迟为3秒,10%的响应是失败的。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: coffee-processor spec: hosts: - coffee-processor http: - route: - destination: host: coffee-processor subset: v1 fault: delay: fixedDelay: 3s percent: 50 abort: httpStatus: 500 percent: 10 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: coffee-processor spec: host: coffee-processor subsets: - name: v1 labels: version: v1
现在,我们可以运行其他测试,并验证应用程序将如何处理这些增加的响应时间和故障状况。
除了可以注入错误响应之外,服务网格技术还可以用来从环境中添加弹性。代理容器可以处理超时,实现断路器和隔板,而应用程序无需关心这些问题。
容器编排和服务网格通过将应用程序的关注点转移到环境中来提高微服务应用程序的可测试性。有了服务抽象,我们就可以透明地替换服务或进行重新路由。服务网格不仅支持更复杂的路由,还允许我们注入故障或减慢响应,让应用程序处于压力之下,以此来验证其相应的行为。
Sebastian Daschner 是一名自由Java顾问、作家和培训师,对编程和Java充满热情。他是“Architecting Modern Java EE Applications”一书的作者。Sebastian正在参与JCP,协助制定Java EE的未来标准,是JAX-RS、JSON-P和Config专家组的成员,并在各种开源项目上进行协作。由于在Java社区和生态系统中的贡献,他获得了Java Champion、Oracle Developer Champion和2016年JavaOne Rockstar的殊荣。除了Java之外,Sebastian还是Linux和云原生技术的重要用户。他通过 博客 和Twitter(@DaschnerS)传播计算机科学实践。工作之余,他喜欢乘飞机或骑摩托车旅行。
查看英文原文: Improving Testability of Java Microservices with Container Orchestration and a Service Mesh