Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'd like to add some custom assertions to our code base that properly hide from the failure trace. I know how to write a public static method that someone can statically import. I know how to reuse old assertions or throw a new AssertionError.

What I can't figure out how to do is keep the new custom assertions out of the Failure Trace. We're used to the first hit in the failure trace NOT being the assertion code itself but the test code that called the assertion.

I know there is a filtertrace attribute that controls filtering the stack but I can't find any good documentation of what I'd have to do to add the new assertions to the filter.

An example of what I want to do:

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

Failure Trace of myAssertTrueFailing() shows:

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

I need it to only show:

java.lang.AssertionError
    at testassertions.ExampleTest.myAssertTrueFailing(ExampleTest.java:12)
like image 741
candied_orange Avatar asked Jul 14 '15 04:07

candied_orange


2 Answers

As mentioned in another question about cleaning noise from stack traces, filtering classes from within your IDE is probably the easiest solution. In fact, the stack traces you've shown in your question are already filtered.

If you really wanted to do this in code, you could add filtering to your custom assertion class something like below:

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]));
        }
    }
}

The name of the enclosing class 'newassertions.MyAssertions' (hard-coded) is filtered from the stack trace in this example. This mechanism would obviously also work to filter the stack trace from an AssertionError that you create yourself and not just those raised from other assertions.

like image 163
gar Avatar answered Oct 19 '22 12:10

gar


Have you considered using org.junit.Assert.assertThat with Hamcrest matchers?

With Hamcrest, you wouldn't need to change the assertion methods, but instead implement your own matchers. For example, to verify a BCrypt-hashed password matches the plain password, write a matcher like this:

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

Next, add a method somewhere that you can statically import:

public class CustomMatchers {

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

}

Finally, write your test like this:

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

    assertThat(hashedPassword, matchesPassword(plainPassword));
}

A mismatch will be logged to the console like this:

java.lang.AssertionError: 
Expected: matches password "wrong"
     but: was "$2a$10$5lOyLzUeKMAYPJ5A3y5KfOi747DocksLPHgR7GG3XD8pjp8mhaf0m"
    at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:18)
    at org.junit.Assert.assertThat(Assert.java:956)
    at org.junit.Assert.assertThat(Assert.java:923)
    ...

Note: BCryptPasswordEncoder is from Spring Security and just used as an example.

like image 4
hzpz Avatar answered Oct 19 '22 14:10

hzpz