单元测试,TDD,BDD

Last Updated: 2023-12-10 06:52:02 Sunday

-- TOC --

单元测试

单元测试很重要,它是开发人员自己实施的最重要的测试活动。

我以前写的一段单元测试的文字:

1, Unit test could not kill all bugs, but you will come across more bugs if there is not unit test.

2, Unit test is the key to practice TDD and give you more confidence of your code.

3, Unit test can help you thinking and designing code in a more layered way. (Abstraction is the key in computer system design)

4, If you find the code is hard to do unit test, maybe there is a chance to refactor it.

5, Coding while testing is my best practice, why not keep the test code decently! You will find them useful all the time.

6, Unit tests can be served as a kind of test report or bug report.

TDD

TDD (Text Driven Development)在多个层面发挥作用,单元测试也是TDD的重要一环。

也不是要硬着头皮必须给每个function做单元测试,根据实际情况来,那些standalone或很核心的接口,非常适合先完成单元测试,而那些控制流程的代码,在后期看看是否有可能被单元测试覆盖。

越是底层的接口,越是要想办法把单元测试做扎实了!

TDD

看到一本C++的英文教材,里面介绍了TDD的概念,与我之前理解的稍有不同:TDD是先写测试代码,后写业务代码。

貌似我个人的习惯是,业务代码和测试代码同时编写...

写单元测试是在节省时间

当单元测试覆盖了项目的大部分功能代码以后,每当你对代码做出任何调整,只要跑一遍所有的单元测试,绝大多数问题都会自动浮出水面。许多隐蔽的bug根本不可能被发布出去,因为单元测试会将它们扼杀在摇篮里。

因此,虽然不写单元测试看上去节约了一丁点儿时间,但有问题的代码上线后,你会花费更多的时间去定位、去处理这些bug。缺少了单元测试的帮助,你需要耐心找到改动可能会影响到的每个模块,手动验证它们是否还能工作正常。所有这些事儿所花费的时间,足够你写好几十遍单元测试了。

另一个单元测试能节约时间的场景,发生在项目需要重构时。

假设你要对某个模块做大规模的重构,那么,这个模块是否有单元测试,分别对应着两种天差地别的重构难度。对于没有任何单元测试的模块来说,重构是地狱难度。在这种环境下,每当你调整任何代码,你都必须仔细找到模块的每一个被引用处,小心翼翼的手动测试每一个场景。稍有不慎,重构就会引入新的Bug,好心就会办出坏事。

而在有着完善单元测试的模块里,重构是件轻松惬意的事情。在重构时,你可以按照任何你想要的方式,随意调整和优化旧代码。每次调整后,只要重新跑一遍测试用例,几秒钟之内你就能得到完善和准确的反馈。

所以,写单元测试不是浪费时间,也不会降低你的开发效率。你在单元测试上花费的那点时间,会在未来的日子里,为项目的所有参与者节约不计其数的时间。

没有测试代码,几乎无法重构!

不要总想着“补”测试

单元测试不光能验证程序的正确性,它还能极大的帮助你改进代码设计。但这种帮助有一个前提条件,那就是你必须在编写代码的同时,编写单元测试。当开发功能与编写测试同步进行时,你会来回切换自己的角色,分别作为代码的设计者和使用者,不断从代码里找出问题,调整设计。经历过多次调整与打磨后,你写出的代码会变得更好,更有扩展性。

但是,当你已经开发完功能,准备“补”单元测试时,你的心态和所处环境就已经完全不同了。

测试代码并不比普通代码地位低,选择事后补测试,你其实白白丢掉了用测试来驱动代码设计的机会。只有在编写代码时同步编写单元测试,你才能最大的享受到单元测试的好处。

难测试的代码就是烂代码

在不写单元测试时,烂代码就已经是烂代码了,只是我们并不能很好的意识到这一点。也许在Code Review阶段,某个经验丰富的同事会在Review评论里,友善而委婉的提道:“我感觉UserPostService类好像有点复杂?要不要考虑拆分下?”但也许他也不能准确的说出拆分的深层理由,也许经过妥协后,这堆复杂的代码最终就这么上线了。

但有了单元测试后,情况就完全不同了。每当你写出难以测试的代码时,单元测试总会无差别的大声告诉你:“你写的代码太烂了!”不留一点情面。

因此,每当你发现很难为代码编写测试时,你就应该意识到代码设计可能存在问题,你需要努力调整设计,让代码变得更容易被测试。

单元测试给了你一个评估代码质量的标尺。每当你写好一段代码时,你都能清楚知道代码到底写的是好还是坏,因为单元测试不会撒谎。

每一个函数接口,其功能都应该是清晰的单纯的,这样的接口一定是容易测试的,不容易测试的接口,可能其功能不够清晰,机制和策略在某种程度上混在了一起。烂代码的味道,就是难以实施单元测试!

像功能代码一样对待测试代码

随着项目的不断发展,功能代码一定会越来越多,测试代码也同样会随之增长。在看过许许多多的功能代码与测试代码后,我发现,人们在对待这两类代码的态度上,常常有着一些微妙的区别。

第一个区别,是对重复代码的容忍程度。举个例子,假如在功能代码里,你提交了10行非常相似的重复代码。那么这些重复代码,几乎一定会在Code Review阶段,被其他同事作为烂代码指出来,最后它们非得被抽象成函数不可。但在测试代码里,10行重复代码是件稀松平常的事情,人们甚至能容忍更长的重复代码段。

另一个区别,是对代码执行效率的重视程度。在编写应用代码时,我们非常关心代码的执行效率。假如某个核心API的耗时,突然从100毫秒变成了130毫秒,会是个严重的问题,需要尽快解决。但是,假如有人在测试代码里,引入了一个效率低下的fixture,导致整套测试的执行耗时突然变慢了30%,似乎也不是什么大事儿,极少会有人关心。

最后一个区别,是对于“重构”的态度。在写功能代码时,我们常常会定期回顾一些质量糟糕的模块,在必要时做一些改善质量的重构工作。但是,我们却很少对测试代码做同样的事情,除非某个旧测试用例突然坏掉了,否则我们绝不去动它。

总体来说,在大部分人看来,测试代码更像是代码世界里的“二等公民”。人们很少关心测试代码的执行效率,也很少会想办法提升测试代码的质量。

但这样其实是不对的。如果人们对测试代码缺少必要的重视,那么测试代码就会慢慢腐烂。当项目的测试代码最终变得不堪入目,执行耗时以小时为单位计算时,人们从心理上就会开始排斥编写测试,也不愿意去执行测试。所以,我建议你应该像对待功能代码一样,来对待测试代码。

测试代码的执行效率同样也十分重要。只有当整套单元测试,总能在足够短的时间内执行完时,大家才会更愿意频繁的执行测试。在开发项目时,所有人能更快、更频繁的从测试中获得反馈,写代码的节奏才会变得更好。

总结一下,在项目开发的过程中,除了关注功能代码的质量与效率以外,你也应该对测试代码一视同仁,只有这样做,才能最大发挥出测试的能力,让项目保持活力。

避免教条主义

比如:有一些测试用例,可能同时测试了两个或更多API,或者测试整个CLASS,有人说这已经不是单元测试(Unit Test)了,而是集成测试(integration test)了。不需要那么教条,单元,Unit,的含义不仅仅是一个API,它可以是更宽泛的功能单元。

测试单个函数接口,测试接口的组合,测试流程,用脚本测试不同的命令行接口,这些都可以是单元测试!认真严肃的项目,必须要要有各种测试代码。


Unit Tests

Unit tests verify that a focused, cohesive collection of code — a unit, such as a function or a class — behaves exactly as the programmer intended. Good unit tests isolate the unit being tested from its dependencies. Sometimes this can be hard to do: the unit might depend on other units. In such situations, you use mocks to stand in for these dependencies. Mocks are fake objects you use solely during testing to provide you with fine-grained control over how a unit’s dependencies behave during the test. Mocks can also record how a unit interacted with them, so you can test whether a unit is interacting with its dependencies as expected. You can also use mocks to simulate rare events, such as a system running out of memory, by programming them to throw an exception.

Integration Tests

Testing a collection of units together is called an integration test. Integration tests can also refer to testing interactions between software and hardware, which system programmers deal with often. Integration tests are an important layer on top of unit tests, because they ensure that the software you’ve written works together as a system. These tests complement, but don’t replace, unit tests.

将多个unit合并在一起的测试,叫做集成测试,iintegration test。

Acceptance Tests

Acceptance tests ensure that your software meets all of your customers’ requirements. High-performing software teams can use acceptance tests to guide development. Once all of the acceptance tests pass, your software is deliver able. Because these acceptance tests become part of the code base, there is built-in protection against refactoring or feature regression, where you break an existing feature in the process of adding a new one.

Performance Tests

Performance tests evaluate whether software meets effectiveness requirements, such as speed of execution or memory/power consumption. Optimizing code is a fundamentally empirical exercise. You can (and should) have ideas about which parts of your code are causing performance bottlenecks but can’t know for sure unless you measure. Also, you cannot know whether the code changes you implement with the intent of optimizing are improv ing performance unless you measure again. You can use performance tests to instrument your code and provide relevant measures. Instrumentation is a technique for measuring product performance, detecting errors, and logging how a program executes. Sometimes customers have strict performance requirements (for example, computation cannot take more than 100 milliseconds or the system cannot allocate more than 1MB of memory). You can automate testing such requirements and make sure that future code changes don’t violate them.

Power Consumption也可能在测试范围内!

How Does Jenkins Work?

Jenkins takes the development steps from integration to deployment, automating every step of the way.

Each time a developer publishes a commit to the source code repository, Jenkins triggers a build. Typically, the commits post to a development branch.

The build steps include testing the code, ensuring the build does not break. If any error occurs, Jenkins notifies the developer to act accordingly. If all tests pass, the Pipeline proceeds to the integration steps.

Integration takes longer and requires testing the code against multiple system configurations. Jenkins performs parallel integration tests on different nodes, reducing the time needed to try and integrate code.

Further down the Pipeline, Jenkins automates user acceptance testing, which is a requirement before deployment. If all tests pass, the code merges into the main branch, making the code available to the users.

BDD

Behavior Driven Development(BDD),行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。行为驱动开发(BDD)利用简单的格式化自然语言(包括英语、中文等语言)来提升敏捷研发团队和产品经理或业务人员之间的沟通水平,使得敏捷研发团队更够更好的理解业务目标,从而更好的满足产品经理或业务人员的产品需求。

BDD的软件研发过程是这样的:

bdd.png

本文链接:https://cs.pynote.net/sf/202203241/

-- EOF --

-- MORE --