JUnit 4 +5 注释摘要,附有示例

在写这篇文章之前,我只知道一些常用的JUnit 4注释,如

@RunWith 
@Test
@Before
@After
@BeforeClass
@AfterClass

你不得不评论多少次测试?令我吃惊的是,有注释可以做到这一点。

@Ignore("Reason for ignoring")
@Disabled("Reason for disabling")

嗯,事实证明,还有其他一些注释,特别是在JUnit 5,可以帮助编写更好和更有效的测试。


期待什么?

在本文中,我将用用例来涵盖以下注释。本文的目的是向您介绍注释,它不会进入每个注释的更大细节。

_本文的所有示例也可在 Github 中找到。请查看以下存储库。_

GitHub logo 哈米迪/朱尼特注释 - 示例

JUnit 4 和 5 注释与示例

本文的目标受众是任何级别的开发人员。

Alt Text

JUnit 4

以下JUnit 4注释将包括

Alt Text

JUnit 5

以下JUnit 5注释用示例进行解释

Alt Text


JUnit依赖

本文中的所有示例都使用以下 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'

有关详细信息,请查看 Github存储库


JUnit注释使用

让我们用简短的使用示例逐一探索 JUnit 4 注释

单位测试的你好世界

注释用于将方法标记为测试。@Test

public class BasicJUnit4Tests {
  @Test
  public void always_passing_test() {
    assertTrue("Always true", true);
  }
}

班级级和测试级注释

注释,如和是JUnit 4类注释。@BeforeClass``````@AfterClass

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.
  }
}

注释为JUnit 5等价物,使用以下语句导入。@BeforeAll``````@AfterAll

// JUnit 5
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.AfterAll

忽略测试与假设

测试被注释忽略,或者断言可以更改为假设,JUnit 亚军将忽略失败的假设。@Ignore

处理服务器与本地时区等场景时会使用假设。当假设失败时,抛出一个,JUnit跑步者将忽略它。AssumptionViolationException

public class BasicJUnit4Tests {
  @Ignore("Ignored because of a good reason")
  @Test
  public void test_something() {
    assertTrue("Always fails", false);
  }
}

按顺序执行测试

一般来说,编写顺序不可知单位测试是一种好的做法。

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class FixedMethodOrderingTests {
  @Test
  public void first() {}
  @Test
  public void second() {}
  @Test
  public void third() {}
}

除了按测试名称的升序排序外,允许和级别排序。MethodSorter``````DEFAULT``````JVM


向测试添加超时

单元测试大多具有快速执行时间:但是,在某些情况下,单位测试可能需要更长的时间。

JUnit 4中,注释接受如下参数@Test``````timeout

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/Before``````@AfterEach/After

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);
  }
}

使用规则与JUnit测试

我发现在编写单元测试时很有帮助。适用于以下规则@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");
  }
}

临时折叠规则

规则在测试的生命周期内为创建和删除文件夹和文件夹提供便利。

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());
 }
}

错误同事规则

在执行单元测试期间,如果有许多断言,而第一个断言失败,则随后的申报将跳过如下所示。

@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
  }
}

还有验证器规则,我不会进入细节,你可以在这里阅读更多关于它。

有关两者的更多信息和区别,请参阅此堆叠溢出帖子。@ClassRule


JUnit套房

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
}

以上将导致以下

Alt Text


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亚军称为用于执行这些测试Categories.class

@RunWith(Categories.class)
@IncludeCategory(CategoryA.class)
@ExcludeCategory(CategoryB.class)
@SuiteClasses({CategoriesTests.class})
public class CategroyTestSuite {}

以上只会运行测试,但是,如果我们删除以下条目,然后两者兼而有之执行。test1``````test1``````test3

@ExcludeCategory(CategoryB.class)

在 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() {}
}

如上所示,标签适用于整个类以及单个方法。让我们执行在给定包中标记的所有测试。qa

@RunWith(JUnitPlatform.class)
@SelectPackages("junit.exmaples.v2.tags")
@IncludeTags("qa")
public class JUnit5TagTests {}

以上将导致以下输出

Alt Text

如上所示,仅运行带有标记的测试类。让我们同时运行和标记测试,但是,过滤和标记测试。qa``````qa``````development``````dao``````report

@RunWith(JUnitPlatform.class)
@SelectPackages("junit.exmaples.v2.tags")
@IncludeTags({"qa", "development"})
@ExcludeTags({"report", "dao"})
public class JUnit5TagTests {}

如下所示,这两个测试注释了并排除在外。dao``````report

Alt Text


参数化单元测试

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跑步者Parametrized.class
  • 声明一种非私有静态方法,该方法返回带有注释的集合@Parameters
  • 用和值属性申报每个参数@Parameter
  • 使用测试中的注释字段@Parameter

以下是我们参数化输出的外观test_isPrime

Alt Text

以上是使用注射,我们也可以实现相同的结果使用构造器,如下图所示。@Parameter

public class JUnit4ConstructorParametrized {
  private int number;
  private boolean expectedResult;

  public JUnit4ConstructorParametrized(int input, boolean result) {
    this.number = input;
    this.expectedResult = result;
  }
  ...
}

在 JUnit 5 中,引入以下来源@ParameterizedTest

  • The @ValueSource
  • The @EnumSource
  • The @MethodSource
  • 和和@CsvSource``````@CsvFileSource

让我们详细探讨每一个。


具有价值源的参数化测试

注释允许以下声明@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);
}

带内源的参数化测试

注释可用于以下方式@EnumSource

@EnumSource(Level.class)
@EnumSource(value = Level.class, names = { "MEDIUM", "HIGH"})
@EnumSource(value = Level.class, mode = Mode.INCLUDE, names = { "MEDIUM", "HIGH"})

类似,我们可以以以下方式使用ValueSource``````EnumSource

@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));
}

带方法源的参数化测试

注释接受提供输入数据的方法名称。提供输入数据的方法可以返回单个参数,或者我们可以使用如下所示@MethodSource``````Arguments

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);
  }
}

下面是一个例子,它也可以用来返回POJOArguments

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.
  }
}

带 CSV 源的参数化测试

当涉及到执行带有 CSV 内容的测试时**,JUnit 5参数化测试**提供了两种不同类型的源

  • A - 逗号分离值CsvSource
  • A - 引用 CSV 文件CsvFileSource

下面是一个示例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

Alt Text

您还可以设计自定义转换器,将CSV转换为对象。有关详细信息,请参阅此处


JUnit4中的理论

注释和亚军理论是实验特征。与参数化测试相比,理论将数据点的所有组合都馈送至测试,如下所示。@Theory

@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¹组合test_theory

Java
node

但是,该测试将输出两个数据点的所有组合。换句话说,22个组合。test_theory_combos

Java - Java
Java - node
node - Java
node - node

如果我们有以下数据点,则测试将生成 2 个*组合(2 个 args ^ 3 数据点数)。该测试将创建 33 组合。oss``````test_theory_one``````test_theory_two

@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 测试显示名

JUnit 5 引入了用于给单个测试或测试类显示名称的注释,如下所示。@DisplayName

@Test
@DisplayName("Test If Given Number is Prime")
public void is_prime_number_test() {}

它会显示如下在控制台

Alt Text

重复JUnit测试

如果需要重复单位测试 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

Alt Text

使用 JUnit 5 嵌套执行内部类单元测试

我很惊讶地得知,JUnit亚军不扫描内部类的测试。

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``````@Nested

Alt Text


JUnit 5 测试生命周期与测试灌输注释

在调用每个方法之前,JUnit 亚军创建了该类的新实例。这种行为可以在帮助下改变@Test``````@TestInstance

@TestInstance(LifeCycle.PER_CLASS)
@TestInstance(LifeCycle.PER_METHOD)

有关更多信息,请查看文档@TestInstance(LifeCycle.PER_CLASS)


使用 JUnit 5 测试工厂注释的动态测试

带有注释的 JUnit 测试是静态测试,因为它们在编译时间中指定。另一方面,在运行期间生成。下面是使用该类的示例。@Test``````DynamicTests``````DynamicTests``````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))));
    }
}

有关动态测试的更多内容,请参阅此博客文章


有条件地执行 JUnit 5 测试

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,输出如下

Alt Text

有关其他注释的示例,请查看这些测试