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

组件测试:改造遗留系统的起点

时间:2023-03-18 09:45:17 科技观察

在遗留系统中工作,无论是开发新功能,修改旧功能,还是重构重振雄风,都会面临很多挑战。这些挑战主要来自丢失的业务知识、丢失的技术和烂代码。一般来说,在重构遗留系统时,首先会增加必要的测试,然后进行重构、重新设计等一系列过程,以提高其内在质量。MartinFowler在他的微服务测试策略分享中详细讨论了各种测试方法及其适用场景。在那次讨论中,他介绍了组件测试:组件是较大系统中封装良好、可独立更换的中间子系统。单独测试这些组件有很多好处。通过将测试范围限制在组件内,您可以对组件封装的行为进行验收测试,同时保持比高层测试更好的执行效率。在微服务架构中,组件就是服务本身。MartinFowler还根据测试时调用组件的方式,以及为组件构建测试替身时测试替身位于进程内还是进程外,将组件测试分为进程内和进程外两种形式。组件依赖的存储或服务。在实践中,为遗留系统添加单元测试和端到端接口测试会遇到相应的困难,但我们发现组件测试由于其以行为为中心的特性,可以在单元测试和端到端测试之间取得平衡。它为改造遗留系统提供了一个良好的起点。回避单元测试实践的被动遗留系统自最初发布以来已经过去了很多年,最初的开发人员早已离开,只为后来者留下一段代码。在遗留系统上工作通常不需要破坏其他现有功能,而只需根据需要“恰到好处”地进行修改。作为敏捷开发人员,计划的第一步是使用单元测试来确保现有功能不被破坏。但团队很快发现,遗留系统使用的技术早已失传,新团队中基本没有人懂,很难基于这样的技术构建单元测试。对于一个没有任何自动化测试的老系统,往往意味着它的内部设计是高度耦合的,想要理解清楚就已经很困难了,更不用说可测试性了。在下面的示例代码中,我们无法方便的模拟StockService依赖的WebClient实例,所以无法测试GetUpdatedStock的功能:stocks.com/stocks.json");varstocks=JsonConvert.DeserializeObject>(stockContent);returnstocks.Select(ToStock);}privatestaticStockToStock(StockResponseresp){//对象转换逻辑略}}另一方面,在旧系统上的开发工作往往意味着需要对其进行大规模的重构,以利于更好的可维护性和更容易地添加新功能。在这种背景下,即使在系统中加入了单元测试,后续的重构也会让细粒度的单元测试成为一种浪费——重构必然会修改代码设计,导致需要随之修改单元测试。与单元测试的矛盾相比,组件测试侧重于Web应用程序本身的功能和行为,而不是单个层。事实上,许多遗留系统甚至没有清晰的层次设计。组件测试是对Web应用暴露的API或网页源代码进行测试,在避免代码细节设计不当导致的被动局面的同时,也能保证Web应用行为的正确性,这就是为什么我们在legacysystem测试要保证。组件测试侧重于业务行为而不是代码实现细节。因此,它不会受到代码实现细节变化的影响。因此,组件测试不会限制重构方法的使用,也不会在调整设计时带来修改测试的额外负担。相反,它可以为重构提供强有力的保障,有助于保证重构的安全性。绕过端到端接口测试的困境在重构遗留系统的实践中,很多团队都尝试加入端到端接口自动化测试策略,以摆脱单元测试的被动局面。这样几乎可以完全忽略代码细节,直接关注业务场景。相对来说,只需要自动部署web应用和必要的依赖(如数据库)来测试应用。但在实际实施过程中,团队发现要为老系统搭建这样的环境并不容易。端到端集成测试需要在真实的Web应用实例上运行测试,要求基础设施尽可能真实,包括数据库、缓存设施等。因此,为了让端到端集成测试能够在持续集成环境中自动运行,要求应用程序及其依赖的基础设施具有自动化部署的能力。遗留系统通常自动化程度很低,无法自动部署以进行端到端集成测试。即使Web应用程序本身的部署并不复杂,它所依赖的其他服务也很难自动部署,例如SMTP服务器。在测试金字塔中,端到端测试接口测试处于较高层次。这意味着,即使自动化环境搭建成功,由于测试依赖的资源较多,测试成本也会比较高。由于端到端测试集成了系统的多个级别,因此测试用例的执行比较低级别的测试用例更脆弱、更慢。这些挑战和特点决定了我们很难在短时间内为遗留系统添加足够多的端到端接口测试用例来保证后续的重构工作。在进行组件测试时,完全不需要担心端到端测试的这些问题。组件测试通过一定的方式模拟和隔离Web应用的外部依赖,避免了部署和配置外部依赖的复杂过程。较小的专用仿真层的启动和运行速度可根据测试需求定制;如果采用进程内组件测试,可以进一步提高测试用例的运行效率和稳定性。组件测试的最佳实践是在单元测试中将Web应用本身作为被测单元,用测试替身模拟和隔离Web应用的外部依赖,根据组件中提供的API或Web页面进行测试。业务场景。行为,即组件测试。在进程内组件测试的实用方法中,我们直接在测试代码中自动构建Web应用程序所需的依赖项,启动被测服务,然后调用被测API并执行断言。下面的代码演示了这样一个测试的大致流程:动态创建一个关系数据库,启动web应用,使用web应用中repository需要的数据来准备测试,然后调用待测接口并assert结果。[事实]publicasyncvoidshould_handle_search_request_with_mocked_database(){varsqliteConnection=DatabaseUtils.CreateInMemoryDatabase(outvardatabaseOptions);varappServices=SetupApplication(sqliteConnection,outvarclient);varjim=newEmployee{Id=12,Name="Jim"};appServices.Get>>Service>Service().Save(jim);varresponse=awaitclient.GetAsync("/employees/search/im");varemployeeString=awaitresponse.Content.ReadAsStringAsync();Assert.Equal("Jim(id=12)",employeeString);}组件测试在进程内运行,可选择以适当的方式模拟Web应用程序的依赖项。以数据访问层为例,我们可以直接模拟DAO类,或者在需要测试事务支持的时候构建一个真实的数据库实例用于测试,在测试运行结束时清理这些临时创建的资源。您可以享受上述行为测试的稳定性和代码级模拟的灵活性。具体来说,由于需要在测试代码中按需启动应用程序,因此这对Web应用程序的基础设施提出了一些要求。如果我们基于ASP.NETWEBAPI或SpringBoot等框架开发应用,框架已经提供了这种能力。模拟数据层时,简单情况下可以使用在内存中重新实现的RepositoryStub,必要时可以使用内存中运行的嵌入式数据库,如SqlLite、H2数据库,在数据库中动态创建必要的数据框。表结构(Schema),流行的ORM框架如EntifyFrameworkCodeFirst和Hibernate都具备这样的能力。对于HTTP外部依赖,也可以使用临时实现的Stub对象,也可以使用社区流行的mockhttp、Client-driver等工具库。在此,本文还准备了一个简单的示例程序供读者参考,提供了C#版本和Java版本。从形式上来说,组件测试是一种单元测试,从测试范围来看,也是一种集成测试。在某些场合,我们形象地理解为“集成单元测试”。但它与单元测试的侧重点不同。在编写组件测试的用例时,不要过多关注代码逻辑的细节,而是要从业务场景的角度关注Web应用的行为。例如,在测试用户注册的API时,注册API的成功场景测试给出的响应是正确的,并向用户发送确认邮件,而不是向API提供多个用户名并测试哪些用户名是有效的(那些应该包含在测试用户名验证器的单元测试中)。与进程内组件测试相比,进程外组件测试直接测试已部署的服务,集成度更高,但由于进程外组件测试需要先部署并启动Web服务再运行,其成本较大;测试运行时需要通过网络调用,效率会比较低。因此,在进程外运行组件测试没有任何优势。当进程中的组件测试无法有效完成时,这只是一种妥协。除非需要重构的遗留系统的外部依赖不能基于代码高效设置,不能通过代码在进程内启动,否则应该优先进行进程内组件测试。结论没有人愿意每天都使用遗留系统,但总有一些限制我们不得不妥协。使用遗留系统总是会带来许多挑战。在实践中我们发现,在遗留系统改造过程中,遇到困难时,组件测试总能给出满意的答案。在实践组件测试时,如果一开始无法进行组件在进程内测试,可以先从进程外开始,逐步实现更稳定高效的进程内组件测试。重要的是要注意,在改造遗留系统时,组件测试可能是当前限制条件下的一种有价值的折衷方案。但它不能替代其他类型的测试,我们仍然需要借助其他类型的测试来为应用提供更完善的保障。组件测试只测试应用程序(组件)的内部行为,因此必要时可以采用契约测试等方法来关注系统间交互行为的正确性。在开发新功能时,还是要优先考虑成本最低、最有利于保证系统设计的单元测试;而在保障业务场景的同时,必要的端到端接口测试还是必不可少的。【本文为专栏作者“ThoughtWorks”原创稿件,微信公众号:Thinkworker,转载请联系原作者】点此查看该作者更多好文