如何创建不显示在失败跟踪中的自定义 JUnit4 断言

How to create custom JUnit4 assertions that don't show in the Failure Trace

我想向我们的代码库中添加一些自定义断言,以正确隐藏故障跟踪。我知道如何编写可以静态导入的 public 静态方法。我知道如何重用旧断言或抛出新的 AssertionError.

我不知道该怎么做是将新的自定义断言排除在失败跟踪之外。我们习惯于失败跟踪中的第一个命中不是断言代码本身,而是调用断言的测试代码。

我知道有一个 filtertrace 属性可以控制对堆栈的过滤,但我找不到任何好的文档说明我必须做什么才能将新断言添加到过滤器。

我想做的一个例子:

package testassertions;

import static newassertions.MyAssertions.myAssertTrue;

import org.junit.Test;

public class ExampleTest {
    @Test
    public void myAssertTruePassing() { myAssertTrue(true); }

    @Test
    public void myAssertTrueFailing() { myAssertTrue(false); }
}

package newassertions;

import static org.junit.Assert.assertTrue;

public class MyAssertions {

    public static void myAssertTrue(boolean b) {
        assertTrue(b);
    }
}

myAssertTrueFailing() 的失败跟踪显示:

java.lang.AssertionError
    at newassertions.MyAssertions.myAssertTrue(MyAssertions.java:8)
    at testassertions.ExampleTest.myAssertTrueFailing(ExampleTest.java:12)

我只需要它来显示:

java.lang.AssertionError
    at testassertions.ExampleTest.myAssertTrueFailing(ExampleTest.java:12)

您可以将自定义 JUnit 方法规则与自定义断言一起使用。自定义断言可以与 AssertionError 的子类型一起使用。这甚至允许您同时使用 Junit 断言和自定义断言。

例子

这是一个使用自定义 MyAssert class 的示例,它会在断言失败时抛出 MyAssertionError。 JUnit 规则处理 MyAssertionError 并隐藏故障跟踪的任何细节。

public class RuleTest {

  @Rule
  public TestVerifier testVerifier = new TestVerifier();

  @Test
  public void myAssertOk() { MyAssert.assertCondition("ok", true); }

  @Test
  public void myAssertNotOk() { MyAssert.assertCondition("nok", false); }

  @Test
  public void junitAssertNotOk() { assertTrue(false); }

  @Test
  public void junitAssertOk() { assertTrue(true); }

  static class TestVerifier implements TestRule {

    @Override
    public Statement apply(Statement base, Description description) {
      return new Statement() {

        @Override
        public void evaluate() throws Throwable {
          try {
            base.evaluate();
          } catch (MyAssertionError t) {
            throw new AssertionError("Test failed: " + description.getMethodName());
          }
        }
      };
    }

  }

  static class MyAssertionError extends AssertionError {
    public MyAssertionError(Object detailMessage) { super(detailMessage); }
  }

  static final class MyAssert {
    public static void assertCondition(String message, boolean condition) {
      if (!condition) { throw new MyAssertionError(message); }
    }
  }
}

使用此自定义 TestVerifier 规则,您的失败跟踪只会显示:

java.lang.AssertionError: Test failed: verifierTest
    at RuleTest$TestVerifier.apply(RuleTest.java:26)
    at org.junit.rules.RunRules.applyAll(RunRules.java:26)
    ...

在您的 IDE 中它看起来像这样:

您是否考虑过将 org.junit.Assert.assertThatHamcrest 匹配器一起使用?

使用 Hamcrest,您不需要更改断言方法,而是实现您自己的匹配器。例如,要验证 BCrypt 散列密码与普通密码匹配,请编写如下匹配器:

public class MatchesPassword extends TypeSafeMatcher<String> {

    private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();

    private final String password;

    public MatchesPassword(String password) {
        this.password = password;
    }

    @Override
    protected boolean matchesSafely(String encodedPassword) {
        return PASSWORD_ENCODER.matches(password, encodedPassword);
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("matches password ");
        description.appendValue(password);
    }
}

接下来,在可以静态导入的地方添加一个方法:

public class CustomMatchers {

    public static Matcher<String> matchesPassword(String password) {
        return new MatchesPassword(password);
    }

}

最后,像这样编写你的测试:

@Test
public void passwordShouldMatch() {
    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder()
    String plainPassword = "secret";
    String hashedPassword = passwordEncoder.encode(plainPassword);

    assertThat(hashedPassword, matchesPassword(plainPassword));
}

不匹配将像这样记录到控制台:

java.lang.AssertionError: 
Expected: matches password "wrong"
     but: was "alOyLzUeKMAYPJ5A3y5KfOi747DocksLPHgR7GG3XD8pjp8mhaf0m"
    at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:18)
    at org.junit.Assert.assertThat(Assert.java:956)
    at org.junit.Assert.assertThat(Assert.java:923)
    ...

注意:BCryptPasswordEncoder 来自 Spring Security,仅用作示例。

another question about cleaning noise from stack traces 中所述,从 IDE 中过滤 class 可能是最简单的解决方案。事实上,您在问题中显示的堆栈跟踪已被过滤。

如果您真的想在代码中执行此操作,您可以向自定义断言添加过滤 class 如下所示:

package newassertions;

import static org.junit.Assert.assertTrue;
import java.util.ArrayList;

public class MyAssertions {

    public static void myAssertTrue(boolean b) {
        try {
            assertTrue(b);
        } catch (AssertionError e) {
            filterStackTrace(e);
            throw e;
        }
    }

    private static void filterStackTrace(AssertionError error) {
        StackTraceElement[] stackTrace = error.getStackTrace();
        if (null != stackTrace) {
            ArrayList<StackTraceElement> filteredStackTrace = new ArrayList<StackTraceElement>();
            for (StackTraceElement e : stackTrace) {
                if (!"newassertions.MyAssertions".equals(e.getClassName())) {
                    filteredStackTrace.add(e);
                }
            }
            error.setStackTrace(filteredStackTrace.toArray(new StackTraceElement[0]));
        }
    }
}

封闭的名称 class 'newassertions.MyAssertions'(硬编码)在此示例中从堆栈跟踪中过滤掉。这种机制显然也可以从您自己创建的 AssertionError 中过滤堆栈跟踪,而不仅仅是从其他断言中引发的堆栈跟踪。

我的首选解决方案也是 IDE 过滤器,正如其他人已经建议的那样。如果您执行 "hard-coded" 解决方案,这在自动构建过程中将难以追踪。

在 Eclipse 中,您可以打开首选项和 select Java -> JUnit 并使用右侧的按钮添加 classes 或包。

但只是为了好玩:


如果您真的想以编程方式执行此操作,@gar 的解决方案听起来很合理。但是,如果您有更多的断言,这可能会有点乏味。

您还可以做的是子class AssertionError 并在其根部过滤堆栈跟踪。

public class MyAssertionError extends AssertionError {

    public MyAssertionError(String message) {
        super(message);
    }

    @Override
    public synchronized Throwable fillInStackTrace() {
        super.fillInStackTrace();
        filterStackTrace();
        return this;
    }

    protected void filterStackTrace() {
        StackTraceElement[] trace = getStackTrace();
        ArrayList<StackTraceElement> list = new ArrayList<StackTraceElement>(trace.length);
        for (StackTraceElement element : trace) {
            if (!element.getClassName().equals("newassertions.MyAssertions")) {
                list.add(element);
            }
        }
        this.setStackTrace(list.toArray(new StackTraceElement[0]));
    }

}

这里注意两点: 1) StackTraceElement 的 class 名称永远不能为 null,因此将常量写在右侧是可以的 2)如果你把所有的断言放在一个单独的包中,你也可以写 element.getClassName().startsWith("newassertions")

您的断言 class 将如下所示:

package newassertions;

public class MyAssertions {

    public static void myAssertTrue(boolean b) {
        if (!b) {
            fail(null);
        }
    }

    public static void fail(String message) {
        if (message == null) {
            throw new MyAssertionError(message);
        }
        throw new MyAssertionError(message);
    }


}

这样您就无法调用 Assert 中的方法,但是如果您编写更复杂的断言,则无论如何都没有理由这样做。但是,与将所有内容都包装在大的 try-catch 块中相比,它会让您的断言代码更简洁。