(2023.01.31 Tues)
Unit test related
unit test和integration test的区别
(2023.02.02 Thur)
unit test和integration test的区别在于隔离程度(level of isolation)。Unit test必须完全隔离,而integration test在执行过程中不能摆脱对外部依赖。Integration test通常以整合的方式测试多个模块,因此有了integration test这个名字。Integration test比unit test提供英勇的更多高级视角,因此其提供的反馈更真实而较少聚焦在某一单一功能。因为integration test对外部的依赖,可能导致效率变低并难于设置(setup)。
Best Practices
(2023.01.31 Tues)
每次只测一个功能 Test one thing at a time
比如测试一个函数,该函数返回两个数的和。显然可以调用任意两个数字的组合并检验结果是否正确来确认函数是否正确,尽管这个方法有若干问题。
更好的方式是对函数的不同部分使用独立的测试。比如可以检测该函数能否正确计算两个正数的和,另一个测试检测能否计算两个负数的和,等等。这种独立的测试,其优势在于,1) 易于设计和维护,每次只需要关注一件事,2) 更精确,容错的机会少,3) 易于debug,如果test失败,很容易知道哪个部分出错了。
总之,每次测试一个功能会给unit test带来更高效率,更快更准确,更易于debug。
起个有意义的名字 Name tests meanfully
比如测试页面的login功能,用一个test_login
方法来执行。一旦test fails,则无法知道是因为什么而login失败。但如果将test_login功能拆分为多个不同的模块,比如test_login_with_incorrect_username
,test_login_with_incorrect_password
,等等方法,则不同的测试方法失败可以带给QA精确的信息,关于哪里出现了错误。
除此之外,有意义的名字对于非开发者来说也友好,见名知其含义。
保证单元测试简短而精炼 Keep unit test short and focused
如果unit test过长,则很可能测试超过一件事,将导致测试难于理解和维护,同时也增加测试过程出问题的机会。
如果unit test过于简短精炼,则不会覆盖特别多的内容,可能会导致不同的test suite之间有疏漏。
最好的方案是在简短和精炼之间取得一个平衡,使得测试易于理解维护,同时提供足够的覆盖(adequate coverage)。
用断言验证结果 Use assertions to validate results
Python unit test中的assertion是一个基础部分,用于判断代码中某个特定不是本该返回怎样的结果。如果断言失败,则代码中有bug。
尽管不用断言也可以通过其他方式判断代码是否运行正确,但assertion命令更加专业。
为每个测试方法写文档 Write docstrings for each test method
测试方法中的文档用于解释该unit test的目的以及如何使用。失去了文档则使用者不得不猜测该unit test的功能,可能会导致一些意想不到的错误。
写文档时迫使代码开发者不得不思考每个test case的最终目的,帮助理清思路。当需要修改该方法,文档将提醒开发该测试方法的意义和目的,帮助理顺思路和方法。
避免重复 Don't Repeat Yourself (DRY)
代码重复不仅低效,也可能导致错误(error-prone)。如果你需要更新不同地方/方法中的相同代码段,则很可能在过程中犯错。
避免重复代码,可以使测试模块化,每个test case覆盖单独的一个功能,使得代码更高效、易于维护和少错误。
(Python)用setUp
和tearDown
创建和清理fixture Make use setUp
and tearDown
to create/clean up Fixture
Fixtures是用于作为测试基准(baseline)的对象或数据。通过setUp()
方法创建fixtures,可确保每个测试以clean slate形式运行。这可以避免因使用前一次测试数据导致的假阳性(false positives)
用这两种方法创建和清理fixture同样可以保证测试的DRY。如果在多个测试中创建相同的对象或数据,则fixture是个理想的解决方案。通过将这些对象/数据抽象成一个fixture,可以在多个测试中直接引用,而不用重复代码。
使用fixtures可以提高测试的可读性和可理解性。直接通过名字引用fixtures,而无需写一堆代码用来创建对象或数据,这使得测试使用的数据更加明确,也易于修改。
测试结束时,如果之前设置了fixture,则需要用tearDown()
方法清理资源,否则后续测试中随着未清理的资源渐涨,测试将变慢。setUp()
和tearDown()
应成对出现,否则无法保证每次测试始于一个clean slate.
避免不稳定测试 Avoid brittle tests
不稳定测试(brittle test)是随着被代码修改而崩溃的测试代码。如果测试过于针对代码,较容易发生这种情况。
比如要测试一个求和函数,一个不稳定测试会检测返回值是否等于特定输入列表的预期和。最初这没什么问题,但是如果函数内部做出改变,比如用了不同的算法计算和,则测试将确定失效。
更好的方法是写测试检测函数对于一系列不同的输入返回正确的和。如果函数改变,则这个测试不容易崩溃。
总之,测试代码应尽可能独立于被测试代码的具体实现。这样能保证即便被测试代码发生改变,测试仍然可以正常运行。
经常运行测试代码 Run your test suite regularly
如果被测试的代码经常修改,而测试代码不常运行,则很可能测试代码逐渐变得低效。
经常运行测试代码可在易于发现bugs。越早发现越容易解决。如果被测试代码已经上线却未运行测试,则不易追踪代码中的问题。
(2023.02.01 Wed)
测试应保证确定性 Test should be deterministic
确定性指的是在被测试代码没有修改的情况下,测试代码的行为和结果应该总是相同。确定性一定程度上构成了测试的可被信任的程度。
为保证确定性,代码需要完全隔离(completely isolated),测试不能代码不能依赖
- 其他test cases
- 环境变量,诸如当前时间、系统语言设定、文件系统、网络、API等
总之,test case的输出产生变化,当且仅当被测试的代码发生变化。
测试成为构建过程的一部分 Make test a part of build process
考虑到我们讨论的是自动化测试,将测试加入build process就很有意义。确保构建过程执行unit test并在测试失败时将构建标记为broken.
尽管对于开发者来说频繁在开发机上运行测试代码是必要步骤,特别是基于测试的开发(TDD),但这仍然不够。在构建过程中加入unit test使得系统更加安全,比如开发者在local开发时忘记执行unit test,CI server不会忘记。
Reference
1 climbtheladder点com,10 Python Unit Testing Best Practices
2 testim点io,Unit Testing Best Practices: 9 to Ensure You Do It Right