检查JUnit Extension是否抛出特定的Exception

时间:2017-11-11 11:55:05

标签: java junit junit5 junit5-extension-model

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

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?

4 个答案:

答案 0 :(得分:2)

另一种方法可能是使用新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资源可以成为灵感的重要来源。

答案 1 :(得分:1)

如果扩展程序抛出异常,则@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,而对于其他扩展名,扩展名可以通过断言其副作用进行测试。

答案 2 :(得分:0)

在回答中的解决方案和评论中链接的问题后,我最终得到了使用JUnit平台启动器的解决方案。

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的内部类。因此在构建过程中没有失败的测试。

更新

  

JUnit本身不会运行MyTest,因为它是没有@Nested的内部类。因此在构建过程中没有失败的测试。

这不完全正确。 JUnit本身也会运行MyTest,例如如果在IDE中或在Gradle构建中启动“运行所有测试”。

MyTest未执行的原因是因为我使用了Maven并且我使用mvn test对其进行了测试。 Maven使用Maven Surefire插件执行测试。此插件的default configuration 排除所有嵌套类,如MyTest

另见this answer关于“通过Maven从内部类运行测试”以及评论中的链接问题。

答案 3 :(得分:0)

JUnit 5.4引入了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")
    }
  }
}