All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.robtimus.junit.support.ThrowableAsserter Maven / Gradle / Ivy

Go to download

Contains interfaces and classes that make it easier to write tests with JUnit

There is a newer version: 3.0
Show newest version
/*
 * ThrowableAsserter.java
 * Copyright 2022 Rob Spoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.robtimus.junit.support;

import static com.github.robtimus.junit.support.AssertionFailedErrorBuilder.assertionFailedError;
import static com.github.robtimus.junit.support.ThrowableAssertions.rethrowIfUnrecoverable;
import static com.github.robtimus.junit.support.ThrowableAssertions.unexpectedExceptionTypeThrown;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.function.ThrowingSupplier;
import org.opentest4j.AssertionFailedError;

/**
 * An object that asserts that executing an {@link Executable} or retrieving the value of a {@link ThrowingSupplier} throws an error.
 * 

* This class is like several of the throwing assertions of {@link ThrowableAssertions}, especially the "one-of" assertions, but it provides more * flexibility. It also removes the need for checking the return type of the returned error in case of the "one-of" assertions; instead, a set of * assertions can be configured per expected error type (or none to just specify that the error type is one of the expected error types). *

* This class should be used as follows: *

    *
  1. Call one of the static {@code whenThrows} or {@code whenThrowsExactly} methods to create an instance, and specify the assertions for that error * type using {@code thenAssert} or {@code thenAssertNothing}.
  2. *
  3. Call {@code whenThrows} and {@code whenThrowsExactly} any number of times and in any order, and specify the assertions for that error type * using {@code thenAssert} or {@code thenAssertNothing}.
    * However, all calls must be unique, i.e. you cannot call {@code whenThrows} twice with the same type, or call {@code whenThrowsExactly} twice * with the same type
  4. *
  5. Call {@code whenThrowsNothing} at most once, and specify the assertions for no error using {@code thenAssert} or {@code thenAssertNothing}. *
  6. *
  7. Call {@code execute}. This will execute the {@link Executable} or retrieve the value of the {@link ThrowingSupplier}, and perform the necessary * assertions.
  8. *
  9. Optionally, use the return value of the {@code execute} method to retrieve the error that was thrown or the {@link ThrowingSupplier}'s value. *
  10. *
*

* An example: *


 * whenThrows(UnsupportedOperationException.class, () -> map.computeIfAbsent(key, function)).thenAssertNothing()
 *         .whenThrows(IllegalArgumentException.class).thenAssert(thrown -> assertSame(exception, thrown))
 *         .execute();
 * 
*

* All methods throw a {@link NullPointerException} when provided with {@code null} arguments unless specified otherwise. * * @author Rob Spoor * @param The result type of the code to execute. * @since 2.0 */ @SuppressWarnings("nls") public final class ThrowableAsserter { private static final Consumer DO_NOTHING_CONSUMER = o -> { // do nothing }; private final ThrowingSupplier supplier; private final Map, Consumer> errors; private final Map, Consumer> exactErrors; private final Set> expectedErrorTypes; private Consumer nothingThrownAsserter; private State state; // The following are only set if state is CONFIGURING_ERROR_TYPE private Class configuringErrorType; private boolean configuringExactErrorType; private ThrowableAsserter(ThrowingSupplier supplier) { this.supplier = supplier; errors = new HashMap<>(); exactErrors = new HashMap<>(); expectedErrorTypes = new LinkedHashSet<>(); state = State.INITIALIZED; } private static ThrowingSupplier asThrowableSupplier(Executable executable) { return () -> { executable.execute(); return null; }; } /** * Returns an object for configuring the assertions that should be performed when an instance of a specific error type is thrown when an * {@link Executable} is run. * * @param The error type. * @param errorType The error type. * @param executable The {@link Executable} to run. * @return An object for configuring the assertions that should be performed when an instance of a specific error type is thrown. * @see Assertions#assertThrows(Class, Executable) */ public static ThrownError whenThrows(Class errorType, Executable executable) { return whenThrows(errorType, asThrowableSupplier(executable)); } /** * Returns an object for configuring the assertions that should be performed when an instance of a specific error type is thrown when the result * of a {@link ThrowingSupplier} is retrieved. * * @param The error type. * @param The result type of the {@link ThrowingSupplier}. * @param errorType The error type. * @param supplier The {@link ThrowingSupplier} with the result to retrieve. * @return An object for configuring the assertions that should be performed when an instance of a specific error type is thrown. * @see Assertions#assertThrows(Class, Executable) */ public static ThrownError whenThrows(Class errorType, ThrowingSupplier supplier) { Objects.requireNonNull(supplier); return new ThrowableAsserter<>(supplier).whenThrows(errorType); } /** * Returns an object for configuring the assertions that should be performed when an instance of a specific error type is thrown. * * @param The error type. * @param errorType The error type. * @return An object for configuring the assertions that should be performed when an instance of a specific error type is thrown. * @throws IllegalArgumentException If this method has already been called with the given error type. * @see Assertions#assertThrows(Class, Executable) */ public ThrownError whenThrows(Class errorType) { Objects.requireNonNull(errorType); return whenThrows(errorType, errors, false); } /** * Returns an object for configuring the assertions that should be performed when an exact instance of a specific error type is thrown when an * {@link Executable} is run. * * @param The error type. * @param errorType The error type. * @param executable The {@link Executable} to run. * @return An object for configuring the assertions that should be performed when an exact instance of a specific error type is thrown. * @see Assertions#assertThrowsExactly(Class, Executable) */ public static ThrownError whenThrowsExactly(Class errorType, Executable executable) { return whenThrowsExactly(errorType, asThrowableSupplier(executable)); } /** * Returns an object for configuring the assertions that should be performed when an exact instance of a specific error type is thrown when the * result of a {@link ThrowingSupplier} is retrieved. * * @param The error type. * @param The result type of the {@link ThrowingSupplier}. * @param errorType The error type. * @param supplier The {@link ThrowingSupplier} with the result to retrieve. * @return An object for configuring the assertions that should be performed when an exact instance of a specific error type is thrown. * @see Assertions#assertThrowsExactly(Class, Executable) */ public static ThrownError whenThrowsExactly(Class errorType, ThrowingSupplier supplier) { Objects.requireNonNull(supplier); return new ThrowableAsserter<>(supplier).whenThrowsExactly(errorType); } /** * Returns an object for configuring the assertions that should be performed when an exact instance of a specific error type is thrown. * * @param The error type. * @param errorType The error type. * @return An object for configuring the assertions that should be performed when an exact instance of a specific error type is thrown. * @throws IllegalArgumentException If this method has already been called with the given error type. * @see Assertions#assertThrowsExactly(Class, Executable) */ public ThrownError whenThrowsExactly(Class errorType) { Objects.requireNonNull(errorType); return whenThrows(errorType, exactErrors, true); } private ThrownError whenThrows(Class errorType, Map, Consumer> errorMap, boolean exact) { if (state != State.INITIALIZED && state != State.CONFIGURED) { throw new IllegalStateException("Cannot configure assertions for an error type when current state is " + state); } errorMap.merge(errorType, DO_NOTHING_CONSUMER, (c1, c2) -> { throw new IllegalArgumentException(errorType + " already configured"); }); expectedErrorTypes.add(errorType); state = State.CONFIGURING_ERROR_TYPE; configuringErrorType = errorType; configuringExactErrorType = exact; return new ThrownError<>(this, errorType, errorMap, exact); } /** * Returns an object for configuring the assertions that should be performed if no error is thrown. *

* If this method is not called, {@link #execute()}, {@link #execute(String)} and {@link #execute(Supplier)} will fail if no error is thrown. * * @return An object for configuring the assertions that should be performed if no error is thrown. * @throws IllegalStateException If this method is called without configuring the assertions for at least one error type, * or If this method has already been called. */ public NoError whenThrowsNothing() { if (state != State.CONFIGURED) { throw new IllegalStateException("Cannot configure assertions for no error when current state is " + state); } if (nothingThrownAsserter != null) { throw new IllegalStateException("Assertions for no error already configured"); } nothingThrownAsserter = DO_NOTHING_CONSUMER; state = State.CONFIGURING_NO_ERROR; return new NoError<>(this); } /** * Executes the {@link Executable} or retrieves the value of the {@link ThrowingSupplier} used to create this object, and perform the necessary * assertions. * * @return An object that represents this object in its asserted state. * @throws AssertionFailedError If an error is thrown that is not an instance of one of the configured error types, * or if no error is thrown and {@link #whenThrowsNothing()} has not been called. */ public Asserted execute() { return execute((Object) null); } /** * Executes the {@link Executable} or retrieves the value of the {@link ThrowingSupplier} used to create this object, and perform the necessary * assertions. * * @param message The failure message to fail with; may be {@code null}. * @return An object that represents this object in its asserted state. * @throws AssertionFailedError If an error is thrown that is not an instance of one of the configured error types, * or if no error is thrown and {@link #whenThrowsNothing()} has not been called. */ public Asserted execute(String message) { return execute((Object) message); } /** * Executes the {@link Executable} or retrieves the value of the {@link ThrowingSupplier} used to create this object, and perform the necessary * assertions. * * @param messageSupplier The supplier for the failure message to fail with; may be {@code null}. * @return An object that represents this object in its asserted state. * @throws AssertionFailedError If an error is thrown that is not an instance of one of the configured error types, * or if no error is thrown and {@link #whenThrowsNothing()} has not been called. */ public Asserted execute(Supplier messageSupplier) { return execute((Object) messageSupplier); } private Asserted execute(Object messageOrSupplier) { if (state != State.CONFIGURED) { throw new IllegalStateException("Cannot run assertions when current state is " + state); } R result; try { result = supplier.get(); } catch (Throwable actualError) { runAssertionsForError(actualError, messageOrSupplier); state = State.ASSERTED; return new Asserted<>(actualError); } runAssertionsWhenNothingThrown(result, messageOrSupplier); state = State.ASSERTED; return new Asserted<>(result); } private void runAssertionsWhenNothingThrown(R result, Object messageOrSupplier) throws AssertionFailedError { if (nothingThrownAsserter != null) { nothingThrownAsserter.accept(result); return; } throw assertionFailedError() .message(messageOrSupplier) .reasonPattern("Expected one of %s to be thrown, but nothing was thrown.") .withValues(expectedErrorTypes) .format() .build(); } private void runAssertionsForError(Throwable actualError, Object messageOrSupplier) throws AssertionFailedError { boolean hasRunAssertions = runAllAssertions(actualError); if (hasRunAssertions) { return; } rethrowIfUnrecoverable(actualError); throw unexpectedExceptionTypeThrown() .message(messageOrSupplier) .expectedOneOf(expectedErrorTypes) .actual(actualError.getClass()) .cause(actualError) .build(); } boolean runAllAssertions(Throwable actualError) { Class errorType = actualError.getClass(); boolean hasRunAssertions = false; Consumer asserter = exactErrors.get(errorType); if (asserter != null) { asserter.accept(actualError); hasRunAssertions = true; } Class iterator = errorType; while (iterator != Object.class) { asserter = errors.get(iterator); if (asserter != null) { asserter.accept(actualError); hasRunAssertions = true; } iterator = iterator.getSuperclass(); } return hasRunAssertions; } /** * An object that can be used to configure the assertions that should be performed when an error is thrown. * * @author Rob Spoor * @param The error type. * @param The result type of the code to execute. * @since 2.0 */ public static final class ThrownError { private final ThrowableAsserter throwableAsserter; private final Class errorType; private final Map, Consumer> errorMap; private final boolean exact; private ThrownError(ThrowableAsserter throwableAsserter, Class errorType, Map, Consumer> errorMap, boolean exact) { this.throwableAsserter = throwableAsserter; this.errorType = errorType; this.errorMap = errorMap; this.exact = exact; } /** * Specifies the assertions that should be performed when an error is thrown. * * @param asserter An operation with the assertions that should be performed. The thrown error will be the operation's input. * @return The error asserter that returned this object. * @throws NullPointerException If the given operation is {@code null}. */ @SuppressWarnings("unchecked") public ThrowableAsserter thenAssert(Consumer asserter) { Objects.requireNonNull(asserter); return configureAssertions((Consumer) asserter); } /** * Specifies that no assertions should be performed when an error is thrown. * * @return The error asserter that returned this object. */ public ThrowableAsserter thenAssertNothing() { return configureAssertions(DO_NOTHING_CONSUMER); } private ThrowableAsserter configureAssertions(Consumer asserter) { if (throwableAsserter.state != State.CONFIGURING_ERROR_TYPE) { throw new IllegalStateException("Cannot specify assertions for an error type when current state is " + throwableAsserter.state); } if (errorType != throwableAsserter.configuringErrorType || exact != throwableAsserter.configuringExactErrorType) { throw new IllegalStateException(String.format("Cannot specify assertions; currently configuring for %s (exact: %b)", throwableAsserter.configuringErrorType, throwableAsserter.configuringExactErrorType)); } errorMap.put(errorType, asserter); throwableAsserter.state = State.CONFIGURED; throwableAsserter.configuringErrorType = null; throwableAsserter.configuringExactErrorType = false; return throwableAsserter; } ThrowableAsserter throwableAsserter() { return throwableAsserter; } } /** * An object that can be used to configure the assertions that should be performed when no error is thrown. * * @author Rob Spoor * @param The result type of the code to execute. * @since 2.0 */ public static final class NoError { private final ThrowableAsserter throwableAsserter; private NoError(ThrowableAsserter throwableAsserter) { this.throwableAsserter = throwableAsserter; } /** * Specifies the assertions that should be performed when no error is thrown. * * @param asserter A runnable with the assertions that should be performed. * @return The error asserter that returned this object. * @throws NullPointerException If the given runnable is {@code null}. */ public ThrowableAsserter thenAssert(Runnable asserter) { Objects.requireNonNull(asserter); return configureAssertions(r -> asserter.run()); } /** * Specifies the assertions that should be performed when no error is thrown. * * @param asserter An operation with the assertions that should be performed. The result of the executed code will be the operation's input. * @return The error asserter that returned this object. * @throws NullPointerException If the given operation is {@code null}. */ public ThrowableAsserter thenAssert(Consumer asserter) { Objects.requireNonNull(asserter); return configureAssertions(asserter); } /** * Specifies that no assertions should be performed when no error is thrown. * * @return The error asserter that returned this object. */ public ThrowableAsserter thenAssertNothing() { return configureAssertions(DO_NOTHING_CONSUMER); } private ThrowableAsserter configureAssertions(Consumer asserter) { if (throwableAsserter.state != State.CONFIGURING_NO_ERROR) { throw new IllegalStateException("Cannot specify assertions for no error when current state is " + throwableAsserter.state); } throwableAsserter.nothingThrownAsserter = asserter; throwableAsserter.state = State.CONFIGURED; return throwableAsserter; } ThrowableAsserter throwableAsserter() { return throwableAsserter; } } /** * An object that represents a {@link ThrowableAsserter} in its asserted state. It can be used to query the assertion results. * * @author Rob Spoor * @param The result type. * @since 2.0 */ public static final class Asserted { private final R result; private final Throwable thrown; private Asserted(R result) { this.result = result; this.thrown = null; } private Asserted(Throwable thrown) { this.result = null; this.thrown = thrown; } /** * Returns the result of the executed code. * * @return An {@link Optional} describing the result, or {@link Optional#empty()} if an error was thrown or the result was {@code null}. */ public Optional andReturnResult() { return Optional.ofNullable(result); } /** * Returns the error that was thrown. * * @return The error that was thrown. * @throws IllegalStateException If no error was thrown. */ public Throwable andReturnError() { if (thrown == null) { throw new IllegalStateException("Nothing was thrown"); } return thrown; } /** * Returns the error that was thrown. * * @param The expected type of error. * @param errorType The expected type of error. * This should be a common super type of all configured error types to prevent any {@link ClassCastException}s. * @return The error that was thrown. * @throws IllegalStateException If no error was thrown. * @throws ClassCastException If the error that was thrown is not an instance of the given error type. */ public T andReturnErrorAs(Class errorType) { if (thrown == null) { throw new IllegalStateException("Nothing was thrown"); } return errorType.cast(thrown); } /** * Returns the error that was thrown. * * @return An {@link Optional} describing the error that was thrown, or {@link Optional#empty()} if no error was thrown. */ public Optional andReturnErrorIfThrown() { return Optional.ofNullable(thrown); } /** * Returns the error that was thrown. * * @param The expected type of error. * @param errorType The expected type of error. * This should be a common super type of all configured error types to prevent any {@link ClassCastException}s. * @return An {@link Optional} describing the error that was thrown, or {@link Optional#empty()} if no error was thrown. * @throws ClassCastException If an error was thrown that is not an instance of the given error type. */ public Optional andReturnErrorIfThrownAs(Class errorType) { return thrown == null ? Optional.empty() : Optional.of(errorType.cast(thrown)); } } State state() { return state; } Class configuringErrorType() { return configuringErrorType; } boolean configuringExactErrorType() { return configuringExactErrorType; } enum State { INITIALIZED("initialized"), CONFIGURING_ERROR_TYPE("configuring assertions for an error type"), CONFIGURING_NO_ERROR("configuring assertions for no error"), CONFIGURED("configured"), ASSERTED("asserted"), ; private final String stringValue; State(String stringValue) { this.stringValue = stringValue; } @Override public String toString() { return stringValue; } } }