标签(空格分隔): 架构
单体架构的好处与弊端
微服务架构的好处与弊端
三层架构与六边形架构对比
拆分单体应用为服务的难点
模式语言概述
共享库的角色:消除重复代码,但如果更改会影响使用共享库的服务。应该把可能会更改的通用功能作为服务来实现,而不是共享库。使用共享库来实现不太可能改变的功能。例如Money类。
这是因为如果您想要快速而可靠地交付软件,进行自动化测试是绝对必要的。
这是缩短交付时间(即将提交的代码投入生产所需的时间)的唯一方法。也许更重要的是,自动化测试是必不可少的,因为它迫使您开发可测试的应用程序。通常很难将自动化测试引入一个已经很大的、复杂的应用程序中。换句话说,通往地狱的捷径就是不编写自动化测试。
- 单元测试:测试服务的一小部分,比如类。
- 集成测试:验证服务是否可以正常与基础设施服务(数据库等)和其他服务交互
- 组件tests:单个服务的验收测试
- 端到端tests:测试整个应用程序。
它们的主要区别在于范围。在一端的极端是单元测试,它验证最小的有意义的程序元素的行为。对于面向对象语言,如Java,这是一个类。另一端的极端是端到端测试,用于验证整个应用程序的行为。中间是组件测试,用于测试各个服务。正如您将在下一章看到的,集成测试的范围相对较小,但它们比纯单元测试更复杂。范围只是描述测试的一种方法。另一种方法是使用测试象限
测试是开发不可分割的一部分。现代开发工作流是编辑代码,然后运行测试。此外,如果您是一个测试驱动开发(TDD)从业者,您开发一个新特性或修复一个bug时,首先要编写一个失败的测试,然后编写代码使其通过。即使您不是TDD的追随者,修复错误的一个好方法是编写一个重现错误的测试,然后编写修复错误的代码。
作为此工作流的一部分运行的测试称为编译时测试。在现代IDE(如IntelliJ IDEA或Eclipse)中,通常不将编译代码作为单独的步骤。相反,您使用单个按键来编译代码并运行测试。为了保持在流中,这些测试需要快速执行—理想情况下,不超过几秒钟。
- 无论测试是面向业务的还是面向技术的——面向业务的测试使用领域专家的术语进行描述,而面向技术的测试使用开发人员和实现的术语进行描述。
- 是否测试的目标是支持编程或对应用程序开发人员使用测试,支持编程作为日常工作的一部分。对应用程序进行评价的测试旨在识别需要改进的地方。
测试象限按照两个维度对测试进行分类。第一个维度是测试是面向业务还是面向技术。第二个是测试的目的是面向编程还是面向应用程序。
我们必须编写不同类型的测试,以确保我们的应用程序能够正常工作。然而,挑战在于测试的执行时间和复杂性随其范围的增加而增加。而且,测试范围越大,活动部件越多,它就越不可靠。不可靠的测试几乎和没有测试一样糟糕,因为如果您不相信测试,您很可能会忽略失败。
一个极端是针对单个类的单元测试。它们执行速度快,易于编写,而且可靠。另一个极端是针对整个应用程序的端到端测试。它们往往很慢,难以编写,而且由于它们的复杂性,常常不可靠。因为我们没有无限的开发和测试预算,所以我们希望专注于编写小范围的测试,而不影响测试套件的有效性。
测试金字塔的关键思想是,当我们沿着金字塔向上移动时,我们应该编写越来越少的测试。我们应该编写大量的单元测试和少量的端到端测试。
如何测试单个的微服务,比如不依赖于任何其他服务的消费者服务,是很清晰的。但是,像订单服务这样依赖于许多其他服务的服务呢?我们如何才能确信应用程序作为一个整体是有效的呢?这是测试具有微服务体系结构的应用程序的关键挑战。测试的复杂性已经从单个服务转移到它们之间的交互。让我们看看如何解决这个问题。
作为服务的开发人员,您需要确信所使用的服务具有稳定的api。同样,您也不希望无意中对服务的API进行破坏更改。例如,如果您正在处理Order服务,您希望确保您的服务依赖项的开发人员,如消费者服务和 厨房服务,不要以与您的服务不兼容的方式更改他们的api。类似地,您必须确保不会以破坏API网关或订单历史服务的方式更改订单服务的API。
验证两个服务可以交互的一种方法是运行两个服务,调用触发通信的API,并验证它是否有预期的结果。这当然可以解决集成问题,但它基本上是端到端的。测试可能需要运行这些服务的许多其他传递依赖项。测试还可能需要调用复杂的高级功能(如业务逻辑),即使其目标是测试相对低级的IPC。
最好避免编写这样的端到端测试。无论如何,我们需要编写更快、更简单、更可靠的测试,在理想情况下独立地测试服务。解决方案是使用所谓的consumer-driven contract testing(消费者驱动的契约测试)。
一定要记住,契约测试不会彻底测试提供者的业务逻辑。那是单元测试的工作。
开发消费者的团队编写一个契约测试套件,并将其(例如,通过pull请求)添加到提供者的测试套件。调用Order Service的其他服务的开发人员也提供一个测试套件,如图9.7所示。
每个测试套件将测试与每个消费者相关的Order Service API的那些方面。例如,订单历史服务的测试套件验证该订单。
开发使用Order service API的服务的每个团队都贡献一个契约测试套件。测试套件验证API是否符合消费者的期望。
此测试套件以及其他团队提供的测试套件由Order Service的部署管道运行。
这些测试套件由Order Service的部署管道执行。如果消费者契约测试失败,则该失败将告诉生产者团队,他们对API进行了破坏性的更改。他们必须要么修复API,要么与消费者团队对话。
也就是服务提供方也就是生产者,会有很多调用服务的消费者。消费者需要编写自己所调用接口的相关测试套件,并通过相关步骤添加到提供者的测试套件上。让各个消费者的测试套件都在生产者的部署管道上运行。如果在pipeline上运行失败,也就表明对API进行了破坏性的更改。要么修改,要么找到消费者团队进行对话。
让人耳目一新的思路
单元测试,尽可能多,测试相关的业务逻辑,对于外部依赖,应该通过相关的mock(消费者驱动的契约测试(集成测试)、 依赖注入)。但不会验证与其他服务是否正确交互(例如不会验证是否持久化在MySQL),这是集成测试的工作。单元测试的几个思路:
- 对于没有依赖的,可以很简单编写用例。
- 对于需要外部服务的(发送消息代理、调用存储和消息传递),可以通过编写模拟与数据库和消息代理交互的类的测试。也就是拦截(依赖注入)掉交互类,并验证是否发送了正确的消息,并返回相应的伪造数据。
- 对于控制器相关的,可以通过框架自带的进行模拟的HTTP请求,例如Django自带的client,验证中间件相关逻辑。无需进行真正的网络调用。
集成测试:测试与其他服务的交互。第一个策略是测试每个服务的适配器。例如直接测试MySQL的相关Repository,看是否能正确持久化。另一个是是用契约,简化验证服务之间交互的目的(类似yapi、另起一个mock服务等)。
分两个类型,一个是持久层相关的基础设施,例如数据库。另一个是外部服务。持久层的集成测试,可以通过docker启动相关的基础设施,进行集成测试。另一种是一些服务调用相关的。通过消费者驱动的契约测试。
契约是验证两端的适配器。也就是契约侧重验证提供者API的参数定义是否符合消费者的期望。不是彻底验证业务逻辑。消费者,(验证的是网络请求类)调用契约生成的服务器以得到测试交互后返回的响应结果。提供者,契约则生成相关的请求,并验收提供者的返回是否服务契约要求(验证controller)。
思考:集成测试与单元测试的界限?例如上面所说的数据存储类,其实不就是该类的单元测试吗?只是因为是需要外部依赖设施,所以叫做集成测试?还是说该类的测试应该是理解为,测试其实就是一个调用方,进行该类服务的调用交互,所以叫做集成测试。对于服务请求的,单元测试是直接mock拦截,集成测试则是通过契约进行真实的调用交互验证(yapi的消费者验证?)
集成测试验证服务是否与其客户端和依赖关系进行通信。单元测试验证逻辑是否正确。
将服务视为一个黑盒,并通过调用API验证服务的行为。不需要启动所有依赖项,通过使用模拟行为的桩代替服务的依赖关系,甚至使用内存版本的基础设施服务。
每组测试都是描述一个场景,进行测试。Given、When、Then
进层外的组件测试(其实就是我现在编写的大部分测试):将服务打包为生产环境就绪的格式,并将其作为单独的进程运行。进程外组件测试使用真实的基础设施服务(数据库和消息代理: Mysql、Kafka、ES),但是对于应用程序服务的任何依赖项使用桩(mock服务)。好处提高测试覆盖率,因为内容更接近部署的内容。缺点就是编写起来更复杂、执行速度更慢了。
组件测试:测试一个服务,假设一些组合场景测试,更接近部署的内容。集成测试则可以是简化的单纯调用某个服务的交互验收。
思考:现在的mock服务,跟消费者驱动的契约的区别?mock这是简单的提供类似yapi的服务器,只是稍微更灵活丰富一些。缺点就是没有约定,维护成本高。契约则更规范更可维护。服务提供根据契约的改动所改变(类似yapi的协议维护),不仅仅如此,契约还提供了yapi没有的发起者的作用。
测试整个应用程序(网站),必须部署多个服务及支撑它的基础设施服务。端到端测试很慢。控制端到端的测试的数量。
一个好的策略是编写用户旅程测试:对应于用户使用系统的过程。编写一个完成所有三项测试的单个测试,而不是单独测试创建订单、修改订单和取消订单。这种方法显著减少数量和执行时间。
用户旅行测试。例如写可视化的文档,可以让测试写、产品写等。以验证是否符合产品的预期。 单个测试应该是通过金字塔底层的支撑完成。