检查 JUnit 扩展是否抛出特定异常

Check that JUnit Extension throws specific Exception

假设我开发了一个扩展,它不允许测试方法名称以大写字符开头。

public class DisallowUppercaseLetterAtBeginning implements BeforeEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        char c = context.getRequiredTestMethod().getName().charAt(0);
        if (Character.isUpperCase(c)) {
            throw new RuntimeException("test method names should start with lowercase.");
        }
    }
}

现在我想测试我的扩展是否按预期工作。

@ExtendWith(DisallowUppercaseLetterAtBeginning.class)
class MyTest {

    @Test
    void validTest() {
    }

    @Test
    void TestShouldNotBeCalled() {
        fail("test should have failed before");
    }
}

我如何编写测试来验证执行第二种方法的尝试是否会抛出带有特定消息的 RuntimeException?

如果扩展程序抛出异常,那么 @Test 方法无能为力,因为测试运行器永远不会到达 @Test 方法。在这种情况下,我认为,您必须在 之外测试扩展名 它在正常测试流程中的使用,即让扩展名成为 SUT。 对于您问题中提供的扩展,测试可能是这样的:

@Test
public void willRejectATestMethodHavingANameStartingWithAnUpperCaseLetter() throws NoSuchMethodException {
    ExtensionContext extensionContext = Mockito.mock(ExtensionContext.class);
    Method method = Testable.class.getMethod("MethodNameStartingWithUpperCase");

    Mockito.when(extensionContext.getRequiredTestMethod()).thenReturn(method);

    DisallowUppercaseLetterAtBeginning sut = new DisallowUppercaseLetterAtBeginning();

    RuntimeException actual =
            assertThrows(RuntimeException.class, () -> sut.beforeEach(extensionContext));
    assertThat(actual.getMessage(), is("test method names should start with lowercase."));
}

@Test
public void willAllowTestMethodHavingANameStartingWithAnLowerCaseLetter() throws NoSuchMethodException {
    ExtensionContext extensionContext = Mockito.mock(ExtensionContext.class);
    Method method = Testable.class.getMethod("methodNameStartingWithLowerCase");

    Mockito.when(extensionContext.getRequiredTestMethod()).thenReturn(method);

    DisallowUppercaseLetterAtBeginning sut = new DisallowUppercaseLetterAtBeginning();

    sut.beforeEach(extensionContext);

    // no exception - good enough
}

public class Testable {
    public void MethodNameStartingWithUpperCase() {

    }
    public void methodNameStartingWithLowerCase() {

    }
}

但是,您的问题表明上述扩展名只是一个示例,更笼统地说;如果你的扩展有一个副作用(例如在可寻址上下文中设置一些东西,填充一个系统属性等)那么你的@Test方法可以断言这个副作用存在。例如:

public class SystemPropertyExtension implements BeforeEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        System.setProperty("foo", "bar");
    }
}

@ExtendWith(SystemPropertyExtension.class)
public class SystemPropertyExtensionTest {

    @Test
    public void willSetTheSystemProperty() {
        assertThat(System.getProperty("foo"), is("bar"));
    }
}

这种方法的好处是可以避免以下可能笨拙的设置步骤:创建 ExtensionContext 并使用测试所需的状态填充它,但它可能会以限制测试覆盖率为代价,因为你真的只能测试一个结果。而且,当然,只有当扩展具有可以在使用扩展的测试用例中评估的副作用时才可行。

因此,在实践中,我怀疑您可能需要结合使用这些方法;对于某些扩展,扩展可以是 SUT,对于其他扩展,可以通过断言其副作用来测试扩展。

另一种方法可能是使用新的 JUnit 5 - Jupiter 框架提供的工具。

我把我在 Eclipse Oxygen 上用 Java 1.8 测试的代码放在下面。该代码缺乏优雅和简洁,但有望作为为您的元测试用例构建强大解决方案的基础。

请注意,这实际上是 JUnit 5 的测试方式,我建议您参考 the unit tests of the Jupiter engine on Github

public final class DisallowUppercaseLetterAtBeginningTest { 
    @Test
    void testIt() {
        // Warning here: I checked the test container created below will
        // execute on the same thread as used for this test. We should remain
        // careful though, as the map used here is not thread-safe.
        final Map<String, TestExecutionResult> events = new HashMap<>();

        EngineExecutionListener listener = new EngineExecutionListener() {
            @Override
            public void executionFinished(TestDescriptor descriptor, TestExecutionResult result) {
                if (descriptor.isTest()) {
                    events.put(descriptor.getDisplayName(), result);
                }
                // skip class and container reports
            }

            @Override
            public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) {}
            @Override
            public void executionStarted(TestDescriptor testDescriptor) {}
            @Override
            public void executionSkipped(TestDescriptor testDescriptor, String reason) {}
            @Override
            public void dynamicTestRegistered(TestDescriptor testDescriptor) {}
        };

        // Build our test container and use Jupiter fluent API to launch our test. The following static imports are assumed:
        //
        // import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass
        // import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request

        JupiterTestEngine engine = new JupiterTestEngine();
        LauncherDiscoveryRequest request = request().selectors(selectClass(MyTest.class)).build();
        TestDescriptor td = engine.discover(request, UniqueId.forEngine(engine.getId())); 

        engine.execute(new ExecutionRequest(td, listener, request.getConfigurationParameters()));

        // Bunch of verbose assertions, should be refactored and simplified in real code.
        assertEquals(new HashSet<>(asList("validTest()", "TestShouldNotBeCalled()")), events.keySet());
        assertEquals(Status.SUCCESSFUL, events.get("validTest()").getStatus());
        assertEquals(Status.FAILED, events.get("TestShouldNotBeCalled()").getStatus());

        Throwable t = events.get("TestShouldNotBeCalled()").getThrowable().get();
        assertEquals(RuntimeException.class, t.getClass());
        assertEquals("test method names should start with lowercase.", t.getMessage());
}

虽然有点冗长,但这种方法的一个优点是它不需要模拟并在同一 JUnit 容器中执行测试,稍后将用于实际单元测试。

通过一些清理,可以实现更具可读性的代码。同样,JUnit-Jupiter 资源可以成为很好的灵感来源。

在尝试了答案中的解决方案和评论中链接的问题后,我最终找到了使用 JUnit Platform Launcher 的解决方案。

class DisallowUppercaseLetterAtBeginningTest {

    @Test
    void should_succeed_if_method_name_starts_with_lower_case() {
        TestExecutionSummary summary = runTestMethod(MyTest.class, "validTest");

        assertThat(summary.getTestsSucceededCount()).isEqualTo(1);
    }

    @Test
    void should_fail_if_method_name_starts_with_upper_case() {
        TestExecutionSummary summary = runTestMethod(MyTest.class, "InvalidTest");

        assertThat(summary.getTestsFailedCount()).isEqualTo(1);
        assertThat(summary.getFailures().get(0).getException())
                .isInstanceOf(RuntimeException.class)
                .hasMessage("test method names should start with lowercase.");
    }

    private TestExecutionSummary runTestMethod(Class<?> testClass, String methodName) {
        SummaryGeneratingListener listener = new SummaryGeneratingListener();

        LauncherDiscoveryRequest request = request().selectors(selectMethod(testClass, methodName)).build();
        LauncherFactory.create().execute(request, listener);

        return listener.getSummary();
    }

    @ExtendWith(DisallowUppercaseLetterAtBeginning.class)
    static class MyTest {

        @Test
        void validTest() {
        }

        @Test
        void InvalidTest() {
            fail("test should have failed before");
        }
    }
}

JUnit 本身不会 运行 MyTest 因为它是一个没有 @Nested 的内部 class。所以在构建过程中没有失败的测试。

更新

JUnit itself will not run MyTest because it is an inner class without @Nested. So there are no failing tests during the build process.

这并不完全正确。 JUnit 本身也会 运行 MyTest,例如如果 "Run All Tests" 在 IDE 或 Gradle 构建中启动。

之所以没有执行MyTest是因为我用的是Maven,我是用mvn test测试的。 Maven 使用 Maven Surefire 插件来执行测试。此插件有一个 default configuration,其中 排除了 所有嵌套的 class,例如 MyTest

另见 this answer 关于 "Run tests from inner classes via Maven" 和评论中的链接问题。

JUnit 5.4 introduced the JUnit Platform Test Kit 允许您执行测试计划并检查结果。

要从 Gradle 获取对它的依赖,它可能看起来像这样:

testImplementation("org.junit.platform:junit-platform-testkit:1.4.0")

使用您的示例,您的扩展测试可能如下所示:

import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.fail
import org.junit.platform.engine.discovery.DiscoverySelectors
import org.junit.platform.testkit.engine.EngineTestKit
import org.junit.platform.testkit.engine.EventConditions
import org.junit.platform.testkit.engine.TestExecutionResultConditions

internal class DisallowUpperCaseExtensionTest {
  @Test
  internal fun `succeed if starts with lower case`() {
    val results = EngineTestKit
        .engine("junit-jupiter")
        .selectors(
            DiscoverySelectors.selectMethod(ExampleTest::class.java, "validTest")
        )
        .execute()

      results.tests().assertStatistics { stats ->
          stats.finished(1)
        }
  }

  @Test
  internal fun `fail if starts with upper case`() {
    val results = EngineTestKit
        .engine("junit-jupiter")
        .selectors(
            DiscoverySelectors.selectMethod(ExampleTest::class.java, "TestShouldNotBeCalled")
        )
        .execute()

    results.tests().assertThatEvents()
        .haveExactly(
            1,
            EventConditions.finishedWithFailure(
                TestExecutionResultConditions.instanceOf(java.lang.RuntimeException::class.java),
                TestExecutionResultConditions.message("test method names should start with lowercase.")
            )
        )

  }

  @ExtendWith(DisallowUppercaseLetterAtBeginning::class)
  internal class ExampleTest {
    @Test
    fun validTest() {
    }

    @Test
    fun TestShouldNotBeCalled() {
      fail("test should have failed before")
    }
  }
}