Java 单元测试简介
我们都经历过这样的情况:我们在一个项目上投入了大量时间,确保运行所有可能想到的场景,力求让项目尽可能完美。但当你让别人尝试时,他们会发现一些小的异常情况,让你的应用出现意想不到的行为。
为了防止这种情况频繁发生,我们使用不同类型的测试技术:单元测试、集成测试和端到端测试。后者(端到端测试)可以由开发人员或质量保证团队成员进行。
这也被称为测试金字塔
尽量不要被前面提到的另外两种测试类型分散注意力,而要专注于这样一个想法:我们的单元测试在数量上会比其他测试多得多,但请记住,它们不能互相替代。它们都很重要。
闲聊够了,我们开始编码吧,好吗?
要求
- 在您的机器上安装 JDK(呃!)。
- IDE(我推荐 IntelliJ IDEA 社区版)。
- 就 Java 语言而言,您需要熟悉
variables、、和的概念,才能完全理解这篇文章。(我或许可以在这方面constant提供帮助)。functionclassobject - 将此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@AfterMethodtearDown()
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
后端开发教程 - Java、Spring Boot 实战 - msg200.com
