什么是 Java 中的单元测试?
在开始学习 Java 中的 JUnit 之前,让我们简要概述一下单元测试是什么以及为什么它如此流行(如果您已经知道这些东西,请跳到“我如何用 Java 编写 JUnit 测试?”)。Java 中的单元测试使大规模软件开发更加高效和轻松。它可以帮助个人和团队减少无数小时的调试时间并极大地简化协作过程。
https://junit.org/junit4/
单元测试的基本思想是:编写单个功能的原子测试(称为单元测试),并在测试并确保之前的功能正常后慢慢添加更多功能。这是一个极其简单但功能强大的想法。作为此过程的外观示例,假设您正在构建一个虚拟科学计算器。除了明显的算术运算符 (
+
,
-
,
x
,
%
) 之外,此计算器还具有需要其他子功能才能在其中运行的高级功能。要计算指数,您的计算器需要能够正确乘法。因此,构建和测试此计算器的单元测试方法是:
- 写一个加法函数。仔细测试它,改变它,重复直到它起作用。
- 对减法、乘法、除法函数执行相同的操作。
- 使用这些基本运算符编写更高级的运算符函数(如指数),然后也测试这些函数。
这确保了构建其他较小子功能的功能不仅可以正常工作,而且不会在其中包含错误的子功能。例如,如果我正在测试指数函数并且出现问题,我知道错误可能不在乘法子功能中,因为乘法函数已经过广泛测试。这极大地消除了我需要回溯和检查以查找错误的代码总量。希望这个简单的例子可以清楚地说明围绕单元测试的思维过程是如何构建的。但是单元测试如何与软件开发过程的其余部分交互?如果您有更复杂的功能,需要能够一起工作和交流怎么办?单元测试不足以确保如此复杂的功能可以一起正常工作。事实上,这只是软件测试四个级别的第一步(我使用大写字母是因为我指的是行业标准或最常见的软件测试方法)。最后三个步骤是
集成测试、
系统测试和
验收测试. 这些可能都意味着您认为它们所做的,但让我澄清一下:集成测试是我们要做的,以确保上面提到的那些“复杂功能”能够正确地相互交互。(例如,确保计算器可以处理“3 + 7 * 4 - 2”)系统测试是测试特定系统的整体设计;通常有多个复杂功能的系统在一个产品中协同工作,因此您将这些系统分组并单独测试它们。(例如,如果你正在构建一个图形计算器,你首先要构建算术“系统”来处理数字,测试直到它按预期工作,然后你将构建并测试图形“系统”来处理取款,因为它将建立在算术系统之上)。验收测试是用户级测试;它正在查看是否所有系统都可以同步工作以创建成品以供用户接受(例如,用户测试计算器)。软件开发人员有时会忽略流程的最后一步,因为公司通常会让其他员工单独部署用户(测试版)测试。
如何用 Java 编写 JUnit 测试?
现在您对单元测试的好处和局限性有了更清晰的认识,让我们来看看一些代码!我们将使用一种流行的 Java 测试框架,称为 JUnit(另一个流行的框架是 TestNG,如果您愿意,也可以使用它。它们在语法上非常相似;TestNG 的灵感来自 JUnit)。您可以
在此处下载并安装 JUnit 。对于这个示例代码,我们将继续我之前提到的“科学计算器”示例;脑洞大开非常简单,测试代码也超级简单。传统做法是为每个类编写单独的测试类,这就是我们要做的。假设此时,我们有一个
Math.java
包含所有数学函数(包括
Math.add
)的文件,我们正在编写一个
MathTests.java
文件在同一个包中。现在让我们设置导入语句和类主体:(可能的 JUnit 面试问题:您可能会被问到将 JUnit 测试放在哪里以及是否需要导入源文件。如果您将测试类编写在与你的主类,那么你不需要在测试类中为你的源文件导入任何语句。否则,确保你正在导入你的源文件!)
import org.junit.jupiter.Test; //gives us the @Test header
import static org.junit.jupiter.api.Assertions.assertEquals; //less typing :)
public class MathTests {
//...
}
第一个 import 语句给了我们
@Test
标题。我们
@Test
直接在每个测试函数定义的顶部写上 ' ',以便 JUnit 知道这是一个可以单独运行的单一单元测试。稍后,我将向您展示如何使用此标头运行特定的单元测试。第二个 import 语句为我们节省了一些输入。我们用来测试函数的主要 JUnit 函数是 to
Assert.assertEquals()
,它有两个参数(实际值和预期值)并确保它们相等。有了第二个 import 语句,我们只需键入 '
assertEquals(...
',而不必每次都指定它是哪个包的一部分。现在让我们写一个非常简单的测试用例来验证 2 + 2 确实是 4!
import org.junit.jupiter.Test; // gives us the @Test header
import static org.junit.jupiter.api.Assertions.assertEquals; // less typing :)
public class MathTests {
@Test
public void add_twoPlusTwo_returnsFour(){
final int expected = 4;
final int actual = Math.add(2, 2);
assertEquals(“2+2 is 4”, actual, expected);
}
}
让我们回顾一下测试函数的五行中的每一行以及它们的作用: 第 5 行:此
@Test
标头指定下面的函数定义
add_twoPlusTwo_returnsFour()
确实是 JUnit 可以单独运行的测试函数。第 6 行:这是我们测试用例的函数签名。测试用例总是非常单一;他们只测试一个特定的例子,比如 2+2=4。通常以“
[function]_[params]_returns[expected]()
”的形式命名您的测试用例,其中
[function]
是您正在测试的函数的名称,
[params]
是您正在测试的特定示例参数,
[expected]
是函数的预期返回值。测试函数几乎总是有一个返回类型 '
void
' 因为整个函数的要点是运行
assertEquals
, 无论您的测试是否通过,它都会输出到控制台;您不需要在任何地方返回任何其他数据。第 7 行:我们声明一个
final
返回类型为 的 ' ' 变量
Math.add (int)
,并按照约定将其命名为 'expected'。它的值就是我们期待的答案(4)。第 8 行:我们声明一个
final
返回类型为 的 ' ' 变量
Math.add (int)
,并按照约定将其命名为 'actual'。它的值是 的结果
Math.add(2, 2)
。第九行:黄金线。这是比较实际值和预期值的线,告诉我们只有当它们相等时我们才能通过测试。传递的第一个参数“2+2 is 4”是对测试函数的描述。
如果我的函数应该抛出异常怎么办?
如果您的特定测试示例应该抛出异常而不是断言实际值和预期值相等,那么 JUnit 有一种方法可以在标头中阐明这一点
@Test
。让我们看下面的一个例子。
Math.java
假设我们在called中有一个函数
Math.divide
,我们想确保输入不能被 0 除。相反,尝试调用
Math.divide(a, 0)
任何 'a' 值应该抛出异常 (
ArithmeticException.class
)。我们在标头中这样指定:
import org.junit.jupiter.Test; // gives us the @Test header
import static org.junit.jupiter.api.Assertions.assertEquals; // less typing :)
public class MathTests {
@Test (expectedExceptions = ArithmeticException.class)
public void divide_byZero_throwsException() throws ArithmeticException{
Math.divide(1, 0);
}
}
您可以为 设置多个异常
expectedExceptions
,只需确保使用方括号和逗号来列出您的异常类,例如:
expectedException = {FirstException.class, SecondException.class, … }
如何在 Java 中运行我的 JUnit 测试?
如何将 JUnit 添加到 IntelliJ:
https://stackoverflow.com/questions/19330832/setting-up-junit-with-intellij-idea 您可以像通常运行测试一样运行您的项目。在测试类中运行所有测试将按字母顺序运行它们。在 JUnit 5 中,您可以通过添加标记来为测试添加优先级
@Order
。一个例子:
@TestMethodOrder(OrderAnnotation.class)
public class Tests {
…
@Test
@Order(2)
public void a_test() { … }
@Test
@Order (1)
public void b_test() { … }
…
}
即使在代码中按字母顺序
a_test()
排在前面,也会在此处运行在前面,因为 1 按顺序排在 2 之前。这就是 JUnit 基础知识的全部内容。现在,让我们解决几个常见的 JUnit 面试问题,并在此过程中了解更多关于 JUnit 的知识!
b_test()
b_test()
a_test()
JUnit 面试问题(附加信息)
在这里,我收集了最流行的 JUnit 面试问题。如果你有什么要补充的——请在下面的评论中随意添加。
问:您可以在测试方法中调用什么方法来使测试自动失败? A: fail(“这里有错误描述!”); 问:你正在测试一个 Dog 类;要测试 Dog 对象,您需要先实例化它,然后才能对其运行测试。所以你写了一个 setUp() 函数来实例化 Dog。您只想在所有测试期间运行此函数一次。您必须直接在 setUp() 函数签名上方放置什么,以便 JUnit 知道在运行测试之前运行 setUp()? 答: @BeforeClass(JUnit 5 中的@BeforeAll) 问:上面描述的 setUp() 函数的函数签名必须是什么? A: public static void。任何带有@BeforeClass(JUnit 5 中的@BeforeAll)或@AfterClass(JUnit 5 中的@AfterAll)的函数都需要是静态的。 问:您已经完成了 Dog 类的测试。您编写 void tearDown() 函数来清理数据并在每次测试后将信息打印到控制台。您希望此函数在每次测试后运行。您必须直接在 tearDown() 函数签名上方放置什么,以便 JUnit 知道在运行每个测试后运行 tearDown()? 答: @After(JUnit 5 中的@AfterEach)
GO TO FULL VERSION