为了测试,技术战略和技术选择的细节

为考试选择技术。

最近,我自己从零开始制作了一个 API。在这个过程中,我决定要好好写测试。于是,我开始收集我的知识,终于成功创建了一些基本类型。然而,我为什么会这么苦恼呢?回想起来,我觉得这种为了做好这样的测试而进行技术选择的过程,非常依赖于技术选择的方式和库的细节,给人留下了相当混乱的印象。而且,在这样的过程中,需要把握的要点有些模糊,因此不知道该如何做出正确的选择。这导致了一种境地,即在测试技术选择方面,我们应该关注什么,什么样的选择才算是正确的。为了解决这个问题,我把这些内容进行了梳理,并以备忘录的形式写下来。

考试和架构战略

在“单元测试的定义、价值和风险”一文中,引用了“实践测试驱动开发”中所定义的测试的概念。

分類説明受け入れテストシステム全体が機能するか?インテグレーションテスト私たちが変更できないコードに対して,書いたコードが機能するか?ユニットテストオブジェクトは正しく振舞っているか?また,オブジェクトは扱いやすいか?

以下是一个简单的CRUD REST API的例子。我们将以以下顺序图所示的架构来创建它。

image.png

控制器(Controller)是与框架密切相关的一层,可以从HTTP请求中提取param和header,并返回HTTP响应。应用(Application)层是所谓的业务逻辑层,用于处理业务领域的任务。而存储库(Repository)是与数据库协作的一层,用于获取和写入数据。

在这种结构下,我会按照以下方式进行测试构建。
单元测试(UnitTest)用于验证应用层的业务逻辑,并使用模拟(Mock)替代与存储库层的协作部分。集成测试(IntegrationTest)实际进行与数据库的协作,以验证数据是否被持久化且可检索。最后,验收测试(Acceptance测试)用于验证连接所有层的操作。发送HTTP请求,进行路由,根据参数从数据库中获取数据,进行处理,并确认是否返回为HTTP响应。

单元测试的困难之处

言語おすすめテストフレームワークおすすめモックライブラリJavaJUnitMockitoGotestinggomockPythonpytestpytest(MagicMock)PHPPHPUnitPHPUnitTypeScriptjestjest

在选择测试框架时的困难之处

选择测试框架是不是很难呢?可能会被认为是”依赖语言”的部分很多。比如,如果是PHP,可以选择PHPUnit。如果是Java,JUnit可能是更好的选择。Go语言也很方便,因为它提供了go test等标准库。但有点麻烦的是Python和JavaScript。Python有内置的doctest和unittest。除了内置的,还有pytest和nose2等。该选择哪个呢?最近,当我开发一个名为agwui的Python库时遇到了困难。那时候,我参考了开源软件的选择。当时我看了Flask,因为它采用了pytest,所以我认为它应该是可信赖的。于是我决定使用pytest。实际写代码后感觉非常好。JavaScript的技术选择非常麻烦。前端JavaScript社区更新很快,大约三个月过去后,就会有人说这已经过时了吧?对我个人来说,这个领域非常艰难,因为即使搜索”测试javascript”,由于不准确的信息或过时的信息,搜索结果也不理想。从外部看,很难区分哪些是新的,哪些是旧的。例如,只是稍微想一想,就有Karma、Istanbul、Chai、Mocha、Jasmine、Jest、Selenium、Cypress等许多选项。而且,有时很难判断这些是测试库还是断言库,还是专注于端对端测试。如果一次性学习,就会有脑力不够的问题。因此,下面是我以前总结的一篇文章。

使用TypeScript进行测试驱动的REST API的HowTo指南。

老实说,我不太确定最近那篇文章的代码是否能够运行。但是,在这个快速发展的领域里,我认为只有自己动手实践,不断摸索和经验总结才是唯一的选择。个人而言,我推荐使用jest。原因有四个:它自带了模拟功能,测试覆盖率容易查看,命令行界面的输出结果有着彩色标记,而且它是由Facebook开发的。对我来说,开发者的背景和实力对于稳定性是相当重要的。

选择模拟库的困难

我认为动态语言不需要Mock库。然而最近有点变化。PHP 7以后可以使用类型,Python也可以使用类型提示。因此,动态语言也可能需要Mock库,因为可以更难进行松散的类型检查。在静态语言的经验中,Java中的Mockito非常好。但我指的是纯粹的Mockito。这可能是个人喜好的问题,但是Spring Boot附带的Mockito非常令人困扰。我真的讨厌@MockBean注解。编译时也不会报错,并且Spring Boot本身会要求E2E测试,所以基本上DI容器会被调用,导致测试很慢。如果不熟悉,开发周期会非常困难。因此,开始运行非常困难。

对于Golang,gomock更好一些。虽然也有testify,但gomock的”大致”更好。它的意思是,为了编写模拟,testify需要定义一个”用于创建模拟的类”,这非常麻烦。因此,在构建小型项目时它还行,但是当需要大量测试用例时,使用gomock会更容易。但gomock也有一些特殊的用法,需要从接口自动生成模拟类的源代码。使用这类生成工具时,需要额外的CI和开发环境设置,这对我来说很麻烦。

集成测试的困难之处

在这里,我们将在“存储库模式中与数据库协作的测试”上下文中讨论集成测试。在这种情况下,重点是什么,

    • 冪等性の確保

 

    本番環境との差異

这就是所谓的”幂等性”,无论进行多少次测试,结果都相同。在进行集成测试时,确保幂等性变得非常重要。另一个是”与生产环境的差异”。在开发环境中创建与生产环境相同的环境是很困难的。对于简单的系统可能还好,但是对于像”由RAID0+1存储构成的MySQL”或”在北海道和关东两个地点实现地理分布的Cassandra”这样的系统,将非常困难。此外,使用像Amazon Aurora这样的数据库,它会被供应商绑定,无法在本地环境中运行,也是导致测试变得困难的原因之一。

手法冪等性の確保本番環境との差異軽量DBによるテスト易しい大きいDockerを用いたテスト普通少ない共有DBのテスト難しい少ない

通过轻量级数据库进行测试。

「輕量級數據庫」這個詞可能不太正確,但因為它有時會被使用,所以我們使用這個術語。所謂的「SQLite」和「H2」是指的資料庫。要談到它們的輕量級特點,PostgreSQL和MySQL在安裝後需要分別啟動守護進程。而輕量級數據庫則可以在程式端作為庫進行安裝,並且可以輕鬆地呼叫。例如,SQLite是一個基於文件的資料庫,已經作為Python的標準庫內建,也可以在Android上使用。H2是一個在Java中使用的資料庫。這些輕量級數據庫可以輕鬆地使用並且可以隨時棄用,可以在內存中建立資料庫等。這一特點非常方便,因為不需要花費很長的時間來啟動和關閉資料庫,也不會在測試環境中留下垃圾數據,非常易於使用。然而,這樣的輕量級數據庫與生產環境之間存在差異,因此在生產環境中往往更常使用MySQL、PostgreSQL、Oracle等。在這種情況下,也會有「無法進行測試」之類的討論。例如,假設使用了Python的ORM工具SQLAlchemy。在生產環境中使用了MySQL,在測試環境中使用了SQLite。然後,建立了一個作為數據持久化層的Repository,並撰寫了整合測試。這樣做有什麼問題嗎?問題在於,在測試環境中進行驗證的是「通過SQLAlchemy將數據持久化到SQLite的Repository類」。並未驗證數據是否被持久化到了MySQL。這似乎是很自然的事情,但觀點稍有不同。進一步討論應該是要相信SQLAlchemy還是不相信SQLAlchemy。自己撰寫Repository的源代碼,這意味著「自己撰寫的數據持久化邏輯」可能存在缺陷。在使用SQLite進行測試時,可以對該方面進行充分測試。因此,在MySQL上也可能正常運作。這是一種解釋。但如果從SQLite切換到MySQL,使用SQLAlchemy可能無法正確地進行持久化。這也是一種觀點。或許你可能會覺得這太過小題大作,但當系統變得關鍵且不能中斷時,技術選擇這個問題變得相當敏感。根據所建構系統的可靠性等依賴情況,需要考慮測試策略等。

稍微有點離題,但在相反情況下,出現錯誤的可能性是我們的體驗中很常見的。例如,有一個連接到MySQL的生產環境系統,但沒有進行測試。然後,為了撰寫測試,我們試圖在本地簡單地使用SQLite代替,卻發現ORM只有一個適當的實現,因此測試無法通過。這種情況相當常見。

Note: The translation provided above is one possible option. There may be other variations that could also accurately convey the same meaning in Chinese.

使用 Docker 进行测试 (DinD,DooD)

在这种情况下,我认为最好的选择是使用Docker进行测试。对于MySQL和PostgreSQL来说,Docker镜像已经公开并且进行了很详细的版本控制,因此与生产环境的差异也很小,配置也很容易。因此,可以缓解“测试不完整”等批评。此外,最近通过docker-compose等工具可以轻松启动多个Docker容器,因此部署开发环境也很简单。

然而,这也有一些困难。其中一个问题是“在CI中的实现性”。这涉及到DinD和DooD等模式,以及如何构建测试容器和其他容器的问题(涉及在Docker容器内部使用Docker)。个人认为DooD可能是一个不错的选择,但现有的文献相对较少。目前在Qiita上发布的一些关于CircleCI和GithubActions的文章主要是入门级别,并且大多涉及简单的单元测试。因此,关于这类较为复杂的测试,可用的信息非常有限。因此,需要充分利用CI平台的功能,阅读文档,并自行建立专业知识,以构建稳定并持续运行的CI环境,这需要一些时间。

共享数据库的测试

制作关键任务的系统,如果性能要求严格,在开发期间可能需要准备一个与实际环境相似的数据库。然而,要实现与实际环境相似的配置,就必须考虑到服务器硬件、网络交换机和物理布局等因素,因此不能轻易地为个人提供一个独立的环境。在这种情况下,团队可能会共享一个数据库进行测试。由于与实际环境的差异非常小,因此很少出现在实际环境中暴露的错误。然而,只能一个人进行集成测试,如果破坏了数据库,就需要手动恢复,导致开发效率非常低下。此外,在开发过程中经常会发出非法查询,进入一个不断破坏和修复的恶性循环。

受入测试的难度

有时也被称为端到端测试的接收测试。在这个测试中

    • フレームワークのテストのドキュメントがあること

 

    モックが差し込めること

这两个变得重要起来。

有测试框架文件的文档。

在node.js的框架Express.js中,缺乏测试文档。但是,这并不意味着Express没有测试。如果查看其仓库,可以看到使用了一些著名的库,如istanbul、mocha、should和supertest。虽然这些库很有名,但并没有提及测试技术,需要自己归纳总结经验。

另外一个例子是go-swagger,虽然有文档,但也感到有些不和谐。大致来说,建议通过将业务逻辑和处理程序进行分离,以使业务逻辑部分可以进行测试。虽然这个策略本身是合理的,但作为框架的说明却有点偏离主题的感觉。目前,我想要进行的测试是”验收测试”,因此必须编写与框架集成的测试。因此,尽管我希望了解go-swagger的惯例,他们却在观点上建议”使其解耦以进行测试”等事项。这个回答与我要解决的问题不符。当然,在go-swagger中,关于验收测试的方法也已经提到了,所以并不坏……例如,如果我完全相信了这个建议并编写了测试,可以编写/users的入口点测试并确认没有错误。但由于缺乏验收测试,不能保证在发送请求到/users时能够正确路由到先前的逻辑,这就会导致问题的出现。

再举一个例子,比如使用Java编写API,一般会选择SpringBoot。例如,TERASOLUNA的文档非常全面,是非常优秀的。但是问题在于有些东西不懂。Spring如果严肃使用,在没有考虑”DI容器”、”低耦合度”、”分层”和”测试策略”的情况下,就会面临”无法进行测试”的问题。但是,初学者只关心”测试方法”,并且在没有理解的情况下阅读文档时,会产生许多疑问,比如”为什么会出现分层概念?””为什么有这么多种测试方法?”。即使如此,还是要编写测试!这样会以错误的方式切割层次,使用不恰当的方法进行强制测试。这样就很容易产生与框架紧密结合的、易于破坏且不完整的测试。 如果严肃而谨慎地使用Spring,需要相当多的设计、架构以及对Spring设计思想的了解才行。这本身就是一个非常高背景要求的东西。

刚才提到了设计思想。大多数框架都是类似的,可能会以为它们都差不多。但实际上,是否了解这种设计思想将影响到实施策略。我之前写过一篇关于验证实现的文章,提出了应将验证实现放在哪里的问题。可以明确地说,分为将验证放在Controller中的派别(如Spring、Laravel)和将其放在Model中的派别(如Django、RoR)。它们之间存在相当大的分歧,不同的模块划分方式、测试顺序也会因此不同。由此引发的测试方法也有所不同。因此,需要对思想层面进行理解,并考虑将其应用于何种产品现场。

总的来说,我必须说缺乏文档。我做了一个用RoR创建的项目。我尝试了用Flask创建项目。或者我学习了这样那样的框架。有很多这样的文档,如”只需阅读这些就能运行Web应用程序”。但是关于依赖于框架的测试知识真的很少。如果没有方便地进行搜索和查看这样的知识,可能就不会产生将E2E测试引入产品的想法。另外,英语也让人感到困扰……因此,受入测试的引入门槛可能会变高。相反地,我要说引入受入测试这件事,就是预先思考好并且愿意走这样的困难之路。

模型可以插进去

为什么在接受测试时需要插入模拟?我认为有这样一个疑问。例如,当与数据库的连接失败时,API级别需要返回500错误。在这种情况下,需要在执行任意时刻的测试时关闭数据库连接。但是,这是非常困难的处理。因此,在进行此类测试时,能否插入模拟成为关键点之一。另外,还有另一个原因,那就是”为了测试”。模拟是为了测试而存在的,虽然看起来很自然,但有些不同。例如,我想在发送邮件时使用Amazon SES。但是,如果一直连接着Amazon SES,每次测试都会发送电子邮件。在这种情况下,即使是端到端测试,也希望将邮件部分替换为虚拟。这样的需求使得模拟在”替代某一功能”的场景中变得有效。然而,在”插入模拟”方面存在两个障碍,即”是否能够设计松散耦合的架构以便于插入模拟”和”框架是否被设计为能够插入模拟”。关于前者,这取决于程序员的技术水平。他们能理解软件处理流程的程度如何?能否创建可测试的内容?这可以通过努力来解决。对于后者,以前提到的go-swagger为例,它是一种非常难以插入模拟的框架类型。实际上,go-swagger会自动根据swagger定义文件来生成应用程序的启动和路由等。并且,这部分是不允许手动更改的原则。这使得接触的代码部分很少,这是一个好的方面,但个人而言,在测试时和在生产中运行的代码切换不太容易。因此,插入模拟非常困难,需要了解go-swagger将代码纳入黑盒测试,并确保不会对生产环境产生影响,这让我觉得有点困难。尤其是在最近的趋势中,意识到程序的启动变得不容易。正如文章中提到的”库”和”框架”之间的区别,我的程序被”框架”调用的方式越来越多。因此,在编写应用程序时,只需按照教程中的惯例处理启动处理即可,而实际上更多的是关注处理程序的写法。然而,当编写测试时,情况就不同了,测试需要对启动部分的处理流程,以及处理的钩子时机和方法有一定的了解,事实上,查阅此部分文档可能会很困难。

评论

起初我写下了“根据技术选择的方式和库的细节,给人一种相当不确定的印象”,但事实上我真的很苦恼。单单选择一个库本身就很困难,而一旦涉及到测试,又会引入另一个维度,变得更加困难。虽然将“单元测试”、“集成测试”和“验收测试”作为课本上的概念非常有用,但将其实际落实到代码上却存在各种问题。我试图揭示实现这些概念的障碍。是否存在一份清晰地总结了这些内容的文档呢…

广告
将在 10 秒后关闭
bannerAds