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

com.github.lpandzic.junit.bdd.Bdd Maven / Gradle / Ivy

package com.github.lpandzic.junit.bdd;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import java.util.Optional;

/**
 * The Bdd is a {@link TestRule JUnit test rule} which which provides a simple and fluent API that gives you a way of
 * structuring your test code within when and then blocks used in Behavior-driven development.
 *
 * 

Testing with JUnit-BDD starts by defining a JUnit test class that contains following rule definition: *

 * import com.github.lpandzic.junit.bdd.Bdd;
 * import static com.github.lpandzic.junit.bdd.Bdd.when;
 *
 * @Rule
 * public Bdd bdd = Bdd.initialized();
 * 
* *

Return value assertion

* For a given class {@code DeathStar} that contains method with signature {@code Target fireAt(Target target) throws * TargetAlreadyDestroyedException} where {@code TargetAlreadyDestroyedException} is a checked exception, * we can do the following value assertion: *
{@code
 * when(deathStar.fireAt(alderaan)).then(target -> {
 *     assertThat(target.isDestroyed(), is(true));
 *     assertThat(target, is(alderaan));
 *     assertThat(target, is(not(coruscant)));
 * });
 * }
* *

Thrown exception assertion

* In order to catch exception for an assertion we pass a lambda to the when block: *
{@code
 * when(deathStar.fireAt(alderaan));
 * when(() -> deathStar.fireAt(alderaan)).then(thrownException -> {
 *     assertThat(thrownException, is(instanceOf(TargetAlreadyDestroyedException.class)));
 *     assertThat(thrownException.getMessage(), is(equalTo("Cannot fire at a destroyed " + alderaan)));
 * });
 * }
* *

Thrown checked exceptions assertion

* If we decide to change the {@code fireAt} method so that it doesn't throw the {@code * TargetAlreadyDestroyedException} the test mentioned in previous sub chapter will fail, * but it will still compile. Since {@code TargetAlreadyDestroyedException} is a checked exception we can use * Generics to prevent that test from compiling and reduce the time required to detect the error! To use this feature * change {@code then} to {@code thenChecked} and use {@code isA} matcher: *
{@code
 * when(deathStar.fireAt(alderaan));
 * when(() -> deathStar.fireAt(alderaan)).thenChecked(thrownException -> {
 *     assertThat(thrownException, isA(TargetAlreadyDestroyedException.class));
 *     assertThat(thrownException.getMessage(), is(equalTo("Cannot fire at a destroyed " + alderaan)));
 * });
 * }
*

Now if we decide to change the signature of fireAt not to include TargetAlreadyDestroyedException we get a * compilation error.

* *

Assertion framework flexibility

* Although Hamcrest was used in previous examples you are free to use any Java assertion framework. For example, * the first testing example can be translated to: *
    *
  • plain JUnit assertions *
      *
    1. Return value assertion *
      {@code
       * when(deathStar.fireAt(alderaan)).then(target -> {
       *     assertTrue(target.isDestroyed());
       *     assertEquals(target, alderaan);
       *     assertNotEquals(target, coruscant);
       * });
       * }
    2. *
    3. Thrown exception assertion *
      {@code
       * when(deathStar.fireAt(alderaan));
       * when(() -> deathStar.fireAt(alderaan)).then(thrownException -> {
       *     assertEquals(TargetAlreadyDestroyedException.class, thrownException.getClass());
       *     assertEquals("Cannot fire at a destroyed " + alderaan, thrownException.getMessage());
       * });
       * }
    4. *
    *
  • AssertJ *
      *
    1. Return value assertion *
      {@code
       * when(deathStar.fireAt(alderaan)).then(target -> {
       *     assertThat(target.isDestroyed()).isTrue();
       *     assertThat(target).isEqualTo(alderaan);
       *     assertThat(target).isNotEqualTo(coruscant);
       * });
       * }
    2. *
    3. Thrown exception assertion *
      {@code
       * when(deathStar.fireAt(alderaan));
       * when(() -> deathStar.fireAt(alderaan)).then(thrownException -> {
       *     assertThat(thrownException).isExactlyInstanceOf(TargetAlreadyDestroyedException.class);
       *     assertThat(thrownException.getMessage()).isEqualTo("Cannot fire at a destroyed " + alderaan);
       * });
       * }
    4. *
    *
  • *
* * @author Lovro Pandzic * @see Introducing Bdd * @see GivenWhenThen article by M. Fowler */ public final class Bdd implements TestRule { /** * Used for specifying behavior that should throw an exception. * * @param throwableSupplier supplier or throwable * @param the type of * * @return new {@link Then.Throws} */ public static Then.Throws when(ThrowableSupplier throwableSupplier) { Bdd bdd = Bdd.bdd.get(); if (bdd == null) { throw new IllegalStateException("Bdd rule not initialized"); } bdd.requireThatNoUnexpectedExceptionWasThrown(); return new When(bdd).when(throwableSupplier); } /** * Used for specifying behavior that should return a value. * * @param value returned by the specified behavior * @param type of {@code value} * * @return new {@link Then.Returns} */ public static Then.Returns when(T value) { Bdd bdd = Bdd.bdd.get(); if (bdd == null) { throw new IllegalStateException("Bdd rule not initialized"); } bdd.requireThatNoUnexpectedExceptionWasThrown(); return new When(bdd).when(value); } /** * Static factory method for {@link Bdd}. * * @return new bdd */ public static Bdd initialized() { Bdd bdd = new Bdd(); Bdd.bdd.set(bdd); return bdd; } private static final ThreadLocal bdd = new ThreadLocal<>(); /** * Exception thrown in a {@link When} or {@link Optional#empty()}. */ private Optional thrownException; @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { base.evaluate(); requireThatNoUnexpectedExceptionWasThrown(); } }; } /** * Inserts the {@code throwable} into {@link #thrownException}. * * @param throwable to add */ void putThrownException(Throwable throwable) { requireThatNoUnexpectedExceptionWasThrown(); thrownException = Optional.of(throwable); } /** * Retrieves and removes {@link #thrownException}. * * Used by {@link Then} for consuming {@link #thrownException} * * @return {@link #thrownException} */ Optional takeThrownException() { Optional thrownException = this.thrownException; this.thrownException = Optional.empty(); return thrownException; } void throwUnexpectedException(Throwable throwable) { throw new IllegalStateException("Unexpected exception was thrown", throwable); } /** * Throws {@link #thrownException} if present. * * @throws IllegalStateException if a {@code thrownException} already contains an exception, * the previous thrown exception is wrapped */ private void requireThatNoUnexpectedExceptionWasThrown() { if (thrownException.isPresent()) { throwUnexpectedException(thrownException.get()); } } private Bdd() { thrownException = Optional.empty(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy