【问题标题】:Check that JUnit Extension throws specific Exception检查 JUnit 扩展是否抛出特定异常
【发布时间】:2018-04-24 13:23:44
【问题描述】:

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

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?

【问题讨论】:

  • 这个扩展只是一个例子,真正的扩展是特定领域的。我的问题更多是关于如何测试我的扩展。
  • 我在stackoverflow.com/questions/46841243/… 中提出了类似的问题,我希望不久的将来可以为扩展开发一个测试工具。

标签: java junit junit5 junit5-extension-model


【解决方案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】:

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

    【讨论】:

      【解决方案3】:

      在尝试了答案中的解决方案和 cmets 中链接的问题后,我最终使用 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 插件来执行测试。这个插件有一个default configuration,它排除所有嵌套类,比如MyTest。

      另请参阅this answer,了解“通过 Maven 从内部类运行测试”以及 cmets 中的相关问题。

      【讨论】:

      • 其实JUnit运行MyTest应该没有问题。它实际上是一个静态 nested 类(受支持)而不是 inner 类。
      • 所以MyTest在你的构建过程中没有被执行一定有其他原因。
      • @SamBrannen 我刚刚用一个新的 Maven 项目和mvn test 验证了只执行了DisallowUppercaseLetterAtBeginningTest 而不是MyTest。你会说这是一个错误?我应该对我的示例项目提出问题吗?
      • 是的,我会说这是一个错误。 Gradle、IntelliJ IDEA 和 Eclipse 都将执行该静态嵌套测试类。请打开一个问题并在此处提供链接。谢谢!
      • 请注意这个bug可能和这个有关:github.com/junit-team/junit5/issues/1343
      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 2011-09-21
        • 2013-08-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-06-11
        • 2015-04-16
        相关资源
        最近更新 更多