Skip to content

Commit

Permalink
FEATURE assertj#1652 : assert return value of callable throwing excep…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
hosuaby committed Oct 2, 2020
1 parent ca806ce commit c378777
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 0 deletions.
74 changes: 74 additions & 0 deletions src/main/java/org/assertj/core/api/AbstractTryAssert.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.
*
* Copyright 2012-2020 the original author or authors.
*/
package org.assertj.core.api;

public abstract class AbstractTryAssert<SELF extends AbstractTryAssert<SELF, ACTUAL, EX>, ACTUAL, EX extends Throwable>
extends AbstractThrowableAssert<SELF, EX> {
private final Try<ACTUAL, EX> attempt;

protected AbstractTryAssert(Try<ACTUAL, EX> attempt, Class<?> selfType) {
super(attempt.failure, selfType);
this.attempt = attempt;
}

/**
* Verifies that {@link ThrowingReturningCallable} didn't raise a throwable and returns a result that can be verified
* with subsequent calls of methods of {@link ObjectAssert}.
*
* <p>Example :
*
* <pre><code class='java'>
* assertThatReturningCode(() -&gt; 42)
* .doesNotThrowAnyExceptionAndReturns()
* .isEqualTo(42);
* </code></pre>
*
* @return the assertions of result
* @throws AssertionError if the actual statement raised a {@code Throwable}.
*/
public ObjectAssert<ACTUAL> doesNotThrowAnyExceptionAndReturns() {
doesNotThrowAnyException();
return AssertionsForClassTypes.assertThat(attempt.result);
}

public interface ThrowingReturningCallable<V> {
V call() throws Throwable;
}

public static final class Try<V, EX extends Throwable> {
private final V result;
private final EX failure;

private Try(V result, EX failure) {
this.result = result;
this.failure = failure;
}

@SuppressWarnings("unchecked")
static <VALUE, EX extends Throwable> Try<VALUE, EX> tryCallable(ThrowingReturningCallable<VALUE> callable) {
try {
return succeed(callable.call());
} catch (Throwable failure) {
return (Try<VALUE, EX>) failed(failure);
}
}

public static <VALUE, EX extends Throwable> Try<VALUE, EX> succeed(VALUE result) {
return new Try<>(result, null);
}

public static <VALUE, EX extends Throwable> Try<VALUE, EX> failed(EX failure) {
return new Try<>(null, failure);
}
}
}
33 changes: 33 additions & 0 deletions src/main/java/org/assertj/core/api/Assertions.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import java.util.stream.Stream;

import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.assertj.core.api.AbstractTryAssert.ThrowingReturningCallable;
import org.assertj.core.api.filter.FilterOperator;
import org.assertj.core.api.filter.Filters;
import org.assertj.core.api.filter.InFilter;
Expand Down Expand Up @@ -1255,6 +1256,38 @@ public static <VALUE> AtomicStampedReferenceAssert<VALUE> assertThat(AtomicStamp
return AssertionsForClassTypes.assertThatCode(shouldRaiseOrNotThrowable);
}

/**
* Allows to test the code that ether can return a result or raise an exception.
*
* <p>
* Example :
* <pre><code class='java'>
* ThrowingReturningCallable&lt;Integer&gt; succeedCode = () -&gt; 42;
* ThrowingReturningCallable&lt;?&gt; boomCode = () -&gt; {
* throw new Exception("boom!");
* };
*
* // assertions succeed
* assertThatReturningCode(succeedCode).doesNotThrowAnyExceptionAndReturns()
* .isEqualTo(42);
* assertThatReturningCode(boomCode).isInstanceOf(Exception.class)
* .hasMessageContaining("boom");
*
* // assertion fails
* assertThatReturningCode(succeedCode).isInstanceOf(Exception.class);
* assertThatReturningCode(boomCode)
* </code></pre>
*
* @param returningCallable The {@link ThrowingReturningCallable} or lambda that can ether to return result or raise
* the throwable.
* @param <VALUE> The type of retuned value
* @param <EX> The type of raised exception
* @return the created {@link TryAssert}
*/
public static <VALUE, EX extends Throwable> AbstractTryAssert<?, VALUE, EX> assertThatReturningCode(ThrowingReturningCallable<VALUE> returningCallable) {
return TryAssert.assertThatCode(returningCallable);
}

/**
* Creates a new instance of <code>{@link ObjectAssert}</code> for any object.
*
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/org/assertj/core/api/TryAssert.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.
*
* Copyright 2012-2020 the original author or authors.
*/
package org.assertj.core.api;

public class TryAssert<ACTUAL, EX extends Throwable> extends AbstractTryAssert<TryAssert<ACTUAL, EX>, ACTUAL, EX> {
protected TryAssert(Try<ACTUAL, EX> attempt) {
super(attempt, TryAssert.class);
}

static <V, EX extends Throwable> TryAssert<V, EX> assertThatCode(ThrowingReturningCallable<V> callable) {
return new TryAssert<>(Try.tryCallable(callable));
}
}
33 changes: 33 additions & 0 deletions src/main/java/org/assertj/core/api/WithAssertions.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import java.util.stream.Stream;

import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.assertj.core.api.AbstractTryAssert.ThrowingReturningCallable;
import org.assertj.core.api.filter.FilterOperator;
import org.assertj.core.api.filter.Filters;
import org.assertj.core.api.filter.InFilter;
Expand Down Expand Up @@ -2511,6 +2512,38 @@ default AbstractOffsetDateTimeAssert<?> assertThat(final OffsetDateTime offsetDa
return assertThat(catchThrowable(shouldRaiseOrNotThrowable));
}

/**
* Allows to test the code that ether can return a result or raise an exception.
*
* <p>
* Example :
* <pre><code class='java'>
* ThrowingReturningCallable&lt;Integer&gt; succeedCode = () -&gt; 42;
* ThrowingReturningCallable&lt;?&gt; boomCode = () -&gt; {
* throw new Exception("boom!");
* };
*
* // assertions succeed
* assertThatReturningCode(succeedCode).doesNotThrowAnyExceptionAndReturns()
* .isEqualTo(42);
* assertThatReturningCode(boomCode).isInstanceOf(Exception.class)
* .hasMessageContaining("boom");
*
* // assertion fails
* assertThatReturningCode(succeedCode).isInstanceOf(Exception.class);
* assertThatReturningCode(boomCode)
* </code></pre>
*
* @param returningCallable The {@link ThrowingReturningCallable} or lambda that can ether to return result or raise
* the throwable.
* @param <VALUE> The type of retuned value
* @param <EX> The type of raised exception
* @return the created {@link TryAssert}
*/
default <VALUE, EX extends Throwable> AbstractTryAssert<?, VALUE, EX> assertThatReturningCode(ThrowingReturningCallable<VALUE> returningCallable) {
return TryAssert.assertThatCode(returningCallable);
}

/**
* Creates a new instance of <code>{@link ObjectAssert}</code> for any object.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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.
*
* Copyright 2012-2020 the original author or authors.
*/
package org.assertj.core.api;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatReturningCode;
import static org.assertj.core.error.ShouldNotHaveThrown.shouldNotHaveThrown;

import org.junit.jupiter.api.Test;

public class Assertions_assertThatReturningCode_Test {

@Test
void can_invoke_late_assertion_on_assertThatReturningCode() {
// Given
AbstractTryAssert.ThrowingReturningCallable<?> failedCallable = failedReturningCallable("boom");

// Then
assertThatReturningCode(failedCallable)
.isInstanceOf(Exception.class)
.hasMessageContaining("boom");
}

@Test
void should_fail_when_asserting_no_exception_raised_but_exception_occurs() {
// Given
Exception exception = new Exception("boom");
AbstractTryAssert.ThrowingReturningCallable<?> failedCallable = failedReturningCallable(exception);

// Expect
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->{
// When;
assertThatReturningCode(failedCallable).doesNotThrowAnyException();
}).withMessage(shouldNotHaveThrown(exception).create());
}

@Test
void can_use_description_in_error_message() {
// Given
AbstractTryAssert.ThrowingReturningCallable<?> failedCallable = failedReturningCallable("boom");

// Expect
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThatReturningCode(failedCallable)
.as("Test")
.doesNotThrowAnyException())
.withMessageStartingWith("[Test]");
}

@Test
void error_message_contains_stacktrace() {
// Given
AbstractTryAssert.ThrowingReturningCallable<?> failedCallable = failedReturningCallable("boom");

// Then
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThatReturningCode(failedCallable).doesNotThrowAnyException())
.withMessageContaining("java.lang.Exception: %s", "boom")
.withMessageContaining("at org.assertj.core.api.Assertions_assertThatReturningCode_Test.error_message_contains_stacktrace");
}

@Test
void should_succeed_and_return_when_asserting_no_exception_raised_and_no_exception_occurs() {
// Given
AbstractTryAssert.ThrowingReturningCallable<Integer> succeed = succeedReturningCallable();

// Then
assertThatReturningCode(succeed)
.doesNotThrowAnyExceptionAndReturns()
.isNotNull()
.isEqualTo(42);

assertThatReturningCode(succeed)
.doesNotThrowAnyException();
}

@Test
void should_fail_when_asserting_exception_raised_but_no_exception_occurs() {
// Given
AbstractTryAssert.ThrowingReturningCallable<Integer> succeed = succeedReturningCallable();

// Then
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThatReturningCode(succeed).isInstanceOf(Exception.class));
}

private AbstractTryAssert.ThrowingReturningCallable<Integer> succeedReturningCallable() {
return () -> 42;
}

public AbstractTryAssert.ThrowingReturningCallable<Integer> failedReturningCallable(String reason) {
return failedReturningCallable(new Exception(reason));
}

private AbstractTryAssert.ThrowingReturningCallable<Integer> failedReturningCallable(Throwable exception) {
return () -> {
throw exception;
};
}
}

0 comments on commit c378777

Please sign in to comment.