001/*
002 * Copyright © 2018. Sir Wellington.
003 * Licensed under the Apache License, Version 2.0 (the "License");
004 * you may not use this file except in compliance with the License.
005 *
006 * You may obtain a copy of the License at
007 *     http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software
010 * distributed under the License is distributed on an "AS IS" BASIS,
011 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 * See the License for the specific language governing permissions and
013 * limitations under the License.
014 */
015package tech.sirwellington.alchemy.test.junit;
016
017import tech.sirwellington.alchemy.annotations.designs.FluidAPIDesign;
018
019import static org.hamcrest.Matchers.*;
020import static org.junit.Assert.assertThat;
021import static tech.sirwellington.alchemy.test.Checks.Internal.checkNotNull;
022
023/**
024 * Makes it easier syntactically using Java 8 to assert an Exception is thrown by a section of code.
025 * You can also perform additional verification on the exception that is thrown.
026 * <p>
027 * Example:
028 * <p>
029 * <pre>
030 * {@code
031 * assertThrows(() -> someFunctionThatThrows())
032 * .isIntanceOf(RuntimeException.class)
033 * .hasNoCause();
034 * }
035 * </pre>
036 *
037 * @author SirWellington
038 */
039@FluidAPIDesign
040public final class ThrowableAssertion
041{
042
043    /**
044     * Assert that a function throws an exception.
045     *
046     * @param operation The Lambda function that encapsulates code you expect to throw an exception.
047     * @return
048     * @throws ExceptionNotThrownException If no exception is thrown.
049     */
050    public static ThrowableAssertion assertThrows(ExceptionOperation operation) throws ExceptionNotThrownException
051    {
052        checkNotNull(operation, "missing operation");
053        return new ThrowableAssertion(operation)
054                .execute();
055    }
056
057    private Throwable caught;
058    private final ExceptionOperation operation;
059
060    private ThrowableAssertion(ExceptionOperation operation)
061    {
062        this.operation = operation;
063    }
064
065    private ThrowableAssertion execute() throws ExceptionNotThrownException
066    {
067        try
068        {
069            operation.call();
070        }
071        catch (Throwable ex)
072        {
073            this.caught = ex;
074            return this;
075        }
076        throw new ExceptionNotThrownException("Expected an exception");
077    }
078
079    /**
080     * Check that the Exception is of a particular type.
081     *
082     * @param exceptionClass The expected type of the Exception.
083     * @return
084     */
085    public ThrowableAssertion isInstanceOf(Class<? extends Throwable> exceptionClass)
086    {
087        assertThat(caught, isA((Class<Throwable>) exceptionClass));
088        return this;
089    }
090
091    /**
092     * Checks to make sure the exception contains a certain message.
093     *
094     * @param expectedMessage The exact message expected
095     * @return
096     */
097    public ThrowableAssertion hasMessage(String expectedMessage)
098    {
099        assertThat(caught.getMessage(), is(expectedMessage));
100        return this;
101    }
102
103    /**
104     * Assert that the exception contains a string in its message.
105     *
106     * @param messageString The partial message to expected.
107     * @return
108     */
109    public ThrowableAssertion containsInMessage(String messageString)
110    {
111        assertThat(caught.getMessage(), containsString(messageString));
112        return this;
113    }
114
115    /**
116     * Assert that the exception has no causing exception
117     *
118     * @return
119     */
120    public ThrowableAssertion hasNoCause()
121    {
122        assertThat(caught.getCause(), nullValue());
123        return this;
124    }
125
126    /**
127     * Asserts that the Exception has a cause of a particular type.
128     *
129     * @param exceptionClass The type expected.
130     * @return
131     */
132    public ThrowableAssertion hasCauseInstanceOf(Class<? extends Throwable> exceptionClass)
133    {
134        assertThat(caught.getCause(), notNullValue());
135        assertThat(caught.getCause(), isA((Class<Throwable>) exceptionClass));
136        return this;
137    }
138}