5

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

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

4 回答 4

2

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

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

请注意,这实际上是 JUnit 5 的测试方式,我建议您参考Github 上的 Jupiter 引擎的单元测试

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 资源可以成为很好的灵感来源。

于 2017-11-11T14:18:25.683 回答
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,而对于其他扩展,可以通过断言其副作用来测试扩展。

于 2017-11-11T13:59:52.710 回答
1

在尝试了答案中的解决方案和评论中链接的问题之后,我最终得到了一个使用 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. 所以在构建过程中没有失败的测试。

更新

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

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

没有执行的原因MyTest是因为我使用了 Maven 并且我用mvn test. Maven 使用 Maven Surefire 插件来执行测试。这个插件有一个默认配置,它排除了所有嵌套类,如MyTest.

另请参阅有关“通过 Maven 从内部类运行测试”的答案以及评论中的链接问题。

于 2017-12-26T18:42:37.363 回答
1

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")
    }
  }
}    
于 2019-02-12T04:12:48.367 回答