单元测试特征:
1 范围狭窄
2 限于单一类或方法
3 体积小
为什么要编写单元测试?
为了防止错误(很明显!)
而且还可以提高开发人员的生产力,因为单元测试:
(1) 帮助实施——在编码的同时编写测试可以快速反馈正在编写的代码。
(2) 失败时应该易于理解——每个测试在概念上都应该简单,并专注于系统的特定部分。
(3) 作为工程师如何使用被测系统部分的文档和示例(因为书面文档很快就会无可救药地过时)。
在谷歌,80% 的测试都是单元测试。 编写测试的简便性和运行它们的速度意味着工程师每天要运行数千个单元测试。
可维护性的重要性
这个场景有两个关键问题:
(1) 单元测试很脆弱。 它们因无害且无关的更改而中断,该更改没有引入真正的错误。
(2) 单元测试不明确。 很难理解如何修复测试,因为一开始就不清楚测试在做什么。
当代码及其测试有多个贡献者时,这种情况很容易发生(现实生活中的软件项目往往会有很多人同时参与其中)。
争取不变的考验
防止脆弱测试的关键策略是努力编写不需要更改的测试,除非项目的
需求变更:
• 内部重构不应改变测试。
• 新功能应不影响现有功能。
• 错误修复不应要求更新测试。
• 行为改变:这些可能需要改变测试。
Testing Implementation:
Verifies implementation using package-private method
Testing Behaviour:
This test is using the public API, to the extent of almost
playing a game of Connect4.
The resulting behaviour (a change to the board) is checked using a public method
防止脆性测试
1 仅通过调用公共方法(public methods )进行测试。
2 核实结果是什么,而不是如何实现的。
通过以下方式争取不变的测试:
如果您专注于测试实现而不是行为,您将得到脆弱的测试。
所以总是喜欢针对行为进行测试。
编写清晰的单元测试:
Hamcrest 是一个用于在 Java 中编写富有表现力和可读性的断言的库。 它提供了一组匹配器对象,可以与 JUnit 或 TestNG 等测试框架一起使用,以创建更具描述性和灵活性的断言。
Hamcrest assertions 通过允许您以更流畅和自然的方式构建断言来增强测试的可读性。 您可以使用 Hamcrest 匹配器创建自定义断言,以清晰简洁的方式表达预期行为,而不是仅仅依赖测试框架提供的默认断言方法。
以下是 Hamcrest 断言的一些主要特性和优势:
流畅易读的语法: Hamcrest 断言使用流畅易读的语法,这使您可以编写非常类似于普通英语语句的断言。 这使得更容易理解断言的意图。
可扩展和可定制: Hamcrest 提供了一组丰富的内置匹配器,涵盖常见断言,如相等性、可空性、数字比较、集合内容等。 此外,它还允许您创建自定义匹配器来处理内置匹配器未涵盖的特定断言。
可组合性: Hamcrest 匹配器可以使用“and”、“or”和“not”等逻辑运算符进行组合和组合,使您能够从更简单的断言创建复杂的断言。 这允许更灵活和细粒度的断言。
诊断消息: Hamcrest 在断言失败时提供详细的诊断消息。 这些消息提供有关预期值和实际值的信息,有助于快速识别断言失败的原因。
下面是将 Hamcrest 断言与 JUnit 结合使用的示例:
导入 org.junit.jupiter.api.Test;
导入静态 org.hamcrest.MatcherAssert.assertThat;
导入静态 org.hamcrest.Matchers.*;
公开课 MyTest {
@测试
public void testStringLength() {
String text = "你好,世界!";
assertThat(文本,是(notNullValue()));
assertThat(text, containsString("你好"));
assertThat(文本,hasLength(13));
assertThat(文本, anyOf(startsWith("H"), endsWith("!")));
}
}
在此示例中,Hamcrest 匹配器用于对字符串执行各种断言。 assertThat
方法用于将匹配器应用于被测试的实际值(text
)。 与默认的 JUnit 断言相比,匹配器(is
、containsString
、hasLength
、anyOf
)提供了更具表现力和描述性的断言。
通过使用 Hamcrest 断言,您可以编写更具可读性和表达力的测试,从而更容易理解预期行为并提高测试代码的整体质量。
实际结果和预期结果之间的关系由匹配器指定,在本例中为 equalTo。
Hamcrest 匹配器
Hamcrest 有很多“匹配器”,比如 equalTo。
他们可以帮助编写涉及多种类型的断言,包括:
• 字符串——例如,可以检查字符串是否包含子字符串、忽略大小写等。
• 集合——元素是否在集合中; 集合包含什么,忽略顺序等。
如果没有合适的匹配器,很容易编写自己的匹配器。
请参阅 https://www.baeldung.com/java-junit-hamcrest-guide
使您的测试完整而简洁
确保测试包含读者了解它如何得出结果所需的所有信息。
确保它不包含其他不相关和分散注意力的信息。
当测试用例的主体包含读者理解它如何得出结果所需的所有信息时,测试用例就完成了。
当这两个测试用例的主体包含读者需要了解其如何得出结果的所有信息时,这两个测试用例就完成了。
枚举所有的动作使得方法更长并且防止将移动序列重新用作辅助方法,但更清楚发生了什么。 棋盘上增加了两列垂直棋子,红色在第 0 列获胜
不要DRY测试
DRY——不要重复自己:工程师需要更新一段代码而不是跟踪所有实例。
缺点:可以使代码不那么清晰,需要遵循引用链。
为了使代码更易于使用,这可能是一个很小的代价……但是成本/收益分析在测试中的表现不同:
• 我们希望测试在软件更改时中断
• 生产代码具有测试套件的优势,可确保在事情变得复杂时它仍能正常工作。 测试应该独立存在!(当测试需要测试时出了问题)
湿而不干(DAMP not DRY)
DAMP:描述性的和有意义的短语
DAMP 不能替代 DRY——它是补充。
“助手”方法可以通过使测试更简洁来帮助使测试更清晰——分解出重复的步骤,这些步骤的细节与被测试的行为无关。
但是重构应该着眼于使测试更具可读性和描述性,而不仅仅是为了减少重复。
不要包括清晰和简洁。
当测试用例不包含其他分散注意力或不相关的信息时,它就是简洁的。
我们的测试现在包括很多不需要的动作在测试场景并使电路板更难可视化以及测试结果应该是什么。
不要测试方法——测试行为
但是一个方法可能有不止一种行为和/或一些需要更多测试的棘手的极端情况。
例如,makeMove 可以有多种行为,具体取决于棋盘的状态。
对该方法的一次测试没有任何意义,并且可能不是很清楚也不简洁。
更好的方法:为每个行为编写测试。
测试行为
行为是系统在特定状态下如何响应一系列输入的保证。
行为可以表示为“给定 X,当 Y,然后 Z”
例如:
给定一个 Connect4 棋盘,RED 首先开始,当 RED 下了一个棋子后,接下来就轮到 YELLOW 了。
编写行为驱动的测试
行为驱动的测试更倾向于阅读自然语言,因此相应地构建它们
在被测试的行为之后命名测试.
一个好的名称描述了正在测试的动作(“何时”)和预期结果(“然后”),有时还描述了状态(“给定”)。
一个好的技巧是以“应该”开头的名称,例如
shouldInitializeCorrectly
shouldChangePieceAfterTurn
shouldEndGameWithWinnerWhenFourInARowHorizontally etc.
不要把逻辑放在测试中
不要在测试或逻辑操作中放置条件或循环。
测试应该读作简单的事实陈述,而不是同样需要测试的代码块!
最好只初始化一个较小的 2x2 板并明确检查每个位置
Making Your Unit Tests Clear
1 Make your tests concise and complete (DAMP and not too DRY!)
2 Don’t structure tests around methods – instead structure around behaviours
3 Use the Given-When-Then pattern for testing behaviour
4 Name Tests after the Behaviour Being Tested
5 Don’t put logic in tests