Junit 4 测试方法
1. JUnit 最佳实践指南
原文: https://howtodoinjava.com/best-practices/unit-testing-best-practices-junit-reference-guide/
我假设您了解 JUnit 的基础知识。 如果您没有基础知识,请首先阅读(已针对 JUnit 5 更新)。 现在,我们将介绍在编写测试用例时必须考虑的 junit 最佳实践。
编写糟糕的单元测试非常容易,这会给项目增加很少的价值,同时又会天文数字地增加代码更改的成本。
Table of Contents
Unit testing is not about finding bugs
Tips for writing great unit tests
Test only one code unit at a time
Don’t make unnecessary assertions
Make each test independent to all the others
Mock out all external services and state
Don’t unit-test configuration settings
Name your unit tests clearly and consistently
Write tests for methods that have the fewest dependencies first, and work your way up
All methods, regardless of visibility, should have appropriate unit tests
Aim for each unit test method to perform exactly one assertion
Create unit tests that target exceptions
Use the most appropriate assertion methods.
Put assertion parameters in the proper order
Ensure that test code is separated from production code
Do not print anything out in unit tests
Do not use static members in a test class
Do not write your own catch blocks that exist only to fail a test
Do not rely on indirect testing
Integrate Testcases with build script
Do not skip unit tests
Capture results using the XML formatter
Summary
在编程中,“单元测试是一种测试源代码的各个单元以确定它们是否适合使用的方法。” 现在,这个源代码单元可以在不同的情况下使用。
例如:在过程编程中,一个单元可以是一个完整的模块,但更常见的是一个单独的函数或过程。 在面向对象编程中,一个单元通常是一个完整的接口,例如一个类,但可以是一个单独的方法。 直观上,应该将一个单元视为应用中最小的可测试部分。
单元测试不是寻找回归错误
好吧,了解单元测试的动机很重要。 单元测试不是查找应用范围的错误或检测回归缺陷的有效方法。 根据定义,单元测试将分别检查代码的每个单元。 但是,当您的应用实际运行时,所有这些单元都必须协同工作,并且整个过程比独立测试部分的总和更加复杂和微妙。 证明 X 和 Y 组件都可以独立工作,并不表示它们相互兼容或配置正确。
因此,如果您要查找回归错误,则实际上一起运行整个应用会更有效,因为它将在生产环境中运行,就像手动测试 。 如果您将这种测试自动化以检测将来发生的破损,则称为集成测试,通常使用与单元测试不同的技术。
“从本质上讲,单元测试应该被视为设计过程的一部分,因为它是 TDD(测试驱动开发)中的一部分。” 这应该用于支持设计过程,以便设计人员可以识别系统中的每个最小模块并分别进行测试。
编写出色的单元测试的提示
1.一次仅测试一个代码单元
首先,也许是最重要的。 当我们尝试测试代码单元时,该单元可能有多个用例。 我们应该始终在单独的测试用例中测试每个用例。 例如,如果我们为一个函数编写测试用例,该函数应该具有两个参数,并且在进行一些处理后应返回一个值,那么不同的用例可能是:
- 第一个参数可以为空。 它应该抛出无效的参数异常。
- 第二个参数可以为空。 它应该抛出无效的参数异常。
- 两者都可以为空。 它应该抛出无效的参数异常。
- 最后,测试功能的有效输出。 它应该返回有效的预定输出。
当您进行了一些代码更改或重构,然后测试功能没有损坏时,这将很有帮助,运行测试用例就足够了。 同样,如果您更改任何行为,则需要更改单个或最少数量的测试用例。
2.不要做出不必要的断言
请记住,单元测试是某种行为应该如何工作的设计规范,而不是对代码碰巧所做的所有事情的观察列表。
不要试图断言所有事情都只专注于要测试的内容,否则,由于一个原因,您最终将导致多个测试用例失败,这无助于实现任何目标。
3.使每个测试独立于其他所有测试
不要制作单元测试用例链。 这将阻止您确定测试用例失败的根本原因,并且您将不得不调试代码。 同样,它会创建依赖关系,这意味着如果您必须更改一个测试用例,那么您就不必要在多个测试用例中进行更改。
尝试对所有测试用例使用@Before
和@After
方法。 如果您需要多种支持@Before
或@After
中的不同测试用例的方法,请考虑创建新的Test
类。
4.模拟所有外部服务和状态
否则,这些外部服务中的行为会与多个测试重叠,并且状态数据意味着不同的单元测试会影响彼此的结果。 如果您必须按照特定的顺序运行测试,或者只有在数据库或网络连接处于活动状态时才能运行,则您肯定走错了路。
另外,这很重要,因为您不希望调试由于某些外部系统中的错误而实际失败的测试用例。
(顺便说一句,有时您的架构可能意味着您的代码在单元测试期间会触及静态变量。如果可以,请避免这样做,但如果不能这样做,请至少确保每个测试在运行之前将相关的静态操作重置为已知状态。 )
5.不进行单元测试配置设置
根据定义,您的配置设置不是任何代码单元的一部分(这就是为什么您将设置提取到某些属性文件中的原因)。 即使您可以编写用于检查配置的单元测试,也可以只编写一个或两个测试用例,以验证配置加载代码是否正常工作,仅此而已。
在每个单独的测试用例中测试所有配置设置仅证明了一件事:“ 您知道如何复制和粘贴。”
6.清晰一致地命名您的单元测试
好吧,这可能是最重要的一点,要记住并保持关注。 您必须根据测试案例的实际用途和测试来命名它们。 使用类名和方法名作为测试用例名称的测试用例命名约定从来都不是一个好主意。 每次更改方法名称或类名称时,您最终也会更新很多测试用例。
但是,如果您的测试用例名称是逻辑的,即基于操作,则几乎不需要修改,因为大多数应用逻辑将保持不变。
例如。 测试用例名称应类似于:
1) TestCreateEmployee_NullId_ShouldThrowException
2) TestCreateEmployee_NegativeId_ShouldThrowException
3) TestCreateEmployee_DuplicateId_ShouldThrowException
4) TestCreateEmployee_ValidId_ShouldPass
7.首先对依赖关系最少的方法编写测试,然后逐步进行
该原则表明,如果要测试Employee
模块,则应该首先测试Employee
模块的创建,因为它对外部测试用例的依赖项最小。 一旦完成,就开始编写Employee
修改的测试用例,因为它们需要一些雇员在数据库中。
要在数据库中拥有一些员工,您的创建员工测试用例必须先通过,然后才能继续。 这样,如果员工创建逻辑中存在一些错误,则可以更早地发现它。
8.所有方法,无论是否可见,都应进行适当的单元测试
好吧,这确实是有争议的。 您需要查找代码中最关键的部分,并且应该对其进行测试,而不必担心它们是否是私有的。 这些方法可以具有从一到两个类调用的某些关键算法,但是它们起着重要的作用。 您想确保它们按预期工作。
9.针对每种单元测试方法,精确执行一个断言
即使这不是经验法则,您也应该尝试在一个测试用例中仅测试一件事。 不要在单个测试用例中使用断言来测试多个事物。 这样,如果某个测试用例失败,则可以确切地知道出了什么问题。
10.创建针对异常的单元测试
如果某些测试用例希望从应用中抛出异常,请使用“Expected
”属性。 尝试避免在catch
块中捕获异常,并使用fail
或asset
方法结束测试。
@Test(expected=SomeDomainSpecificException.SubException.class)
11.使用最合适的断言方法
每个测试用例都可以使用许多断言方法。 运用最适当的理由和思想。 他们在那里是有目的的。 尊敬他们。
12.以正确的顺序放置断言参数
断言方法通常采用两个参数。 一个是期望值,第二个是原始值。 根据需要依次传递它们。 如果出现问题,这将有助于正确的消息解析。
13.确保测试代码与生产代码分开
在您的构建脚本中,确保测试代码未与实际源代码一起部署。 这浪费了资源。
14.不要在单元测试中打印任何内容
如果您正确地遵循了所有准则,那么您将不需要在测试用例中添加任何打印语句。 如果您想拥有一个,请重新访问您的测试用例,您做错了什么。
15.不要在测试类中使用静态成员。 如果您已经使用过,则针对每个测试用例重新初始化
我们已经说过,每个测试用例都应该彼此独立,因此永远不需要静态数据成员。 但是,如果您在紧急情况下需要任何帮助,请记住在执行每个测试用例之前将其重新初始化为初始值。
16.不要写自己的只能使测试失败的catch
块
如果测试代码中的任何方法引发某些异常,则不要编写catch
块只是为了捕获异常并使测试用例失败。 而是在测试用例声明本身中使用throws Exception
语句。 我将建议使用Exception
类,并且不要使用Exception
的特定子类。 这也将增加测试范围。
17.不要依赖间接测试
不要假定特定的测试用例也会测试另一种情况。 这增加了歧义。 而是为每种情况编写另一个测试用例。
18.将测试用例与构建脚本集成
最好将测试用例与构建脚本集成在一起,以使其在生产环境中自动执行。 这提高了应用以及测试设置的可靠性。
19.不要跳过单元测试
如果某些测试用例现在无效,则将其从源代码中删除。 不要使用@Ignore
或svn.test.skip
来跳过它们的执行。 在源代码中包含无效的测试用例不会帮助任何人。
20.使用 XML 格式器捕获结果
这是感觉良好的因素。 它绝对不会带来直接的好处,但是可以使运行单元测试变得有趣和有趣。 您可以将 JUnit 与 ant 构建脚本集成,并生成测试用例,并使用一些颜色编码以 XML 格式运行报告。 遵循也是一种很好的做法。
总结
毫无疑问,单元测试可以显着提高项目质量。 我们这个行业中的许多学者声称,任何单元测试总比没有好,但是我不同意:测试套件可以成为一项重要资产,但是不良套件可以成为负担不起的同样巨大的负担。 这取决于这些测试的质量,这似乎取决于其开发人员对单元测试的目标和原理的理解程度。
如果您理解上述准则,并尝试在下一组测试用例中实现其中的大多数准则,那么您一定会感到与众不同。
请让我知道您的想法。
学习愉快!
2. 用 JUnit 编写测试
在 JUnit 中,测试方法带有@Test
注解。 为了运行该方法,JUnit 首先构造一个新的类实例,然后调用带注解的方法。 测试抛出的任何异常将由 JUnit 报告为失败。 如果未引发任何异常,则假定测试成功。
import java.util.ArrayList;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class Example {
@BeforeClass
public static void setup() {
}
@Before
public void setupThis() {
}
@Test
public void method() {
org.junit.Assert.assertTrue(new ArrayList().isEmpty());
}
@After
public void tearThis() {
}
@AfterClass
public static void tear() {
}
}
3. 断言
在JUnit 4中,org.junit.Assert
类提供了一系列的静态方法,用于在单元测试中进行断言(Assertion)。断言是测试代码中的一种关键机制,用于验证代码的行为是否符合预期。如果断言失败,即实际结果与预期结果不一致,那么测试就会失败。
以下是一些常用的Assert
方法:
-
assertEquals
: 验证两个值是否相等。Assert.assertEquals(expected, actual);
-
assertTrue
: 验证一个布尔表达式是否为true
。Assert.assertTrue(booleanExpression);
-
assertFalse
: 验证一个布尔表达式是否为false
。Assert.assertFalse(booleanExpression);
-
assertNotNull
: 验证一个引用是否不是null
。Assert.assertNotNull(object);
-
assertNull
: 验证一个引用是否是null
。Assert.assertNull(object);
-
assertSame
: 验证两个引用是否指向同一个对象。Assert.assertSame(expected, actual);
-
assertNotSame
: 验证两个引用是否指向不同的对象。Assert.assertNotSame(val1, val2);
-
assertArrayEquals
: 验证两个数组是否相等。Assert.assertArrayEquals(expectedArray, actualArray);
-
fail
: 手动标记测试为失败。Assert.fail("Test failed with a message");
下面是一个使用Assert
方法的JUnit测试示例:
import org.junit.Test;
import static org.junit.Assert.*;
public class ExampleTest {
@Test
public void testAddition() {
int expected = 5;
int actual = 2 + 3;
assertEquals("Addition method failed", expected, actual);
assertTrue("2 + 3 should be greater than 4", actual > 4);
assertNotNull("The result should not be null", actual);
}
}
在这个例子中,我们测试了一个简单的加法操作,使用了assertEquals
来验证实际结果是否与预期结果相等,使用了assertTrue
来验证一个布尔表达式,以及assertNotNull
来确保结果不为null
。
断言方法通常接受两个参数:一个是预期值,另一个是实际值。有些断言方法还可以接受一个字符串消息,该消息在断言失败时显示,以帮助快速定位问题。
使用断言是编写有效单元测试的关键部分,它确保了测试的准确性和可维护性。
2. 注解
1. @Test
- 表示该方法是一个测试方法。JUnit将会执行标记了此注解的方法。
2. @Before
- 表示在每个测试方法之前执行的方法,通常用于测试环境的初始化。
3. @After
- 表示在每个测试方法之后执行的方法,通常用于清理测试环境。
4. @BeforeClass
`
- 表示在所有测试方法之前仅执行一次的方法,必须是静态方法。通常用于一次性的全局设置。
5. @AfterClass
- 表示在所有测试方法之后仅执行一次的方法,必须是静态方法。通常用于一次性的全局清理。
6. @Ignore
- 表示暂时忽略该测试方法,不执行它。
7. @RunWith
- 用于指定一个自定义的测试运行器。例如,可以使用`@RunWith(Parameterized.class)`来进行参数化测试。
8. @Test(expected = Exception.class)
- 表示预期在执行该测试方法时抛出指定类型的异常。如果没有抛出异常或抛出不同类型的异常,测试将失败。
9. @Test(timeout = 1000)
- 表示测试方法应该在指定的毫秒数内完成。如果超出指定时间,测试将失败。
3. 测试类监听器
在JUnit 4中,RunListener
是一个接口,它允许开发者介入测试运行的生命周期中的各个点。通过实现 RunListener
接口,可以自定义测试执行过程中的行为,例如测试开始、结束、发生错误或失败时的处理等。
以下是 RunListener
中一些关键的方法:
testRunStarted
: 当整个测试运行开始时调用。testRunFinished
: 当整个测试运行结束时调用。testStarted
: 当单个测试方法开始执行时调用。testFinished
: 当单个测试方法执行结束时调用。testFailure
: 当测试失败时调用。testAssumptionFailure
: 当测试因为一个假设(assumption)失败而不是失败(failure)时调用。testIgnored
: 当测试被忽略时调用。
要使用 RunListener
,你需要实现该接口,并重写你感兴趣的方法。然后,你需要告诉 JUnit 你的 RunListener
实现应该被使用。这可以通过几种方式完成:
-
通过程序化注册:在你的测试代码中,使用
Request.runListeners
方法注册RunListener
。 -
通过 JUnit 的
@Rule
机制:创建一个实现TestRule
接口的类,该类在其apply
方法中注册RunListener
。 -
作为 JUnit 的启动参数:在命令行或 IDE 配置中指定你的
RunListener
实现。
下面是一个简单的 RunListener
实现示例,它在控制台输出每个测试的开始和结束:
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.RunListener;
public class SimpleRunListener extends RunListener {
@Override
public void testStarted(Description description) {
System.out.println("Test started: " + description.getMethodName());
}
@Override
public void testFinished(Description description) {
System.out.println("Test finished: " + description.getMethodName());
}
}
要在测试套件中使用这个监听器,你可以在测试类上使用 @Rule
注解一个字段:
import org.junit.Rule;
import org.junit.Test;
public class MyTest {
@Rule
public SimpleRunListener simpleRunListener = new SimpleRunListener();
@Test
public void test1() {
// Your test code here
}
@Test
public void test2() {
// Your test code here
}
}
这样,每当测试运行时,SimpleRunListener
都会输出测试的开始和结束信息。
4. 有序执行测试案例
编写 JUnit 有序测试案例被认为是不良做法。 但是,如果仍然遇到测试用例排序是唯一出路的情况,则可以使用MethodSorters
类
在JUnit 4中,@FixMethodOrder
是一个注解,它确保测试方法按照指定的顺序执行。默认情况下,JUnit 可能会随机化测试方法的执行顺序,以避免依赖测试执行顺序的问题。但是,有些情况下,你可能需要确保测试按照特定的顺序运行,比如当测试方法之间存在依赖关系时。
要使用 @FixMethodOrder
,首先需要导入相应的包:
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;
然后,你可以在测试类上使用 @FixMethodOrder
注解,并指定排序策略。JUnit 4 提供了 MethodSorters
工具类,其中包含了几种预定义的排序方式:
MethodSorters.NAME_ASCENDING
:按照方法名称的字典顺序升序排列。MethodSorters.NAME_DESCENDING
:按照方法名称的字典顺序降序排列。- 等等。
下面是一个使用 @FixMethodOrder
的例子:
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class MyTestClass {
@Test
public void test1() {
// 测试逻辑
}
@Test
public void test2() {
// 测试逻辑
}
@Test
public void test3() {
// 测试逻辑
}
}
在这个例子中,即使测试方法的名称可能不会决定它们的执行顺序,@FixMethodOrder
注解确保了 test1
、test2
和 test3
将按照它们在类中出现的顺序执行。
请注意,依赖测试执行顺序的做法通常不是一个好习惯,因为它可能导致测试变得脆弱和难以维护。在大多数情况下,你应该尽量编写独立且不依赖于其他测试的测试用例。
5. 创建临时文件夹
在JUnit 4中,TemporaryFolder
是一个JUnit的Rule
,它提供了创建和删除临时文件夹和文件的便捷方式,这在需要测试文件和文件夹操作的单元测试中非常有用。TemporaryFolder
可以确保测试运行结束后,所有的临时文件和文件夹都被清理干净,避免测试之间的相互干扰。
以下是如何在JUnit 4测试中使用TemporaryFolder
的基本步骤:
-
首先,确保你的项目中包含了JUnit依赖。如果你使用的是Maven或Gradle,确保你的
pom.xml
或build.gradle
文件中包含了JUnit的依赖项。 -
导入
TemporaryFolder
类和JUnit的Rule
注解:
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
- 在测试类中,使用
@Rule
注解声明一个TemporaryFolder
的实例:
public class TemporaryFolderExampleTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
}
- 在测试方法中,使用
TemporaryFolder
的实例来创建临时文件和文件夹:
import org.junit.Rule;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
public class TemporaryFolderExampleTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void testUsingTemporaryFolder() throws IOException {
// 创建一个临时文件夹
File tempFolder = folder.newFolder();
System.out.println("Temporary folder created at: " + tempFolder.getAbsolutePath());
// 在临时文件夹中创建一个文件
File tempFile = folder.newFile("test.txt");
System.out.println("Temporary file created at: " + tempFile.getAbsolutePath());
// 执行测试逻辑,例如写入文件等
// 测试结束后,TemporaryFolder Rule会自动删除这些临时文件和文件夹
}
}
在上述示例中,TemporaryFolder
会在测试方法执行前创建一个新的临时文件夹和文件。测试完成后,不管测试是否通过,TemporaryFolder
都会删除这些临时文件和文件夹,确保测试环境的整洁。
使用TemporaryFolder
可以帮助你编写更干净、更可重复的测试,特别是在测试需要文件系统操作的代码时。