当前位置: 首页 > 科技观察

十年架构师用尽心血,带你了解微服务如何进行单元测试、集成测试、系统测试?

时间:2023-03-14 12:09:31 科技观察

如何测试微服务对于测试工作,微服务架构给传统架构带来了更多的复杂性。一方面,随着微服务数量的增长,测试用例会不断增长;另一方面,由于微服务之间存在一定的依赖关系,在测试过程中如何处理这些依赖关系就变得异常重要。本节将从微服务架构的单元测试、集成测试和系统测试三个方面进行讨论。微服务的单元测试单元测试要求测试的范围仅限于服务,这样可以保证测试的隔离性,将测试的影响降到最低。TDD要求程序员在实际编码之前编写测试用例。当然,一开始,所有的测试用例都应该失败,然后写代码让这些测试用例一个一个通过。也就是说,编写足够多的测试用例以使测试失败,并编写足够多的代码使测试成功。这样,程序员编码的目的就会更加明确。当然,编写测试用例并不是TDD的全部。测试成功后,还需要及时重构成功的代码,消除代码的“恶臭”。一、为什么要重构代码所谓重构,简单来说就是在不改变代码外部行为的情况下,通过修改代码来完善程序的内部结构。重构的前提是代码的行为是正确的,也就是说代码功能已经过测试,测试通过了,这是重构的前提。只有正确的代码才有意义重构。那么,既然代码是正确的,为什么还要花时间改代码、重构代码呢?重构的原因是大多数程序员无法写出完美的代码。他们不能完全信任自己编写的代码,这就是为什么他们需要测试自己编写的代码,重构也是如此。总结起来,以下几个方面是软件需要重构的原因。?软件不一定从一开始就是正确的。有才华的程序员只有少数,大多数人难免会犯错,所以很多程序员都无法一次写出正确的代码,只能不断地测试和重构来完善代码。即使是像MartinFowler这样的高手也承认,他的编码水平和大多数人一样,需要测试和重构。?随着时间的推移,软件的行为变得难以理解。这种现象尤其集中在一些规模大、历史长、代码质量差的软件中。这些软件的实现要么脱离了最初的设计,要么乱七八糟让人看不懂,尤其是缺乏“活文档”的引导,这些代码最终会“烂掉”。·能运行的代码不一定是好代码。任何程序员都可以编写出计算机可以理解的代码,只有编写出人类可以轻松理解的代码的人才是优秀的程序员。正是当前软件行业存在的这些事实,使得重构成为TDD中必不可少的实践之一。程序员出于以下目的重构程序。消除重复。第一次写代码的时候,只是为了让程序通过测试。过程中可能会出现大量的重复代码和“僵尸代码”,因此需要在重构阶段剔除重复代码。使代码易于理解和修改。一开始,程序员优先考虑程序的正确性,并没有关注代码的规范,因此需要在重构阶段改进代码。改进软件的设计。好的想法不会一蹴而就。当之前的代码有更好的解决方案时,果断重构,改进软件设计。查找错误并提高质量。好的代码不仅让程序员容易理解和理解,也让程序员容易发现和修复问题。测试和重构齐头并进。提高编码效率和编码水平。重构技术有利于消除重复代码,减少冗余代码,提高程序员的编码水平。程序员编码水平的提高也会体现在编码效率上。2.什么时候应该进行重构那么,程序员应该在什么时候进行重构呢?随时重构。也就是说,把重构当做一种开发的习惯,重构应该像测试一样自然。只有三个东西,第三个是重构。当存在重复代码时,就该进行重构了。添加新功能时。增加了新的功能,调整了原有的代码结构,需要进行单元测试和重构。改正错误时。修复错误后,还需要重新进行单元测试和接口重构。代码审查。代码审查是发现“代码味道”的好时机,当然也是重构的好机会。3.代码的“臭味”如果一段代码不稳定或者存在一些潜在的问题,那么代码中往往会有一些明显的痕迹,就像食物在快要腐烂之前常常会散发出一些异味,这些痕迹就是代码“坏品味”。以下是常见的代码“难闻的气味”。DuplicatedCode(重复代码):重复是万恶之源。解决方案是提取公共功能。LongMethod(太长的函数):太长的函数会导致职责不明确、难以切割、难以理解等一系列问题。解决方案是将长函数拆分成几个函数。LargeClass(太大的类):会导致职责不清,难以理解。解决方案是拆分成几个类。LongParameterList(太长的参数列表):太长的参数列表其实并没有真正遵循面向对象的编码方式,程序员也很难理解。解决方案是将参数封装到结构或类中。DivergentChange(发散变化):当多个需求修改时,会移动这个类。解决办法是拆分代码,把经常变化的东西放在一起。ShotgunSurgery(霞弹类型修改):其实就是在没有改包的情况下改一个需求,然后会涉及到多个要修改的类。解决办法是把所有的修改点都集中起来,抽象成一个新的类。FeatureEnvy(附件复杂):一个类对其他类的依赖性太大,比如一个类大量使用了其他类的成员,这就是FeatureEnvy。解决办法是将这个类合并到依赖类中。DataClumps:数据块是经常一起出现的大堆数据。如果数据有意义,解决方案就是将结构化数据转换为对象。PrimitiveObsession(基本类型偏执狂):热衷于使用基本类型,如int、long和String。解决方案是将其修改为使用类。SwitchStatements(switch恐怖外观):当switch语句判断的条件过多时,就要考虑少用switch语句,改用多态。ParallelInheritanceHierarchies(并行继承系统):使用类继承并行连接了太多的并行类。解决方案是从继承关系中删除其中一个类。LazyClass(冗余类):对于这些冗余类,解决方法是将这些不再重要的类中的逻辑合并到相关类中,删除旧类。SpeculativeGenerality(吹嘘未来):对于这些无用的类,直接删除即可。TemporaryField(混淆临时字段):对于这些字段,解决方法是将这些临时变量集中到一个新的类中进行管理。MessageChains(过耦合消息链):使用你真正需要的函数和对象,而不是依赖消息链。MiddleMan(中间人):存在这种过度代理的问题,解决办法就是用继承代替委托。InappropriateIntimacy(亲密关系):两个类互相使用对方的私有值域。解决办法是划清界限,拆除,或合并,或改为单一链接。AlternativeClasseswithDifferentInterfaces(目的相同的类):这些类往往是相似的类,但有不同的接口。解决办法是重命名这些类,移动函数,或者抽象子类,将它们合并为一个类。IncompleteLibraryClass(不完善的库类):解决方法是再包裹一层函数或者包裹成一个新的类。DataClass(朴素数据类):这些类非常简单,往往只有公共成员变量或简单的操作函数。解决方法是封装相关操作,减少公共成员变量。RefusedBequest(拒绝遗赠):这些类表明父类中有很多方法,但只使用了有限的子类方法。解决方案是使用代理而不是继承关系。Comments(过多的注释):如果注释过多,说明代码不清晰。解决方法是先重构再写注释,去掉多余的注释,“好代码会说话”。4.减少测试依赖首先,我们必须承认,对象之间的依赖是不可避免的。功能是通过对象之间的协作来完成的,任何对象都可以使用其他对象的属性、方法和其他成员。但同时也认识到,代码中对象过于复杂的依赖关系往往是不被提倡的,因为对象之间的相关性越大,如果代码发生变化,影响范围就越大,而这并不是完全有利于系统的测试、重构和后期维护。因此,在现代软件开发和测试过程中,应尽可能减少代码之间的依赖。DI(DependencyInjection)相对于传统的JavaEE开发模式,使得代码对容器的依赖程度降低,减少了计算机程序的耦合问题。通过简单的new操作,构成程序员应用程序的POJO对象就可以在JUnit或TestNG下进行测试。即使没有Spring或其他IoC容器,也可以使用mocks来模拟对象以进行独立测试。清晰的分层和组件化代码将有助于简化单元测试。例如,在运行单元测试时,程序员可以使用stubs或mocks来代替DAO或资源库接口,从而实现对服务层对象的测试,而程序员在此过程中不需要访问持久层数据。这减少了对基础设施的依赖。在测试过程中,真实物体具有不可确定的行为,并可能产生不可预测的效果(如股票行情、天气预报)。同时,实物存在以下问题。真实的对象很难创建。真实对象的某些行为很难触发。真实对象实际上还不存在(与其他开发组或新硬件)等。正是由于上述真实对象的测试过程中存在的问题,模拟测试被广泛使用。在单元测试的上下文中,模拟对象是“模拟”具有某些“虚构占位符”功能的某些对象接口的实现的对象。在测试过程中,这些虚构的占位符对象可用于以简单的方式模拟组件的预期行为和结果,使程序员可以专注于组件本身的全面测试,而不必担心其他依赖项。模拟对象经常用于单元测试。使用mock对象进行测试是指在测试过程中,对于一些不易构造(如HttpServletRequest必须在Servlet容器中构造)或难以获取(如JDBC中的ResultSet对象)的复杂对象,使用A为测试测试方法而创建的虚拟对象(模拟对象)。mock最大的作用就是分解单元测试的耦合。如果编写的代码对另一个类或接口有依赖关系,它可以模拟这些依赖关系并验证调用的依赖关系的行为。模拟对象测试的关键步骤如下。使用一个接口来描述这个对象。在生产代码中实现此接口。在您的测试代码中实现此接口。在测试代??码中,只是通过接口引用了对象,所以并不知道引用的对象是真实对象还是mock对象。目前Java阵营主要的mock测试工具有Mockito、JMock、EasyMock等。5、mock和stub的区别mock和stub都是为了替代外部依赖对象。模拟不是存根。两者有以下区别。前者被称为mockistTDD,而后者通常被称为classicTDD。前者是基于行为的验证(BehaviorVerification),后者是基于状态的验证(StateVerification)。前者使用模拟对象,而后者使用真实对象。现在让我们通过一个例子来看看mock和stub之间的区别。如果程序员想测试发送邮件的行为,他可以像下面这样写一个简单的存根。//待测接口publicinterfaceMailservice(){publicvoidsend(Messagemsg);}/lstub测试类publicclassMailServiceStubimplementsMailServiceipvateListmessages=newArrayList();publicvoidsend(Messagemsg){messages.add(msg);}publicintnumberSent({returnmessages.size();}}}也可以在存根上使用状态验证测试方法,如下所示。(warehouse);//通过发送的消息数来验证assertEquals(1,mailer.numberSent();}}当然这是一个很简单的测试,只发送一条消息,这里程序员没有测试是否会是它发送给正确的人或内容是正确的。如果你使用模拟,那么这个测试看起来就不一样了。lassOrderInteractionTester...publicvoidtestorderSendsMailWorkerfUnFilled(){Orderorder=newOrder(TALISKER,51);Mockwarehouse=mock(Warehouse.class);Mockmailer=mock(MailService.class);order.setMailer((Mailservice)mailer.proxy());order.expects(once()).method("hasInventory").withAnyArgument().will(returnvalue(false));order.fill((Warehouse)warehouse.proxy()}在这两个例子中,使用存根和mock都是用来代替真正的MailService对象,不同的是stub使用的是状态确认方式,而mock使用的是行为确认方式,如果想在stub中使用状态确认方式,需要额外增加一个方法以辅助验证。因此,存根实现了MailService,但增加了额外的测试方法。微服务的集成测试也称为组装测试或联合测试,可以说是单元测试的逻辑延伸。其最简单的形式是二次测试单元被组合成一个组件和内部他们之间的脸受到考验。就使用的基本技术而言,集成测试在许多方面与单元测试相似。程序员可以使用相同的测试运行器和构建系统支持。集成测试和单元测试之间的一个很大区别是集成测试使用相对较少的模拟。例如,在测试数据访问层时,单元测试只是模拟从后端数据库返回的数据。在集成测试中,测试过程中会使用真实的数据库。数据库是需要测试并可能暴露问题的资源类型的一个很好的例子。在微服务架构的集成测试中,程序员更关注服务测试。1、服务接口在微服务架构中,服务接口大多以RESTfulAPI的形式暴露。REST是面向资源的,使用HTTP协议完成相关通信。它的主要数据交换格式是JSON,当然也可以是XML、HTML、二进制文件等多媒体类型。资源操作包括获取、创建、修改和删除资源,都可以使用HTTP协议的GET、POST、PUT和DELETE方法映射相关操作。在进行服务测试时,如果只想测试单个服务功能,那么为了隔离其他相关服务,就需要把所有外部服务伙伴都堆起来。每个下游合作伙伴都需要一个存根服务,然后在进行服务测试时启动它们,以确保它们正常运行。程序员还需要对被测服务进行配置,以保证在测试过程中能够连接到这些打桩服务。同时,为了模仿真实的服务,程序员还需要配置打桩服务,将对被测服务的请求的响应发回。下面是使用Spring框架实现的“用户车辆信息”测试接口示例。importorg.junit.*;importorg.junit.runner.*;importorg.springframework.beans.factory.annotation.*;importorg.springframework.boot.test.autoconfigure.web.servlet.*;importorg.springframework.boot.test。mock.mockito.*;importstaticorg.assertj.core.api.Assertions.*;importstaticorg.mockito.BDDMockito.*;importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;importstaticorg.springframework.test.web。servlet.result.MockMvcResultMatchers.*;@RunWith(SpringRunner.class)@WebMvcTest(UserVehicleController.class)publicclassMyControllerTests{@AutowiredprivateMockMvcmvc;@MockBeanprivateUserVehicleServiceuserVehicleService;@TestpublicvoidtestExample(throwsException{given(this.userVehicleService.getVehicle)).willReturn("(newVehicleDetails("BMW","X7"));this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)).andExpect(status().isok()).andExpect(content().string("宝马x7"));)}}本次测试,程序员用mock模拟/sboot/vehicle接口的数据VehicleDetails("BMW","X7"),通过MockMvc判断测试结果2.客户端有很多客户客户端可用于测试RESTful服务。可以直接通过浏览器进行测试,如本书前面介绍的RESTClient、Postman等。很多应用框架本身都提供了用于测试RESTfulAPI的类库,比如Java平台上Spring的RestTemplate和Jersey的ClientAPI,.NET平台上的RestSharp(http:1restsharp.org)。也有一些独立安装的REST测试软件,比如SoapUI(ttps:/www.soapui.org),当然最简洁的方式还是在命令行中使用cURL来测试。下面以测试Elasticsearch是否启动成功为例。您可以直接在终端中使用cURL来执行以下操作。scurl'http://localhost:9200/?pretty'cURL提供了一种方便的方式来向Elasticsearch提交请求,然后你可以在终端中看到类似如下的响应。“clustername”:“elasticsearch”,“clusteruuid”:“uqcQAMTtTIO6CanROYgveQ”,“version”:{“number”:“5.5.0”,“build_hash”:“260387d”,“build_date”:“2017-06-30T23:16:05.735Z"","build_snapshot":false,"lucenerversion":"6.6.O"},"tagline":"YouKnow,forSearch"}微服务架构引入微服务系统测试后,随着数量微服务随着测试用例的增加,测试用例也随之增加,测试工作越来越依赖于测试的自动化,Maven或Gradle等构建工具都会将测试包含在其生命周期中,所以只要相关的编写单元测试用例,在构建过程中可以自动执行单元测试和集成测试,构建完成后可以立即看到测试报告。在系统测试阶段,除了自动化测试之外,还有人工测试仍然不可避免。Docker等容器提供了自动化基础设施也带来了t手动测试的新变化。在基于容器的持续部署过程中,软件最终会被打包成一个容器镜像,这样就可以部署到任何环境,而不用担心工作变量不一致带来的问题。进入部署阶段意味着集成测试和单元测试通过。但是这显然不是所有的测试,很多测试是上线部署之后必须要进行的,比如一些非功能性的需求。同时,用户对需求的期望是否与最初的设计一致,只有在产品上线后才能验证。因此,发射后的测试工作还是很重要的。1、冒烟测试所谓冒烟测试,是指一个新编译的软件,在版本需要正式测试之前,确认软件基本功能是否正常的测试。软件通过冒烟测试后,将进行后续的正式测试。冒烟测试的执行者往往是版本编译器。冒烟测试时间短,可以验证软件的大部分主要功能。因此在CI/CD日常构建过程中会进行冒烟测试。2.蓝绿部署蓝绿部署通过同时部署新旧版本来降低发布新版本的风险。原理是当新版本部署(绿色部署)时,旧版本(蓝色部署)仍然需要在生产环境中保持可用一段时间。如果新版本上线,测试没有问题,那么所有的生产负载都会从旧版本切换到新版本。以下是蓝绿部署的示例。其中,vl代表服务的旧版本(蓝色),v2代表新版本(绿色),如图4-2所示。以下是注意事项。蓝绿两种部署环境是一致的,两者应该是完全隔离的(可以是不同的主机,也可以是不同的容器)。有一种类似于在蓝绿环境之间切换的设备来进行流量切换,比如负载均衡器、反向代理或者路由器。新版本(绿色部署)测试通过后,可以立即回退到旧版本。蓝绿部署通常与冒烟测试结合使用。实施蓝绿部署,全程自动化,用户不会感觉到任何宕机或服务重启。3.A/B测试A/B测试是一种新兴的软件测试方法。A/B测试本质上是将软件分成两个不同的版本,A和B,分别进行不同的实验。AB测试的目的是通过科学的实验设计、样本抽样、流量细分、小流量测试,获得具有代表性的实验结论,并保证结论的可靠性,然后再推广到所有流量。例如,经过一段时间的测试,实验结论是B版本用户认可度更高,可以将线上系统更新到B版本。4.金丝雀发布金丝雀发布是增量发布的一种,执行通过在原始生产版本可用的同时部署软件的新版本。这样,部分生产流量被转移到新部署的版本,以验证系统是否按预期运行。这些预期内容可以是功能需求,也可以是非功能需求。例如,程序员可以验证新部署的服务的请求响应时间是否小于1秒。如果新版本没有按预期运行,您可以快速返回到旧版本。如果达到预期效果,可以将更多生产流量转移到新版本。金丝雀发布与A/B测试非常相似,两者经常结合使用。与蓝绿部署的区别在于,新旧版本的金丝雀发布共存时间更长。