Java 单元测试简介
我们都经历过这样的情况:我们在一个项目上投入了大量时间,确保运行所有可能想到的场景,力求让项目尽可能完美。但当你让别人尝试时,他们会发现一些小的异常情况,让你的应用出现意想不到的行为。
为了防止这种情况频繁发生,我们使用不同类型的测试技术:单元测试、集成测试和端到端测试。后者(端到端测试)可以由开发人员或质量保证团队成员进行。
这也被称为测试金字塔
尽量不要被前面提到的另外两种测试类型分散注意力,而要专注于这样一个想法:我们的单元测试在数量上会比其他测试多得多,但请记住,它们不能互相替代。它们都很重要。
闲聊够了,我们开始编码吧,好吗?
要求
- 在您的机器上安装 JDK(呃!)。
- IDE(我推荐 IntelliJ IDEA 社区版)。
- 就 Java 语言而言,您需要熟悉
variables
、、和的概念,才能完全理解这篇文章。(我或许可以在这方面constant
提供帮助)。function
class
object
- 将此GitHub Gist
class
中的数学添加到您的项目中。
入门
我们首先要做的就是将 TestNG 框架添加到我们的项目中。这将为我们提供一套classes
稍后annotations
会用到的。
将 TestNG 添加到您的项目
您可以通过两种方式完成:手动安装或在项目中使用 Maven。您可以根据自己的项目设置跳过第二种方式。
手动
您可以按照本指南手动添加它。
通过 Maven
- 打开你的
pom.xml
,它应该位于你的项目的根文件夹中。 - 在标签内
<project>
,制作一个<dependencies>
标签。 - 添加以下 XML 代码块:
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.2</version>
</dependency>
如果你还没有这样做,请务必选择
Enable auto-import
屏幕右下角显示的选项。这将允许 IntelliJ IDEA 自动检测你的任何更改pom.xml
并进行相应的刷新。救命啊 :)
测试对象
为了这篇文章的目的,我们不会应用 TDD(测试驱动开发)技术,我们的重点将放在了解如何编写测试类和方法上。
因此,为了开始,我们需要在目录class
中创建一个src/test/java/
,并将其命名为MathTests
。
在测试名称末尾使用后缀“Tests”
class
是一种常见的命名约定,它可以让其他开发人员和您自己快速知道,无需打开文件,它里面就有测试逻辑。
看起来应该是这样的:
public class MathTests {
// ...
}
您能发现常规的class
和这个有什么不同吗?
没有。
这很好。
构成class
“测试类”的不是它的签名,而是它的方法,并且这些类位于标记为测试源根的目录中非常重要。
这些方法应该遵循特定的命名约定,应该具有public
访问修饰符,应该具有void
返回类型以及@Test
签名前的注释。
注释
@Test
向我们的 IDE 的测试运行器表明这不是一个常规方法,并将相应地执行它。
因此,我们的第一个测试方法将是这样的:
@Test
public void add_TwoPlusTwo_ReturnsFour() {
// ...
}
添加测试逻辑
在其他编程语言中还有另一种非常常见的约定,称为 Triple A:
- 安排:由几行代码组成,用于声明和初始化我们在测试中需要的对象。
- 动作:通常是几行代码,我们用它们来执行动作,无论是某些计算还是修改对象的状态。
- 断言:通常由一行代码组成,用于验证Act部分的结果是否成功。将
actual
结果值与expected
我们计划获取的值进行比较。
实际上,这将是:
@Test
public void add_TwoPlusTwo_ReturnsFour() {
// Arrange
final int expected = 4;
// Act
final int actual = Math.add(2, 2);
// Assert
Assert.assertEquals(actual, expected);
}
关键字
final
用于防止我们的值以后被改变,也称为常量。
在这里我们可以看到整个安排、行动和断言是如何结合在一起的。
如果您查看add_TwoPlusTwo_ReturnsFour()
方法左侧的行号,会看到一个绿色的播放按钮,选择它,然后从上下文菜单中选择第一个选项。
等待片刻...测试运行器面板将打开并显示测试结果。
如果您看到一切都是绿色,则表示我们的测试通过了!
但是,作为一名优秀的测试人员,我们也应该尝试让我们的测试失败。
让我们改变行为部分,使其将3
和加2
在一起,这样我们的actual
值就变成了5
,我们的测试就会失败。
...
失败了吗?
伟大的!
现在,你们中的一些人可能想知道为什么我们使用assertEquals()
Assert 中的方法class
,我们可以手动尝试使用if-else
可以模拟相同结果的块,但 Assertclass
提供了一组方便的方法来执行各种类型的验证。
最常见的是:
assertTrue()
:评估给定的condition
或boolean
,如果它的值为true
,则测试将被标记为通过,否则,它将被标记为失败。assertFalse()
:类似于assertTrue()
,但每当condition
或boolean
为时,false
测试将被标记为PASSED,否则,它将被标记为FAILED。asssertEquals()
:通常用于比较两个给定值,可以是原始类型(int、double 等)或任何对象。
如果我们使用 来实现自己的逻辑if-else
,不仅会使代码变得混乱,还可能导致不必要的结果。因为,如果我们忘记throw
在某个if-else
代码块中执行 和 并抛出异常,那么我们的两条代码路径都会被标记为 PASSED。
提示:大多数情况下,每个测试应该只使用一个 Assert 方法,尽管也有例外。但通常建议这样做,以确保测试代码简洁明了,直奔主题。每个测试每次只验证一条代码路径。此外,如果我们有 3 个断言,第一个断言失败,后面的断言将永远不会执行,请记住这一点。
现在我们已经解决了这个问题,让我们继续进行更多测试!
您如何练习测试该add()
方法的更多场景?
- 一个正数和一个负数相加。
- 添加两个负数。
如果我们再看一下我们的数学class
,我们会发现还有两种方法。
我会让你对multiply()
(提示:确保在将数字乘以零时进行测试)方法进行测试,我将在divide()
本文的其余部分重点介绍该方法。
方法divide()
让我们仔细看看这个方法:
public static double divide(int dividend, int divisor) {
if (divisor == 0)
throw new IllegalArgumentException("Cannot divide by zero (0).");
return dividend / divisor;
}
如您所见,如果divisor
参数的值为0
,我们将抛出IllegalArgumentException
,否则将执行除法运算。
注意:该
throw
关键字不仅会抛出给定的异常,还会停止代码执行,因此它的工作方式类似于break
循环或switch
块内的关键字。
所以,这个方法有两种可能的结果,或者说“代码路径”。我们需要确保对它们进行测试。
每种方法的测试数量应该等于或大于其代码路径的数量。
这意味着我们至少应该进行 2 次测试。
让我们继续制作它们吧!
- 将两个数相除,其中
divisor
为除零 (0) 以外的任意数。 - 将两个数相除,其中
divisor
为零 (0)。
我们的第一个测试将是这样的:
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = Math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
我们的第二个测试是:
@Test(expectedExceptions = IllegalArgumentException.class)
public void divide_TenDividedByZero_ThrowsIllegalArgumentException() {
Math.divide(10, 0);
}
等一下!
读者先生/女士:“但是,安排、行动和断言是怎么回事?角色在expectedExceptions
做什么?”
别担心,我很快就会解释的!
- 我决定跳过整个安排、执行和断言步骤,因为当方法运行时,代码的执行会自动中断
divide()
。因此,对于这个测试来说,整个三重 A 可以省略。 - 需要该
expectedException
部分来告诉我们的测试运行器,IllegalArgumentException
在这个测试中实际上有可能发生这种情况,如果我们将其更改为另一个异常,我们的测试就会失败。
.class
提示:请记住在异常名称末尾使用,否则,此代码将无法编译。
测试对象
你已经注意到,到目前为止,我们一直在测试 Math 的静态方法class
,这意味着我们不必创建它的对象。这很好。
但是如果我们没有class
静态方法怎么办?
为此,我们的测试框架(TestNG)提供了一对注释,以确保我们的每个测试都使用我们的一个新实例class
。
假设我们可以创建 Math 的实例class
。
在这种情况下,我们的测试将如下所示:
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final Math math = new Math();
final int expected = 4;
final int actual = Math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final Math math = new Math();
final double expected = 2.0;
final double actual = Math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
这还不算太糟,但请记住,我们可以对此进行更多测试,class
并且一遍又一遍地初始化这个数学对象会产生更多的代码噪音。
如果我们必须忽略测试的某些部分,特别是在安排中,这意味着我们可以使用我们的测试框架工具之一:
@BeforeMethod 和 @AfterMethod
这两个注释可以添加到我们的测试函数中,就像我们一直使用的那样@Test
,但它们以特定的方式工作。
@BeforeMethod
:此代码块将始终在任何其他方法之前执行。@Test
@AfterMethod
:此代码块总是在任何其他方法之后执行。@Test
那么,我们为什么要使用它们呢?
在我们的所有@Test
方法中,我们都必须不断地启动一个新的数学对象,因此在@BeforeMethod
注释的帮助下,我们可以摆脱这种重复的代码。
我们需要做的第一件事是将 Math 对象提升为成员变量或属性。
public final class MathTests {
private Math math;
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final int expected = 4;
final int actual = math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
}
然后添加我们的@BeforeMethod
函数,通常命名为“setUp”。
public final class MathTests {
private Math math;
@BeforeMethod
public void setUp() {
math = new Math();
}
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final int expected = 4;
final int actual = math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
}
现在,为了确保清除我们的对象,我们可以在函数内部math
设置它的值,通常称为:null
@AfterMethod
tearDown()
public final class MathTests {
private Math math;
@BeforeMethod
public void setUp() {
math = new Math();
}
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final int expected = 4;
final int actual = math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
@AfterMethod
public void tearDown() {
math = null;
}
}
这意味着我们的测试的执行顺序将是:
- 这
setup()
。 - 和
add_TwoPlusTwo_ReturnsFour()
。 - 然后
tearDown()
。 setup()
再次。- 和
divide_TenDividedByFive_ReturnsTwo()
。 - 然后
tearDown()
再说一遍。
就这样吧
通过这个,您现在应该更加熟悉单元测试的工作原理。
虽然我们没有做任何需要使用assertTrue()
和的测试assertFalse()
,但我鼓励您做自己的测试来稍微尝试一下它们:)
如果您有任何疑问,请随时发表评论,我会尽力解答!
如果您想查看整个项目,请转到GitHub 上的这个存储库。
文章来源:https://dev.to/chrisvasqm/introduction-to-unit-testing-with-java-2544