Hamcrest Custom Exception Matcher

by GarciaPL on Friday 11 August 2017

Sometimes asserting custom exception is difficult. Sometimes it's not, for instance when you are using AssertJ, it's very quite easy to achieve. In this post I would like to highlight how to achieve that using Hamcrest matcher.

Below you might find a BaseExceptionMatcher which can be easily extended for our needs. Let's assume that we have an exception with four properties like errorType, errorCode, status and description.

import com.my.package.ErrorCode;
import com.my.package.ErrorType;
import com.my.package.base.BaseException;
import com.my.package.MessageType;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;

import javax.ws.rs.core.Response;

public abstract class BaseExceptionMatcher extends TypeSafeMatcher<BaseException> {
    protected ErrorType expectedErrorType;
    protected ErrorCode expectedErrorCode;
    protected Response.Status expectedStatus;
    protected String expectedDescription;

    public BaseExceptionMatcher(ErrorCode expectedErrorCode, Response.Status expectedStatus, ErrorType expectedErrorType, String expectedDescription) {
        this.expectedErrorCode = expectedErrorCode;
        this.expectedStatus = expectedStatus;
        this.expectedErrorType = expectedErrorType;
        this.expectedDescription = expectedDescription;
    }

    @Override
    protected boolean matchesSafely(BaseException item) {
        return item.getStatus().equals(expectedStatus) &&
                item.getError().stream().allMatch(p ->
                        p.getType().equals(expectedErrorType.name()) && p.getCode().equals(expectedErrorCode.name())) &&
                item.getError().stream().allMatch(p -> p.getDescription().contains(expectedDescription));
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("Response status : ")
                .appendValue(expectedStatus.getReasonPhrase() + " (" + expectedStatus.getStatusCode() + ")")
                .appendText(" | ErrorType : ").appendValue(expectedErrorType.name())
                .appendText(" | ErrorCode : ").appendValue(expectedErrorCode.name())
                .appendText(" | Description : ").appendValue(expectedDescription);
    }

    @Override
    protected void describeMismatchSafely(BaseException item, Description mismatchDescription) {
        mismatchDescription.appendText("Exception contains response status : ")
                .appendValue(item.getStatus().getReasonPhrase() + "(" + item.getStatus().getStatusCode() + ")")
                .appendText(" | ErrorType : ").appendValue(getMessageType(item).getType())
                .appendText(" | ErrorCode : ").appendValue(getMessageType(item).getCode())
                .appendText(" | Description : ").appendValue(getMessageType(item).getDescription());
    }

    private MessageType getMessageType(BaseException item) {
        return item.getError().stream().findFirst().orElseGet(() -> {
            MessageType messageType = new MessageType();
            messageType.setType(ErrorType.APPLICATION.name());
            messageType.setCode(ErrorCode.INTERNAL_SERVER_ERROR.name());
            messageType.setDescription("Matcher Error");
            return messageType;
        });
    }
}
Now we can create a concrete exception class which might reuse that above abstract class.
import my.package.ErrorCode;
import my.package.ErrorType;
import my.package.BaseExceptionMatcher;

import javax.ws.rs.core.Response;

public class MyExceptionMatcher extends BaseExceptionMatcher {

    public MyExceptionMatcher(ErrorType expectedErrorType,
                                           ErrorCode expectedErrorCode,
                                           Response.Status expectedStatus,
                                           String expectedDescription) {
        super(expectedErrorCode, expectedStatus, expectedErrorType, expectedDescription);
    }

}
So now you are ready to reuse that matcher in JUnit test. Only what you need to do is define special rule in your test as below.
@Rule
public ExpectedException expectedException = ExpectedException.none();
Last thing is just write a test which might throw an exception and reuse exception matcher written above.
public void testFailure() throws MyException {
    expectedException.expect(new MyExceptionMatcher(ErrorType.APPLICATION, ErrorCode.DOCUMENT_NOT_FOUND, Response.Status.NOT_FOUND, "Document not found"));
    //do smth to fail
}