2024年软件测试最全怎么针对微服务架构做单元测试?_微服务单元测试(1),2024软件测试面试笔试总结

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

无法通过测试的代码不应该被合并到代码仓库里;

无法通过测试的代码不应该被发布出去。

不能为了测试而测试,测试的真正目的是为了交付高质量的软件给用户,而不是把资源浪费在没有实际意义的测试用例上。所有的测试层次、流程和用例,都应该有的放矢。

传统测试方法面临的挑战

以一个常见的开发团队为例,在采用了微服务架构之后,很可能同时会开发多个模块(即微服务),每个微服务有不同的客户要求、开发周期、开发进度和交付期限,但是整个团队又必须保证能够在固定的时间节点(譬如每月一次、每两周一次,甚至每天一次或者多次),持续地、稳定地为用户提供可以部署、使用的产品。这意味着,过去那种先等产品经理、业务部门提供需求,开发人员再进行开发,最后交给测试人员执行集成测试、端到端测试的方法,已经无法提供足够的测试粒度和足够快的响应速度。

归结起来,与基于单体式架构的传统测试方法相比,微服务架构对测试提出了以下挑战:

服务/模块/层次(layer)之间存在复杂的依赖性。

在单体式架构中,通常使用集成测试来验证依赖是否正常。而在微服务架构中,服务数量往往很多,每个服务都是独立的业务单元,服务之间主要通过接口进行交互,如何保证这些依赖的正常,是测试人员面临的主要挑战。这意味着,如果想单独测试某一个服务,或者服务中的某个模块,就必须剥离它们对于其他环节的依赖关系。这需要通过 Mock、Stub 等方法来实现。

不同的服务可能会在不同的环境/设置下运行。

特别是一些后端服务,与前端服务的运行环境可能截然不同。这时在考虑对每种服务设立自动化管线时,就必须有针对性的设置相应的环境配置。而且,在微服务架构中,每个服务都独立部署,交付周期短且频率高,人工部署已经无法适应业务的快速变化。因此如何有效地构建自动化部署体系,保证配置的稳定性、可重复性,是微服务测试面临的另一个挑战,必须与 DevOps 人员一同解决。

涉及多个服务的 UI 端到端测试(End-to-End 测试,简称 E2E 测试)非常容易出错。

因为每种服务的开发进度不同,集成不同服务的端到端测试往往会因为某一个服务的微小改动而出错。这种出错是测试人员希望避免的干扰信息。这意味着,对端到端测试的设计,必须采取一定的防干扰、防误报策略。

测试结果可能取决于网络的稳定性。

微服务架构是基于分布式的系统,而构建分布式系统必然会带来额外的开销。

性能: 分布式系统是跨进程、跨网络的调用,受网络延迟和带宽的影响。

可靠性: 由于高度依赖于网络状况,任何一次的远程调用都有可能失败,随着服务的增多还会出现更多的潜在故障点。因此,如何提高系统的可靠性、降低因网络引起的故障率,是系统构建的一大挑战。

异步: 异步通信大大增加了功能实现的复杂度,并且伴随着定位难、调试难等问题。

数据一致性: 要保证分布式系统的数据强一致性,成本是非常高的,需要在 C(一致性)A(可用性)P(分区容错性)三者之间做出权衡。

特别是涉及到数据存储和外部通信的部分,如果在测试中不摆脱这些因素的影响,就可能会得到一些随机性的误报,干扰测试结果。

故障分析的复杂度会随着服务的增加而提高。

微服务架构中,因为每个服务都需要独立地配置、部署、监控和收集日志,因此在发现问题之后,进行诊断分析时,搜集缺陷信息的成本呈指数级增长。

与交付周期不同的开发团队之间的交流成本。

这一点虽然跟技术无关,但是实际上会对测试人员的工作造成很大的困扰。因为开发模式分解为负责不同服务的多个小组,测试人员往往每天要花费大量的时间,了解不同团队的开发进度。如果还需要手动进行回归测试(Regression Test),最终将会不堪重负。所以自动化测试是必须采取的手段和方向。

如何应对这些挑战,我总结了下面这三个原则:

1.自动化:测试任务的增加,要求测试人员必须把主要的精力用于将测试自动化,摆脱手动测试带来的沉重负担。当然,自动化测试必须足够稳定、稳健,不能动辄误报,否则反而会导致很高的维护成本。

2.层次化:这意味着采用分层次的测试方法,粒度由细到粗,范围由小到大。下图说明了几个主要层次之间的关系:

这就是 Mike Cohn 提出的测试金字塔(Test Pyramid),其中最重要的两个原则是:

应该用不同的粒度来测试应用程序;

层次越高,测试越少。

最底层的是单元测试(Unit Test),粒度最细,速度最快,维护成本也最低。往上是针对每种服务内部的各种模块、业务流程的测试。最上面是基于前端 UI 的测试,这部分的粒度最粗,范围最大(因为会覆盖大多数服务),但是维护成本最高,因为稍微有些细微的变化就可能需要调整脚本。而且,由于基于前端,需要设置很多响应时间和等待时间,所以速度最慢。

Mike Cohn 是 Scrum 软件开发方法的提出者之一,也是 Scrum 联盟的创始成员。他目前是 Mountain Goat Software 公司的所有者,致力于提供关于 Scrum 和 Agile 软件开发技术的培训

3.可视化:为了降低交流成本,最好的办法就是让所有的测试结果可视化。这意味着将构建(Build)、测试(Test)、部署(Deploy)所有这些相关任务构建在一个流水线之中,让所有团队成员都可以随时监控项目进度,找到阻碍项目的瓶颈。

以下面这个典型团队为例,整个从开发、测试、构建到部署的一系列过程,都可以借助 Jenkins 或者 TeamCity 这样的任务调度工具,完全可视化,再借助 SonarQube 这样的代码质量监控工具监控测试结果。Google Analytics 或者 Microsoft 的 Azure ApplicationInsight 等云端监控工具,则可以提供实时生产环境的客户使用信息或者测试数据,让整个团队可以随时把握产品的整个流水线的运行状态。

在微服务架构中所采用的主要测试方法。如下图所示,它们主要包括:

单元测试(Unit Test)

用于验证微服务内部的类方法或函数的行为。它们会根据测试框架,执行代码文件里的类方法或函数,提供不同的输入,并验证与每一个输入相对应的输出。

集成测试(Integration Test)

用于验证微服务与外部模块的通信或者交互行为。测试框架会启动服务的一个实例,并调用服务的外部接口来执行业务逻辑。

组件测试 (Component Test)

即验证微服务能否起到预期的作用。这需要把微服务周边依赖的所有其他服务或者资源全部模拟化,从该服务外部“用户”的角度来检查服务能否提供预期的输出。

端到端测试(End-to-end Test)

验证整个系统的功能能否符合用户的预期,一般是从 UI 层面进行测试,确保用户体验完全达到客户要求。

探索测试( Exploratory Test,即手动测试)

这一步通常由业务专家型用户执行,具体查看某个新添加的特性是否开发、部署成功。

总结

简单总结一下所学习的内容:

微服务架构对软件测试提出了很多全新的挑战。

应对这些挑战的方法包括:

自动化

层次化

可视化

怎么针对微服务架构做单元测试?

单元测试是开发人员编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序 list 中去,然后确认该值出现在 list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。

对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如 C 语言中单元指一个函数,Java 里单元指一个类,前端应用中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。

我们将探讨在微服务架构下,单元测试的设计、实现和质量控制。

设计:定义测试边界

要设计高效率(既运行快速又覆盖率高)的单元测试,首要要准确地定义测试边界。测试的目的就是为了验证边界里“黑盒”的行为是否符合预期,我们向黑盒输入数据,然后验证输出的正确性。在单元测试里,黑盒指的是函数或者类的方法,目的是单独测试特定代码块的行为。

但是在微服务架构中,很多时候黑盒的输出需要依赖于其他的功能或者服务,即存在外部依赖。为了更好地理解这个概念,我们以一个简单的注册功能为例:

从图中可以看出,这个函数包含了一些输入和输出。输入参数包括基本的用户注册信息(姓名、用户名和密码),而返回新创建的用户 ID。

但是在此过程中,还有一些不是很明显的输入数据。这个函数调用了两个外部函数:db.user.inser() 是向数据库插入数据;Password.hashAndsave() 是一个微服务,用于生成密码的哈希值,再加以保存。在某些情况下,数据库可能会返回错误,比如用户名已经存在,导致数据库插入失败。另外,因为需要调用外部的微服务生成密码哈希值,如果网络连接出现问题,或者哈希值生成服务由于发生过载而导致服务超时,那么密码保存就会返回错误。User.create() 函数必须能够妥善地处理这两种错误,这是测试的重点。

也就是说,为了全面地测试用户注册功能,单元测试所要做的不仅仅是简单地输入各种不同的参数,它还要能够让外部函数/微服务,能够产生出指定的错误,再验证函数的错误处理逻辑是否符合预期。

因此,为了在不依赖于外部条件的情况下制造出各种输入数据,就需要使用 Stub 或者 Mock,中文可以理解为对函数外部依赖的模拟器。简而言之,它意味着用一个假的版本替换了真实的对象(例如一个类、模块、函数或者微服务)。假的版本的行为特征和真实对象非常类似,采用相同的调用方法,并按照你在测试开始之前预定义的返回方式,提供返回数据。测试框架在运行被测试的函数时,可以把对外部依赖函数/服务的调用,重定向到 Stub 上,这样单元测试就可以在没有外部服务的情况下进行,即保证了速度,又避免了网络条件的影响。

这里再强调下 Stub 和 Mock 的区别,很多人经常搞混。Stub 就是一个纯粹的模拟器,用于替代真实的服务/函数,收到请求返回指定结果,不会记录任何信息。Mock 则更进一步,还会记录调用行为,可以根据行为来验证系统的正确性。

创建 Stub 的工具有很多,包括 Node.js/JavaScript 框架下的 sinon.js, testdouble.js 等;Python 下的 mock 等。

在刚刚提到的注册函数和密码哈希值生成、保存服务之间,插入一个 Stub(模拟器)的示意图如下:

我们可以使用模拟器来达到各种目的:

模拟器可返回任意的设定值,用于模拟外部函数的输出。这在测试罕见的边界情况时会非常有用,比如有些错误场景可能很少发生或者非常难以重现。

模拟器也可以捕捉被测试函数传给外部函数的参数,或者把这些参数记录下来。这样就可以验证被测试函数需要调用哪些外部函数,以及需要传给外部函数哪些参数。

通过对外部依赖函数使用模拟器,通常可以在几秒钟内,执行数千个单元测试。这样,开发人员就可以把单元测试加入到日常的开发工作管线(Pipeline)当中,包括直接集成到常用的 IDE 里,或者通过终端命令行触发。通过在编写代码的同时,频繁运行单元测试,有助于尽早发现代码中的问题。对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。

顺便说一句,在微服务架构中,单元测试的作用不仅限于代码开发,它们还对 DevOps/CI(持续集成)有很大的帮助,可以集成到代码合并(Merge)流程里。

譬如,GitHub 支持对一些主流 CI 服务的状态检查。一般它会限制对“Master”主分支的提交权限,不允许开发人员直接向该分支提交代码,而是要求他们把代码先提交到其他分支上(提交 Pull Request),再由其他开发人员进行代码审查(Code Review)。最后,在将代码合并到主分支的时候,GitHub 要求先通过状态检查。这时,Jenkins、CircleCI 和 TravisCI 等 CI 服务都提供了状态检查钩子(hook),它们会从分支上获取代码并运行单元测试。如果通过了,就允许合并代码,否则就不允许。整个过程如下图所示:

实现:单元测试的流程

单元测试的工具有很多,例如:

C++:Googletest、GMock

Java:Junit、TestNG、Mockito、PowerMock

JavaScript:Qunit、Jasmine

Python:unittest

Lua:luaunit

一个单元测试的实现主要分为以下几步:

设置测试数据;

在测试中调用你的方法;

判断返回的结果是否符合预期。

这三步可以简化为“三 A 原则”: Arrange(设置)、Act (调用)、Assert(检查)。

或者也可以借用 BDD(行为驱动测试)的概念,把单元测试的流程分为三步:Given(上下文)、When (事件)、Then(结果)。

下面我们来看一个真实的例子,这是一个名为 ExampleController 的类,用于在人名库(PersonRepository)中查找人名。

@RestControllerpublic class ExampleController { private final PersonRepository personRepo; @Autowired public ExampleController(final PersonRepository personRepo) { this.personRepo = personRepo; } @GetMapping(“/hello/{lastName}”) public String hello(@PathVariable final String lastName) { Optional foundPerson = personRepo.findByLastName(lastName); return foundPerson .map(person -> String.format(“Hello %s %s!”, person.getFirstName(), person.getLastName())) .orElse(String.format(“Who is this ‘%s’ youre talking about?”, lastName)); }}

下面,我们将用 Junit,对类中的 hello(lastname)方法进行单元测试。

JUnit 是 Java 社区中知名度最高的单元测试工具,用于编写和运行可重复的测试用例。JUnit 设计得非常小巧,但是功能却非常强大。它诞生于 1997 年,由 Erich Gamma 和 Kent Beck 共同开发完成。其中 Erich Gamma 是经典著作《设计模式:可复用面向对象软件的基础》一书的作者之一,并在 Eclipse 中有很大的贡献;Kent Beck 则是一位极限编程(XP)方面的专家和先驱。

public class ExampleControllerTest { private ExampleController subject; @Mock // 模拟器 private PersonRepository personRepo; @Before // 在每个测试方法之前执行 public void setUp() throws Exception { initMocks(this); subject = new ExampleController(personRepo); } @Test // 测试用例1 public void shouldReturnFullNameOfAPerson() throws Exception { Person peter = new Person(“东”, “王”); given(personRepo.findByLastName(“王”)) .willReturn(Optional.of(东)); String greeting = subject.hello(“王”); assertThat(greeting, is(“你好王东!”)); } @Test // 测试用例2 public void shouldTellIfPersonIsUnknown() throws Exception { given(personRepo.findByLastName(anyString())) .willReturn(Optional.empty()); String greeting = subject.hello(“王”); assertThat(greeting, is(“这位王先生是谁?”)); }}

Arrange(设置)、Act (调用)、Assert(检查)。

可以看到,首先我们用一个 Stub(模拟器),替换真正的 PersonRepository 类,这样我们可以预先定义我们希望返回的值。

记下来,我们按照 3A 原则,编写了两个单元测试。第一个是正常运行的用例:

Arrange(设置):建立一个名为王东的人物,并且让模拟器准备好,在输入参数为王时,返回“王东”。

Act(调用):调用函数 hello(“王”)。

Assert(检查):检查返回结果是否为"你好王东!"。

第二是异常运行的测试用例:

Arrange(设置):让模拟器准备好,在输入任何参数时,均返回空值。

Act(调用):调用函数 hello(“王”)。

Assert(检查):因为模拟器返回的是空值,这是检查返回结果是否为"这位王先生是谁?"

通过这样的正面和反面的测试用例,我们可以彻底地检查 hello(lastname) 方法是否工作正常。

质量控制:监控测试覆盖率

着重需要提及的一点是,测试人员应当设法将单元测试的覆盖率作为一个重要的监控指标,记录并可视化。例如,Teamcity 或者 Jenkins 这样的流程化工具,支持用 dotCover 来统计流程中单元测试的覆盖率,并将结果以 TXT 报告或者 HTML 的方式显示在任务页面上。进一步也可以将覆盖率、测试结果的数据,自动输出到 SonarQube 这样的代码质量监控工具之中,以便随时检查出测试没有通过或者测试覆盖率不符合预期的情况。

高覆盖率的单元测试是保障代码质量的第一道也是最重要的关口。从分工上来说,测试人员可能不会参与单元测试的开发与维护,但是测试人员应当协助开发人员确保单元测试的部署和覆盖率,这是确保后续一系列测试手段发挥作用的前提。

总结

简单总结一下所学习的内容:

用模拟器来定义单元测试的边界,模拟对外界函数/服务的调用;

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

模拟对外界函数/服务的调用;

[外链图片转存中…(img-uhU131f5-1715366070882)]
[外链图片转存中…(img-ojtsZdEH-1715366070882)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值