草庐IT

途游游戏 DevOps 实践|都说「单元测试」好,「AAAC四步法」少不了

极小狐 2023-11-11 原文

近日,极狐(GitLab) 江狐会第十四期在北京圆满落幕。

会上,途游游戏运维安全部研发负责人刘勇基于使用极狐GitLab 提高单元测试 ROI 的实践与体会,进行了《途游游戏敏捷开发工程实践》主题分享,为线上线下众多云计算用户、企业 IT 和运维工程师、架构师、开发者,以及开源和 DevOps 的爱好者们提供一些参考。

本文整理自途游游戏刘勇分享的核心内容,欢迎在公众号【极狐GitLab】首页消息对话栏回复“途游”获取 PPT,enjoy~

单元测试在敏捷开发流程中有什么意义?


单元测试是指对软件中的最小可测试单元进行检查和验证,一个单元测试就是一段自动化代码,这段代码调用被测试的目标单元,检查目标单元的行为是否符合开发人员的预期。

如图 1 所示,单元测试处于测试金字塔的最底层,也就是软件研发的早期阶段,属于白盒测试,是开发的组成部分。

图 1

那么,单元测试在敏捷开发流程中有什么意义呢?

从研发质量角度考虑

敏捷的核心即拥抱变化,但变化带来风险。无论是因为重构、需求变更或其他导致代码必须要变更的时候,单元测试可以第一时间发现变更的代码是否会对业务逻辑造成破坏性的影响,这是单元测试最大的价值——守护程序的业务逻辑

第二,“Talk is cheap,show me the code”,单元测试为研发人员提供了被测试代码的功能和使用案例,相当于更详细的文档,能够对改善代码结构产生积极的影响。

第三,有了高质量的单元测试,开发人员可以对已有代码进行有信心的变更,不管这种变更来自于业务和需求的变化,还是来自于重构。

最后,任何看起来难以测试的代码也将难以维护、发展,并且在其整个生命周期中都会受到许多错误的影响。因此,单元测试促使开发人员重新思考他们的编码方式,提升编码质量

从测试的经济学角度考虑

如图 2 所示,85% 的 Bug 在 Coding 阶段产生,而传统的测试人员往往集中在 Function Test(功能测试)、System Test(系统集成测试)阶段,这些阶段修复 Bug 的成本数十倍增加,发布后的修复成本达到惊人数百倍

图 2

Bug 发现的越早,修复成本就越低。后期 Bug 的修复不仅增加沟通时间,还可能引入新的问题,增加测试验证时间,项目的进度也有延迟上线的风险。

因此,我们要尽可能地把测试左移,在软件开发的早期阶段通过单元测试发现 Bug  ,更低成本处理 Bug ,提高代码质量,优化测试过程的投资回报率。

图 3

总而言之,单元测试杠杠好!

那么如何实施单元测试?

基于极狐GitLab 的单元测试四步法


途游游戏使用极狐GitLab 进行软件研发实践,刘勇有一个深刻体会:极狐GitLab 能够很好地帮助你将花费很多精力、时间和成本,好不容易写出的单元测试的价值充分挖掘出来。

首先回答一个问题:单元测试放在哪里?

可以放在单独目录里,如Java,Maven tests 目录、Package 和被测代码在一起;也可以和源代码在一起,如 Golang。原则上要尽可能离源码近

如何实施单元测试?刘勇归纳了单元测试四步骤(AAAC)

  • Arrange 筹备:为测试做准备;

  • Act 执行:给予特定行为所需的上下文和输入并执行;

  • Assert 断言:判断结果是否符合预期;

  • Clear 清理环境:为后续测试保证上下文干净,测试之间彼此隔离没有依赖性。

1. Arrange 筹备:编写单元测试

Arrange 阶段就像多米诺骨牌之前的排列工作,为了接下来的行为可以被激发,包括但不限于准备所需的输入(对象、基础数据结构等)、启动/终止某服务如 MQ 或数据库、将一些数据预先存入数据库,为尚不存在的用户生成一些凭据等的事情。

极狐GitLab 有一个功能就是可以在容器里边提供很多预置的 Service,让数据库也好, MQ 也好,都可以很轻松地预先准备好。这样的话,当你在测试一个函数的时候,它的环境、数据库都会自动在你的 CI 环境中就绪,大幅提升单元测试的效率,是很贴心的服务。

✨ 单元测试准则参考

  • 每个测试范围职责单一(不要试图用一个测试函数测试10个功能点)

  • 执行速度快(总体执行时间不超过5分钟,单个用例执行时间3秒以内,TDD闭环时间)

  • 自动化测试思路,避免人工查看,比如用 assert 代替 print 信息人眼识别结果

  • 良好设计并命名的可复用单元测试体系

  • 测试可以很容易的在 CI 流水线中在程序员的开发机上运行,不需要搭建或者很容易自动化搭建所需的测试环境

  • 单元测试执行结果稳定,一个 10 次执行,8 次通过 2 次失败的单元测试是错误的

  • 每一个测试函数所依赖的单元测试的环境应该是固定并干净的,测试函数彼此隔离,一个测试函数所依赖的环境不能是另一个测试函数执行后的结果

2. Act 执行:应用单元测试

Act 阶段对应多米诺骨牌,即是指尖轻轻推动第一块触发牌。费了很多的时间和精力,终于写完了单元测试,也能跑了,这个时候我们怎么去用它呢

首先来看极狐GitLab 上的工作流(如图 4),途游游戏在此基础上稍加改动:把代码 Commit 之后,Push 到我们远端的服务仓库时会进行一次单元测试执行,在合并的时候会进行第二次次单元测试执行,在合并之后还会进行第三次单元测试执行。

 

图4

我们如何控制单元测试在何时执行?途游游戏在极狐GitLab上有两种触发方式,分别是:

(1)代码合并到主分支之前,每次代码 Push 到 MR 源分支,并基于合并后的代码(极狐Gitlab Merge Request Pipeline + Merge Result Pipelines)

首先,我们有一个主分支,往往是 Main 分支。这时候,如果我们要开发一个新功能,可根据这个新功能创建一个 Feature Branch 即特性分支,程序员会工作在特性分支上。

接着,我们会不断 Commit 代码,并且 Push 到在极狐GitLab 上创建的特性分支上,这个时候就会触发单元测试执行。

一般的 CI 工具只会执行在 Source Branch 上,就是源分支上,但极狐GitLab 有一个功能叫做 Merge Result Pipeline ,这是一个独特的功能,它的单元测试是跑在特性分支和组分支合并之后的代码上,而不需要你进行真正的合并,避免在特性分支的开发过程中,有其他人员抢先一步,在主分支上合并了其他的代码,而导致你的代码合并失败。

这也是途游游戏所秉承的 “测试前置”,即应用极狐GitLab 的 Merge Request Pipeline + Merge Result Pipeline 在特性分支上就发现问题,尽早解决问题。

(2)代码合并到主分支之后(CI/CD之前)

此时,途游游戏再次执行单元测试来守护代码,这里用到了极狐GitLab 的另一个功能:Job。一个抽象 Job 相当于 “类”,本身不会实例化,也不会真正地执行。做法是在前面加一个 “.”。

这样做的根本目标在于,需要在两种情况下执行同一个 Job,也就是 .unittest 这个 Job。这两种触发条件为:

1. 有新的代码被提交到某一个 Merge Request 的源分支上

2.代码被合并到默认分支后

图 5

3. Assert 断言:查看单元测试结果

单元测试的结果包括了测试覆盖率,以及若测试失败,其失败的结果。具体不同的语言,不同的测试框架,有不同的实现方式。

以 pytest 实现方式为例(如图 6),后面四行就是该单元测试的报告,用来衡量单元测试成功与否;前缀是与测试覆盖率相关的一些东西,在极狐GitLab 界面上可以看到这个结果,实现单元测试用例执行结果的可视化

pytest itom_cmdb_tests/ 
--create-db 
--migrations 
--vcr-record none 
--junitxml=test_reports/itom_cmdb_testcase_report.xml 
--cov-report xml:test_reports/coverage.xml 
--cov-report term 
--cov=itom_cmdb itom_cmdb_tests/tests/

图6 展示的是极狐GitLab 的 Merge Request 的 Code Review 视图:绿色杠杆就是有单元测试的一些代码,红色是单元测试没有覆盖到的代码。极狐GitLab 会自动获取相应信息,并且上传到极狐GitLab 的环境当中

图 6

4. Clear 清理:清除单元测试环境

在单元测试最后阶段,需要清理测试环境,例如还原全局配置、清理创建的文件目录等,为后续测试保留干净的上下文,确保测试之间彼此隔离没有依赖性。

以上就是途游游戏运维安全部研发负责人刘勇分享的单元测试主要内容,欢迎关注极狐GitLab 持续获取各类技术干货!

有关途游游戏 DevOps 实践|都说「单元测试」好,「AAAC四步法」少不了的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - Prawn - 表格单元格内的链接 - 2

    我正在尝试用Prawn生成PDF。在我的PDF模板中,我有带单元格的表格。在其中一个单元格中,我有一个电子邮件地址:cell_email=pdf.make_cell(:content=>booking.user_email,:border_width=>0)我想让电子邮件链接到“mailto”链接。我知道我可以这样链接:pdf.formatted_text([{:text=>booking.user_email,:link=>"mailto:#{booking.user_email}"}])但是将这两行组合起来(将格式化文本作为内容)不起作用:cell_email=pdf.make_c

  3. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  4. ruby-on-rails - Rails 中同一个类的多个关联的最佳实践? - 2

    我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来

  5. ruby - 单元测试文件 I/O 方法 - 2

    我对单元测试还是比较陌生。我用Ruby编写了一个类,它接受一个文件,在该文件中搜索给定的Regex模式,替换它,然后将更改保存回文件。我希望能够为此方法编写单元测试,但我不知道我将如何去做。有人能告诉我我们如何对处理文件i/o的方法进行单元测试吗? 最佳答案 看看这个HowdoIunit-testsavingfiletothedisk?基本上这个想法是一样的,文件系统是你的类的依赖。所以引入一个可以在你的单元测试中模拟的角色/接口(interface)(这样你在单元测试时就没有依赖性);角色中的方法应该是您从文件系统中需要的所有东西

  6. ruby-on-rails - 向 Rails 3 添加 Ruby 扩展方法的最佳实践? - 2

    我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion

  7. ruby - 我需要从 facebook 游戏中抓取数据——使用 ruby - 2

    修改(澄清问题)我已经花了几天时间试图弄清楚如何从Facebook游戏中抓取特定信息;但是,我遇到了一堵又一堵砖墙。据我所知,主要问题如下。我可以使用Chrome的检查元素工具手动查找我需要的html-它似乎位于iframe中。但是,当我尝试抓取该iframe时,它​​是空的(属性除外):如果我使用浏览器的“查看页面源代码”工具,这与我看到的输出相同。我不明白为什么我看不到iframe中的数据。答案不是它是由AJAX之后添加的。(我知道这既是因为“查看页面源代码”可以读取Ajax添加的数据,也是因为我有b/c我一直等到我可以看到数据页面之后才抓取它,但它仍然不存在)。发生这种情况是因为

  8. ruby - 尝试运行 minitest 单元测试时出错 - 2

    尝试使用rubytest/test_foo.rb运行minitest单元测试时出现以下错误:Warning:youshouldrequire'minitest/autorun'instead.Warning:oradd'gem"minitest"'before'require"minitest/autorun"'From:/home/emile/.rvm/rubies/ruby-2.0.0-p0/lib/ruby/2.0.0/minitest/autorun.rb:15:```test_foo.rb看起来像这样:require'minitest/autorun'classTestFoo

  9. ruby 单元测试 : run some code after each failed test - 2

    在Test::Unit中的ruby​​单元测试断言失败后,在执行teardown之前,是否有一些简洁优雅的方法来立即执行我的代码?我正在做一些自动化的GUI测试,并希望在出现问题后立即截图。 最佳答案 如果您使用的是1.9,请不要使用Test::Unit::TestCase作为您的基类。对其进行子类化并覆盖#run_test以进行救援,截取屏幕截图并重新提出:classMyAbstractTestCase或者,我认为这实际上是最简洁的方法,您可以使用before_teardownHook:classMyTestCase这不适用于1.

  10. Ruby 最佳实践 : working with classes - 2

    参见下面的示例,我想最好使用第二种方法,但第一种也可以。哪种方法最好,使用另一种的后果是什么?classTestdefstartp"started"endtest=Test.newtest.startendclassTest2defstartp"started"endendtest2=Test2.newtest2.start 最佳答案 我肯定会说第二种变体更有意义。第一个不会导致错误,但对象实例化完全过时且毫无意义。外部变量在类的范围内不可见:var="string"classAvar=A.newendputsvar#=>strin

随机推荐