A Summary of JUnit 4 & 5 Annotations with Examples
在撰写本文之前,我只知道一些常用的JUnit 4注释,例如
@RunWith
@Test
@Before
@After
@BeforeClass
@AfterClass
您必须注释掉几次测试? 令我惊讶的是,有注释可以做到这一点。
@Ignore("Reason for ignoring")
@Disabled("Reason for disabling")
好吧,事实证明,还有一些其他注释,尤其是在JUnit 5中,可以帮助编写更好,更有效的测试。
What to expect?
在本文中,我将通过用法示例介绍以下注释。 本文的目的是向您介绍该批注,而不会详细介绍每个批注。
*Github中也提供了本文的所有示例。 请签出以下存储库。 *
rhamedy / junit-annotations-examples
JUnit 4 and 5 Annotations with Examples
本文的目标读者是任何级别的开发人员。
JUnit 4
The following JUnit 4 annotations will be covered
JUnit 5
The following JUnit 5 annotations are explained with examples
JUnit Dependencies
本文中的所有示例均使用以下JUnit依赖项进行了测试。
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
testCompileOnly 'junit:junit:4.12'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.3.1'
Please check out the Github repository for more details.
JUnit Annotations Usage
让我们通过一个简短的用法示例逐一探讨JUnit 4批注
The Hello World of Unit Testing
的@测试批注用于将方法标记为测试。
public class BasicJUnit4Tests {
@Test
public void always_passing_test() {
assertTrue("Always true", true);
}
}
The Class-Level and Test-Level Annotations
诸如@课前和@下课以后是JUnit 4类级注释。
public class BasicJUnit4Tests {
@BeforeClass
public static void setup() {
// Setup resource needed by all tests.
}
@Before
public void beforeEveryTest() {
// This gets executed before each test.
}
@Test
public void always_passing_test() {
assertTrue("Always true", true);
}
@After
public void afterEveryTest() {
// This gets executed after every test.
}
@AfterClass
public static void cleanup() {
// Clean up resource after all are executed.
}
}
批注@BeforeAll和@毕竟是JUnit 5 equivalents和imported using the following statements.
// JUnit 5
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.AfterAll
Ignoring a Test vs. Assumption
测试被忽略@忽视批注或断言可以更改为假设,并且JUnit Runner将忽略失败的假设。
在处理服务器与本地时区之类的方案时会使用假设。 当假设失败时,AssumptionViolationException被抛出,JUnit运行器将忽略它。
public class BasicJUnit4Tests {
@Ignore("Ignored because of a good reason")
@Test
public void test_something() {
assertTrue("Always fails", false);
}
}
Executing Tests in Order
通常,写顺序不可知单元测试是一个好习惯。
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class FixedMethodOrderingTests {
@Test
public void first() {}
@Test
public void second() {}
@Test
public void third() {}
}
除了按测试名称的升序排列外,方法排序允许默认和虚拟机级别排序。
Adding Timeout to Tests
单元测试通常具有快速的执行时间; 但是,在某些情况下,单元测试可能需要更长的时间。
在JUnit 4,@测试批注接受超时参数如下所示
import org.junit.Ignore;
import org.junit.Test;
public class BasicJUnit4Tests {
@Test(timeout = 1)
public void timeout_test() throws InterruptedException {
Thread.sleep(2); // Fails because it took longer than 1 second.
}
}
在JUnit 5,超时发生在声明级别
import static java.time.Duration.ofMillis;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import org.junit.jupiter.api.Test;
public class BasicJUnit5Tests {
@Test
public void test_timeout() {
// Test takes 2 ms, assertion timeout in 1 ms
assertTimeout(ofMillis(1), () -> {
Thread.sleep(2);
});
}
}
有时,在所有测试中应用超时(包括@ BeforeEach /之前和@ AfterEach /之后也一样
public class JUnitGlobalTimeoutRuleTests {
@Rule
public Timeout globalTimeout = new Timeout(2, TimeUnit.SECONDS);
@Test
public void timeout_test() throws InterruptedException {
while(true); // Infinite loop
}
@Test
public void timeout_test_pass() throws InterruptedException {
Thread.sleep(1);
}
}
Using Rule with JUnit Tests
我发现@规则在编写单元测试时非常有帮助。 一种规则适用于以下
- 超时-如上图所示ExpectedException临时文件夹错误收集器验证者
ExpectedException Rule
此规则可用于确保测试抛出预期的异常。 在JUnit 4,我们可以做以下事情
public class BasicJUnit4Tests {
@Test(expected = NullPointerException.class)
public void exception_test() {
throw new IllegalArgumentException(); // Fail. Not NPE.
}
}
在JUnit 5但是,可以通过以下声明来实现上述目的
public class BasicJUnit5Tests {
@Test
public void test_expected_exception() {
Assertions.assertThrows(NumberFormatException.class, () -> {
Integer.parseInt("One"); // Throws NumberFormatException
});
}
}
我们还可以定义一个规则在类级别并在测试中重用
public class JUnitRuleTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void expectedException_inMethodLevel() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Cause of the error");
throw new IllegalArgumentException("Cause of the error");
}
}
TemporaryFolder Rule
这个规则可以在测试的生命周期内创建和删除文件和文件夹。
public class TemporaryFolderRuleTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void testCreatingTemporaryFileFolder() throws IOException {
File file = temporaryFolder.newFile("testFile.txt");
File folder = temporaryFolder.newFolder("testFolder");
String filePath = file.getAbsolutePath();
String folderPath = folder.getAbsolutePath();
File testFile = new File(filePath);
File testFolder = new File(folderPath);
assertTrue(testFile.exists());
assertTrue(testFolder.exists());
assertTrue(testFolder.isDirectory());
}
}
ErrorCollector Rule
在执行单元测试期间,如果有很多断言,而第一个断言失败,则跳过后续声明,如下所示。
@Test
public void reportFirstFailedAssertion() {
assertTrue(false); // Failed assertion. Report. Stop execution.
assertFalse(true); // It's never executed.
}
如果我们可以获取所有失败的断言的列表并立即而不是一个接一个地修复它们,将很有帮助。 这是错误收集器规则可以帮助实现这一目标。
public class ErrorCollectorRuleTests {
@Rule
public ErrorCollector errorCollector = new ErrorCollector();
@Test
public void reportAllFailedAssertions() {
errorCollector.checkThat(true, is(false)); // Fail. Continue
errorCollector.checkThat(false, is(false)); // Pass. Continue
errorCollector.checkThat(2, equalTo("a")); // Fail. Report all
}
}
There is also the Verifier Rule that I won't go into details, and you can read more about it here.
For more information on @ClassRule
and the difference between the two, please see this Stackoverflow post.
JUnit Suites
JUnit套件可用于对测试类进行分组并一起执行。 这是一个例子
public class TestSuiteA {
@Test
public void testSuiteA() {}
}
public class TestSuiteB {
@Test
public void testSuiteB() {}
}
假设还有许多其他测试类,我们可以使用以下注释运行这两个或其中之一
@RunWith(Suite.class)
@Suite.SuiteClasses({TestSuiteA.class, TestSuiteB.class})
public class TestSuite {
// Will run tests from TestSuiteA and TestSuiteB classes
}
以上将导致以下结果
Categories in JUnit 4
在JUnit 4,我们可以利用分类目录从执行中包括和排除一组测试。 我们可以使用标记界面创建任意数量的类别,如下所示
没有实现的接口称为标记器接口。
public interface CategoryA {}
public interface CategoryB {}
现在我们有两个类别,我们可以用一个或多个类别类型注释每个测试,如下所示
public class CategoriesTests {
@Test
public void test_categoryNone() {
System.out.println("Test without any category");
assert(false);
}
@Category(CategoryA.class)
@Test
public void test1() {
System.out.println("Runs when category A is selected.");
assert(true);
}
@Category(CategoryB.class)
@Test
public void test2() {
System.out.println("Runs when category B is included.");
assert(false);
}
@Category({CategoryA.class, CategoryB.class})
@Test
public void test3() {
System.out.println("Runs when either of category is included.");
assert(true);
}
}
一个特殊的JUnit Runner叫做Categories.class用于执行这些测试
@RunWith(Categories.class)
@IncludeCategory(CategoryA.class)
@ExcludeCategory(CategoryB.class)
@SuiteClasses({CategoriesTests.class})
public class CategroyTestSuite {}
上面只会运行测试测试1,但是,如果我们删除以下条目,则两者测试1和测试3被执行。
@ExcludeCategory(CategoryB.class)
Tagging & Filtering Tests in JUnit 5
除了类别中JUnit 4,JUnit 5引入了标记和过滤测试的功能。 假设我们有以下内容
@Tag("development")
public class UnitTests {
@Tag("web-layer")
public void login_controller_test() {}
@Tag("web-layer")
public void logout_controller_test() {}
@Tag("db-layer")
@Tag("dao")
public void user_dao_tests() {}
}
和
@Tag("qa")
public class LoadTests {
@Tag("auth")
@Test
public void login_test() {}
@Tag("auth")
@Test
public void logout_test() {}
@Tag("auth")
@Test
public void forgot_password_test() {}
@Tag("report")
@Test
public void generate_monthly_report() {}
}
如上所示,标签适用于整个类以及单个方法。 让我们执行所有标记为的测试a在给定的包装中。
@RunWith(JUnitPlatform.class)
@SelectPackages("junit.exmaples.v2.tags")
@IncludeTags("qa")
public class JUnit5TagTests {}
以上将导致以下输出
如上所示,只有测试类具有a标记已运行。 让我们都运行a和发展标记测试,但过滤道和报告标记的测试。
@RunWith(JUnitPlatform.class)
@SelectPackages("junit.exmaples.v2.tags")
@IncludeTags({"qa", "development"})
@ExcludeTags({"report", "dao"})
public class JUnit5TagTests {}
如下所示,这两个测试分别带有道和报告被排除在外。
Parametrizing Unit Tests
JUnit允许参数化测试使用不同的参数执行,而不是使用不同的参数多次复制/粘贴测试或构建自定义实用程序方法。
@RunWith(Parameterized.class)
public class JUnit4ParametrizedAnnotationTests {
@Parameter(value = 0)
public int number;
@Parameter(value = 1)
public boolean expectedResult;
// Must be static and return collection.
@Parameters(name = "{0} is a Prime? {1}")
public static Collection<Object[]> testData() {
return Arrays.asList(new Object[][] {
{1, false}, {2, true}, {7, true}, {12, false}
});
}
@Test
public void test_isPrime() {
PrimeNumberUtil util = new PrimeNumberUtil();
assertSame(util.isPrime(number), expectedResult);
}
}
参数化test_isPrime测试我们需要以下
- 利用专业参数化类JUnit运行器声明一个非私有静态方法,该方法返回带有注释的Collection@参数声明每个参数@参数和值属性利用@参数测试中带注释的字段
这是我们参数化的输出test_isPrime看起来像
上面是一个使用@参数注入,我们还可以使用构造函数获得相同的结果,如下所示。
public class JUnit4ConstructorParametrized {
private int number;
private boolean expectedResult;
public JUnit4ConstructorParametrized(int input, boolean result) {
this.number = input;
this.expectedResult = result;
}
...
}
在JUnit 5中,@ParameterizedTest介绍以下来源
- 的@ValueSource的@EnumSource的@MethodSource的@CsvSource和@CsvFileSource
让我们详细探讨它们中的每一个。
Parameterized Tests with a ValueSource
的@ValueSource批注允许以下声明
@ValueSource(strings = {"Hi", "How", "Are", "You?"})
@ValueSource(ints = {10, 20, 30})
@ValueSource(longs = {1L, 2L, 3L})
@ValueSource(doubles = {1.1, 1.2, 1.3})
让我们在测试中使用以上之一
@ParameterizedTest
@ValueSource(strings = {"Hi", "How", "Are", "You?"})
public void testStrings(String arg) {
assertTrue(arg.length() <= 4);
}
Parameterized Tests with an EnumSource
的@EnumSource注释可以通过以下方式使用
@EnumSource(Level.class)
@EnumSource(value = Level.class, names = { "MEDIUM", "HIGH"})
@EnumSource(value = Level.class, mode = Mode.INCLUDE, names = { "MEDIUM", "HIGH"})
相近价值来源, 我们可以用枚举源以下列方式
@ParameterizedTest
@EnumSource(value = Level.class, mode = Mode.EXCLUDE, names = { "MEDIUM", "HIGH"})
public void testEnums_exclude_Specific(Level level) {
assertTrue(EnumSet.of(Level.MEDIUM, Level.HIGH).contains(level));
}
Parameterized Tests with a MethodSource
的@MethodSource annotation accepts a method name that is providing the input data. 的method that provides input data could return a single parameter, or we could make use of 争论如下所示
public class JUnit5MethodArgumentParametrizedTests {
@ParameterizedTest
@MethodSource("someIntegers")
public void test_MethodSource(Integer s) {
assertTrue(s <= 3);
}
static Collection<Integer> someIntegers() {
return Arrays.asList(1,2,3);
}
}
以下是一个例子争论它也可以用来返回POJO
public class JUnit5MethodArgumentParametrizedTests {
@ParameterizedTest
@MethodSource("argumentsSource")
public void test_MethodSource_withMoreArgs(String month, Integer number) {
switch(number) {
case 1: assertEquals("Jan", month); break;
case 2: assertEquals("Feb", month); break;
case 3: assertEquals("Mar", month); break;
default: assertFalse(true);
}
}
static Collection<Arguments> argumentsSource() {
return Arrays.asList(
Arguments.of("Jan", 1),
Arguments.of("Feb", 2),
Arguments.of("Mar", 3),
Arguments.of("Apr", 4)); // Fail.
}
}
Parameterized Tests with a CSV Sources
在执行包含CSV内容的测试时,JUnit 5提供两种不同类型的来源参数化测试
- 一种CsvSource- 逗号分隔值一种CsvFileSource-对CSV文件的引用
这是一个例子CsvSource
@ParameterizedTest
@CsvSource(delimiter=',', value= {"1,'A'","2,'B'"})
public void test_CSVSource_commaDelimited(int i, String s) {
assertTrue(i < 3);
assertTrue(Arrays.asList("A", "B").contains(s));
}
假设我们在以下条目sample.csv文件下src / test / resources
Name, Age
Josh, 22
James, 19
Jonas, 55
的CsvFileSource情况如下
@ParameterizedTest
@CsvFileSource(resources = "/sample.csv", numLinesToSkip = 1, delimiter = ',', encoding = "UTF-8")
public void test_CSVFileSource(String name, Integer age) {
assertTrue(Arrays.asList("James", "Josh").contains(name));
assertTrue(age < 50);
}
由于最后一个输入,导致2次成功运行测试和1次失败sample.csv断言失败。
You can also design custom converters that would transform a CSV into an object. See here for more details.
Theory in JUnit4
注释@理论和亚军理论是实验功能。 和。。比较参数化测试,一个理论将数据点的所有组合馈送到测试,如下所示。
@RunWith(Theories.class)
public class JUnit4TheoriesTests {
@DataPoint
public static String java = "Java";
@DataPoint
public static String node = "node";
@Theory
public void test_theory(String a) {
System.out.println(a);
}
@Theory
public void test_theory_combos(String a, String b) {
System.out.println(a + " - " + b);
}
}
当执行以上测试类时,测试理论测试将输出2¹组合
Java
node
但是,测试test_theory_combos将输出两个数据点的所有组合。 换句话说,2²组合。
Java - Java
Java - node
node - Java
node - node
如果我们有以下数据点s然后test_theory_one测试将生成2³组合(2个args ^ 3个数据点)。 考试test_theory_two将创建3³组合。
@DataPoints
public static String[] oss = new String[] {"Linux", "macOS", "Windows"};
@Theory
public void test_theory_one(String a, String b) {
System.out.println(a + " - " + b);
}
@Theory
public void test_theory_two(String a, String b, String c) {
System.out.println(a + " <-> " + b + "<->" + c);
}
以下是有效的数据点
@DataPoints
public static Integer[] numbers() {
return new Integer[] {1, 2, 3};
}
JUnit 5 Test DisplayName
JUnit 5引入了@显示名称注释,用于为单个测试或测试类提供显示名称,如下所示。
@Test
@DisplayName("Test If Given Number is Prime")
public void is_prime_number_test() {}
它会在控制台中显示如下
Repeating a JUnit Test
如果需要重复X次单元测试,JUnit 5提供@RepeatedTest注解。
@RepeatedTest(2)
public void test_executed_twice() {
System.out.println("RepeatedTest"); // Prints twice
}
的@RepeatedTest伴随着currentReptition,totalRepetition变量以及TestInfo.java和RepetitionInfo.java对象。
@RepeatedTest(value = 3, name = "{displayName} executed {currentRepetition} of {totalRepetitions}")
@DisplayName("Repeated 3 Times Test")
public void repeated_three_times(TestInfo info) {
assertTrue("display name matches",
info.getDisplayName().contains("Repeated 3 Times Test"));
}
我们也可以使用RepetitionInfo.java找出当前和重复的总数。
Executing Inner Class Unit Tests using JUnit 5's Nested
得知JUnit Runner没有扫描内部类进行测试,我感到很惊讶。
public class JUnit5NestedAnnotationTests {
@Test
public void test_outer_class() {
assertTrue(true);
}
class JUnit5NestedAnnotationTestsNested {
@Test
public void test_inner_class() {
assertFalse(true); // Never executed.
}
}
}
运行上述测试类时,它将仅执行test_outer_class并报告成功但是,当用@嵌套注解,两个测试都运行。
JUnit 5 Test Lifecycle with TestInstance Annotation
分别调用之前@测试方法,JUnit Runner创建该类的新实例。 可以通过以下方式更改此行为:@测试Instance
@TestInstance(LifeCycle.PER_CLASS)
@TestInstance(LifeCycle.PER_METHOD)
For more information on @TestInstance(LifeCycle.PER_CLASS)
please check out the documentation.
DynamicTests using JUnit 5 TestFactory Annotation
带有注释的JUnit测试@测试是静态测试,因为它们是在编译时指定的。 另一方面,动态测试在运行时生成。 这是一个例子动态测试使用PrimeNumberUtil类。
public class Junit5DynamicTests {
@TestFactory
Stream<DynamicTest> dynamicTests() {
PrimeNumberUtil util = new PrimeNumberUtil();
return IntStream.of(3, 7 , 11, 13, 15, 17)
.mapToObj(num -> DynamicTest.dynamicTest("Is " + num + " Prime?", () -> assertTrue(util.isPrime(number))));
}
}
For more on dynamic tests, see this blog post.
Conditionally Executing JUnit 5 Tests
JUnit 5引入了以下注释,以允许有条件地执行测试。
@EnabledOnOs, @DisabledOnOs, @EnabledOnJre, @DisabledOnJre, @EnabledForJreRange, @DisabledForJreRange, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable, @DisabledIfEnvironmentVariable, @EnableIf, @DisableIf
这是使用示例@EnabledOnOs和@DisabledOnOs
public class JUnit5ConditionalTests {
@Test
@DisabledOnOs({OS.WINDOWS, OS.OTHER})
public void test_disabled_on_windows() {
assertTrue(true);
}
@Test
@EnabledOnOs({OS.MAC, OS.LINUX})
public void test_enabled_on_unix() {
assertTrue(true);
}
@Test
@DisabledOnOs(OS.MAC)
public void test_disabled_on_mac() {
assertFalse(false);
}
}
我使用的是MacBook,输出如下
For examples of other annotations, please check out these tests.
Conclusion
感谢您的阅读。 请在评论中分享您的想法,建议和反馈。
Please Feel free to follow me on dev.to for more articles, Twitter, and join my professional network on LinkedIn.
最后,我还撰写了以下文章,可能会对您有所帮助。
关于如何开始为开源做贡献的指南
我认为可以帮助您成为更好的开发人员的主要习惯清单
最后,简短总结一下Java编码最佳实践