From 629c7fb2c256b22f3f5e90c7d2095c9ba4247718 Mon Sep 17 00:00:00 2001 From: Ashley Frieze Date: Sun, 27 Nov 2016 22:25:01 +0000 Subject: [PATCH] Gherkin syntax added. This allows for Features, Scenarios and Given/When/Then/And steps to be used. The Scenarios are treated as a block and failures within the block stop the scenario. The class has been added to allow state to be shared by steps or specs. A few minor Java 8 refactorings included also. --- .gitattributes | 3 + README.md | 50 +++++++++ .../java/com/greghaskins/spectrum/Block.java | 6 +- .../spectrum/FailureDetectingRunListener.java | 31 ++++++ .../greghaskins/spectrum/GherkinSyntax.java | 68 ++++++++++++ .../greghaskins/spectrum/NotifyingBlock.java | 3 + .../com/greghaskins/spectrum/Spectrum.java | 104 +++++++++++------- .../java/com/greghaskins/spectrum/Suite.java | 61 ++++++++-- .../com/greghaskins/spectrum/Variable.java | 38 +++++++ .../greghaskins/spectrum/SpectrumHelper.java | 2 - .../bdd/annotation/WhenDescribingTheSpec.java | 94 ++++++++++++++++ .../bdd/annotation/WhenRunningTheSpec.java | 90 +++++++++++++++ src/test/java/specs/ExampleSpecs.java | 32 ++++++ src/test/java/specs/FixturesSpec.java | 2 +- src/test/java/specs/GherkinExampleSpecs.java | 77 +++++++++++++ 15 files changed, 610 insertions(+), 51 deletions(-) create mode 100644 .gitattributes create mode 100644 src/main/java/com/greghaskins/spectrum/FailureDetectingRunListener.java create mode 100644 src/main/java/com/greghaskins/spectrum/GherkinSyntax.java create mode 100644 src/main/java/com/greghaskins/spectrum/Variable.java create mode 100644 src/test/java/given/a/spec/with/bdd/annotation/WhenDescribingTheSpec.java create mode 100644 src/test/java/given/a/spec/with/bdd/annotation/WhenRunningTheSpec.java create mode 100644 src/test/java/specs/GherkinExampleSpecs.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..27a32f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.java text eol=lf +*.md text eol=lf +*.gradle text eol=lf diff --git a/README.md b/README.md index 009addd..8cabc30 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,35 @@ public class ExampleSpecs { }); + + describe("The Variable convenience wrapper", () -> { + + final Variable counter = new Variable<>(); + + beforeEach(() -> { + counter.set(0); + }); + + beforeEach(() -> { + counter.set(counter.get()+1); + }); + + it("lets you work around Java's requirement that closures only use `final` variables", () -> { + counter.set(counter.get()+1); + assertThat(counter.get(), is(2)); + }); + + it("can optionally have an initial value set", () -> { + final Variable name = new Variable<>("Alice"); + assertThat(name.get(), is("Alice")); + }); + + it("has a null value if not specified", () -> { + final Variable name = new Variable<>(); + assertNull(name.get()); + }); + + }); } } ``` @@ -209,6 +238,21 @@ describe("Ignored specs", () -> { }); }); ``` +### Gherkin Syntax +The following Gherkin-like constructs are available (within the `GherkinSyntax` class): + +* Feature - this is a suite, declared using `feature` +* Scenario - this is also a suite, declared with `scenario` which lives inside a feature + * Scenarios can live inside other scenarios, though that's not encouraged + * All previous steps in a scenario must have passed for the next to run - the scenario is aborted when a step fails +* ScenarioOutline - this is a templated scenario, declared with `scenarioOutline` allowing you to parameterise a scenario + * You provide a stream of values, each of which is consumed by the definition of your scenario + * n-dimensional test sets might be achieved by nested Scenario Outlines +* Given/When/Then/And - these are all just steps - the same level as `it` specs. They are declared with: + * `given` + * `when` + * `then` + * `and` ### Common Variable Initialization @@ -239,6 +283,12 @@ describe("The `let` helper function", () -> { }); ``` +The `Variable` class is a simpler construct than `let`, intended to help you work around Java's requirement that closures only use final variables. The `Variable` object boxes a value, enabling it to be accessed by all nearby specs. + +It is generally poor practice to have co-dependent tests, but some suites may have this requirement - example, a final `afterAll` or `it` to check a summary of the other specs. + +The Gherkin syntax breaks tests down into independent steps, which MUST share state in order to function as a single scenario, and the use of `Variable` objects is a transparent way to do that - especially since it avoids dropping values into the fields of the parent class. + ## Supported Features The Spectrum API is designed to be familiar to Jasmine and RSpec users, while remaining compatible with JUnit. The features and behavior of those libraries help guide decisions on how Specturm should work, both for common scenarios and edge cases. (See [the discussion on #41](https://github.com/greghaskins/spectrum/pull/41#issuecomment-238729178) for an example of how this factors into design decisions.) diff --git a/src/main/java/com/greghaskins/spectrum/Block.java b/src/main/java/com/greghaskins/spectrum/Block.java index da9390f..80501c2 100644 --- a/src/main/java/com/greghaskins/spectrum/Block.java +++ b/src/main/java/com/greghaskins/spectrum/Block.java @@ -1,6 +1,10 @@ package com.greghaskins.spectrum; +/** + * A generic code block with a {@link #run()} method to perform any action. Usually defined by a + * lambda function. + */ @FunctionalInterface -interface Block { +public interface Block { void run() throws Throwable; } diff --git a/src/main/java/com/greghaskins/spectrum/FailureDetectingRunListener.java b/src/main/java/com/greghaskins/spectrum/FailureDetectingRunListener.java new file mode 100644 index 0000000..4a07808 --- /dev/null +++ b/src/main/java/com/greghaskins/spectrum/FailureDetectingRunListener.java @@ -0,0 +1,31 @@ +package com.greghaskins.spectrum; + +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +/** + * A listener to detect test failure. + */ +class FailureDetectingRunListener extends RunListener { + private boolean hasFailedYet = false; + + /** + * Has the run failed since we've been listening. + * @return whether any previous failures have been reported + */ + boolean hasFailedYet() { + return hasFailedYet; + } + + @Override + public void testFailure(Failure failure) throws Exception { + super.testFailure(failure); + hasFailedYet = true; + } + + @Override + public void testAssumptionFailure(Failure failure) { + super.testAssumptionFailure(failure); + hasFailedYet = true; + } +} diff --git a/src/main/java/com/greghaskins/spectrum/GherkinSyntax.java b/src/main/java/com/greghaskins/spectrum/GherkinSyntax.java new file mode 100644 index 0000000..f38acd5 --- /dev/null +++ b/src/main/java/com/greghaskins/spectrum/GherkinSyntax.java @@ -0,0 +1,68 @@ +package com.greghaskins.spectrum; + +import static com.greghaskins.spectrum.Spectrum.compositeSpec; +import static com.greghaskins.spectrum.Spectrum.describe; +import static com.greghaskins.spectrum.Spectrum.it; + +/** + * A translation from Spectrum describe/it to Gherkin like Feature/Scenario/Given/When/Then syntax + * Note - the beforeEach and afterEach will still be executed between given/when/then steps which + * may not make sense in many situations. + */ +public class GherkinSyntax { + /** + * Describes a feature of the system. A feature may have many scenarios. + * @param featureName name of feature + * @param block the contents of the feature + */ + public static void feature(final String featureName, final Block block) { + describe("Feature: " + featureName, block); + } + + /** + * Describes a scenario of the system. These can be at root level, though are best grouped + * inside features. + * @param scenarioName name of scenario + * @param block the contents of the scenario - given/when/then steps + */ + public static void scenario(final String scenarioName, final Block block) { + compositeSpec("Scenario: " + scenarioName, block); + } + + /** + * A gherkin like given block. + * @param behavior the behaviour to associate with the precondition + * @param block how to execute that precondition + */ + public static void given(final String behavior, final Block block) { + it("Given " + behavior, block); + } + + /** + * A gherkin like when block. + * @param behavior the behaviour to associate with the manipulation of the system under test + * @param block how to execute that behaviour + */ + public static void when(final String behavior, final Block block) { + it("When " + behavior, block); + } + + /** + * A gherkin like then block. + * @param behavior the behaviour to associate with the postcondition + * @param block how to execute that postcondition + */ + public static void then(final String behavior, final Block block) { + it("Then " + behavior, block); + } + + /** + * An and block. + * @param behavior what we would like to describe as an and + * @param block how to achieve the and block + */ + public static void and(final String behavior, final Block block) { + it("And " + behavior, block); + } + +} diff --git a/src/main/java/com/greghaskins/spectrum/NotifyingBlock.java b/src/main/java/com/greghaskins/spectrum/NotifyingBlock.java index 2113a0c..0f7e33c 100644 --- a/src/main/java/com/greghaskins/spectrum/NotifyingBlock.java +++ b/src/main/java/com/greghaskins/spectrum/NotifyingBlock.java @@ -1,5 +1,6 @@ package com.greghaskins.spectrum; +import org.junit.AssumptionViolatedException; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; @@ -13,6 +14,8 @@ static NotifyingBlock wrap(final Block block) { return (description, notifier) -> { try { block.run(); + } catch (final AssumptionViolatedException assumptionViolation) { + notifier.fireTestAssumptionFailed(new Failure(description, assumptionViolation)); } catch (final Throwable exception) { notifier.fireTestFailure(new Failure(description, exception)); } diff --git a/src/main/java/com/greghaskins/spectrum/Spectrum.java b/src/main/java/com/greghaskins/spectrum/Spectrum.java index dded5fd..191d9f6 100644 --- a/src/main/java/com/greghaskins/spectrum/Spectrum.java +++ b/src/main/java/com/greghaskins/spectrum/Spectrum.java @@ -27,7 +27,9 @@ public final class Spectrum extends Runner { /** * A generic code block with a {@link #run()} method to perform any action. Usually defined by a * lambda function. + * @deprecated since 1.0.1 - use {@link com.greghaskins.spectrum.Block} instead */ + @Deprecated @FunctionalInterface public interface Block extends com.greghaskins.spectrum.Block { /** @@ -39,59 +41,77 @@ public interface Block extends com.greghaskins.spectrum.Block { void run() throws Throwable; } + /** + * Declare a test suite that is made of interdependent children. The whole + * suite should pass atomically and if it fails, any remaining children + * can stop running. + * + * @param context Description of the context for this suite + * @param block {@link com.greghaskins.spectrum.Block} with one or more calls to + * {@link #it(String, com.greghaskins.spectrum.Block) it} that define each expected behavior + * + */ + static void compositeSpec(final String context, final com.greghaskins.spectrum.Block block) { + final Suite suite = getCurrentSuiteBeingDeclared().addAbortingSuite(context); + beginDefinition(suite, block); + } + /** * Declare a test suite that describes the expected behavior of the system in a given context. * * @param context Description of the context for this suite - * @param block {@link Block} with one or more calls to {@link #it(String, Block) it} that define + * @param block {@link com.greghaskins.spectrum.Block} with one or more calls to + * {@link #it(String, com.greghaskins.spectrum.Block) it} that define * each expected behavior - * */ - public static void describe(final String context, final Block block) { + public static void describe(final String context, final com.greghaskins.spectrum.Block block) { final Suite suite = getCurrentSuiteBeingDeclared().addSuite(context); - beginDefintion(suite, block); + beginDefinition(suite, block); } /** * Focus on this specific suite, while ignoring others. * * @param context Description of the context for this suite - * @param block {@link Block} with one or more calls to {@link #it(String, Block) it} that define + * @param block {@link com.greghaskins.spectrum.Block} with one or more calls to + * {@link #it(String, com.greghaskins.spectrum.Block) it} that define * each expected behavior * - * @see #describe(String, Block) + * @see #describe(String, com.greghaskins.spectrum.Block) * */ - public static void fdescribe(final String context, final Block block) { + public static void fdescribe(final String context, final com.greghaskins.spectrum.Block block) { final Suite suite = getCurrentSuiteBeingDeclared().addSuite(context); suite.focus(); - beginDefintion(suite, block); + beginDefinition(suite, block); } /** * Ignore the specific suite. * * @param context Description of the context for this suite - * @param block {@link Block} with one or more calls to {@link #it(String, Block) it} that define + * @param block {@link com.greghaskins.spectrum.Block} with one or more calls to + * {@link #it(String, com.greghaskins.spectrum.Block) it} that define * each expected behavior * - * @see #describe(String, Block) + * @see #describe(String, com.greghaskins.spectrum.Block) * */ - public static void xdescribe(final String context, final Block block) { + public static void xdescribe(final String context, final com.greghaskins.spectrum.Block block) { final Suite suite = getCurrentSuiteBeingDeclared().addSuite(context); suite.ignore(); - beginDefintion(suite, block); + beginDefinition(suite, block); } /** * Declare a spec, or test, for an expected behavior of the system in this suite context. * * @param behavior Description of the expected behavior - * @param block {@link Block} that verifies the system behaves as expected and throws a - * {@link java.lang.Throwable Throwable} if that expectation is not met. + * @param block {@link com.greghaskins.spectrum.Block} that verifies the system behaves as + * expected and throws a {@link java.lang.Throwable Throwable} if that expectation + * is not met. */ - public static void it(final String behavior, final Block block) { + public static void it(final String behavior, final com.greghaskins.spectrum.Block block) { getCurrentSuiteBeingDeclared().addSpec(behavior, block); } @@ -100,7 +120,7 @@ public static void it(final String behavior, final Block block) { * * @param behavior Description of the expected behavior * - * @see #xit(String, Block) + * @see #xit(String, com.greghaskins.spectrum.Block) */ public static void it(final String behavior) { getCurrentSuiteBeingDeclared().addSpec(behavior, null).ignore(); @@ -110,12 +130,13 @@ public static void it(final String behavior) { * Focus on this specific spec, while ignoring others. * * @param behavior Description of the expected behavior - * @param block {@link Block} that verifies the system behaves as expected and throws a - * {@link java.lang.Throwable Throwable} if that expectation is not met. + * @param block {@link com.greghaskins.spectrum.Block} that verifies the system behaves as + * expected and throws a {@link java.lang.Throwable Throwable} if that expectation + * is not met. * - * @see #it(String, Block) + * @see #it(String, com.greghaskins.spectrum.Block) */ - public static void fit(final String behavior, final Block block) { + public static void fit(final String behavior, final com.greghaskins.spectrum.Block block) { getCurrentSuiteBeingDeclared().addSpec(behavior, block).focus(); } @@ -123,25 +144,26 @@ public static void fit(final String behavior, final Block block) { * Mark a spec as ignored so that it will be skipped. * * @param behavior Description of the expected behavior - * @param block {@link Block} that will not run, since this spec is ignored. + * @param block {@link com.greghaskins.spectrum.Block} that will not run, since this spec is + * ignored. * - * @see #it(String, Block) + * @see #it(String, com.greghaskins.spectrum.Block) */ - public static void xit(final String behavior, final Block block) { + public static void xit(final String behavior, final com.greghaskins.spectrum.Block block) { it(behavior); } /** - * Declare a {@link Block} to be run before each spec in the suite. + * Declare a {@link com.greghaskins.spectrum.Block} to be run before each spec in the suite. * *

* Use this to perform setup actions that are common across tests in the context. If multiple * {@code beforeEach} blocks are declared, they will run in declaration order. *

* - * @param block {@link Block} to run once before each spec + * @param block {@link com.greghaskins.spectrum.Block} to run once before each spec */ - public static void beforeEach(final Block block) { + public static void beforeEach(final com.greghaskins.spectrum.Block block) { getCurrentSuiteBeingDeclared().beforeEach(block); } @@ -155,7 +177,7 @@ public static void beforeEach(final Block block) { * * @param block {@link Block} to run once after each spec */ - public static void afterEach(final Block block) { + public static void afterEach(final com.greghaskins.spectrum.Block block) { getCurrentSuiteBeingDeclared().afterEach(block); } @@ -163,27 +185,29 @@ public static void afterEach(final Block block) { * Declare a {@link Block} to be run once before all the specs in the current suite begin. * *

- * Use {@code beforeAll} and {@link #afterAll(Block) afterAll} blocks with caution: since they - * only run once, shared state will leak across specs. + * Use {@code beforeAll} and {@link #afterAll(com.greghaskins.spectrum.Block) afterAll} + * blocks with caution: since they only run once, shared state will + * leak across specs. *

* - * @param block {@link Block} to run once before all specs in this suite + * @param block {@link com.greghaskins.spectrum.Block} to run once before all specs in this suite */ - public static void beforeAll(final Block block) { + public static void beforeAll(final com.greghaskins.spectrum.Block block) { getCurrentSuiteBeingDeclared().beforeAll(block); } /** - * Declare a {@link Block} to be run once after all the specs in the current suite have run. + * Declare a {@link com.greghaskins.spectrum.Block} to be run once after all the specs in the + * current suite have run. * *

- * Use {@link #beforeAll(Block) beforeAll} and {@code afterAll} blocks with caution: since they - * only run once, shared state will leak across tests. + * Use {@link #beforeAll(com.greghaskins.spectrum.Block) beforeAll} and {@code afterAll} blocks + * with caution: since they only run once, shared state will leak across tests. *

* - * @param block {@link Block} to run once after all specs in this suite + * @param block {@link com.greghaskins.spectrum.Block} to run once after all specs in this suite */ - public static void afterAll(final Block block) { + public static void afterAll(final com.greghaskins.spectrum.Block block) { getCurrentSuiteBeingDeclared().afterAll(block); } @@ -204,11 +228,11 @@ public static void afterAll(final Block block) { */ public static Supplier let(final ThrowingSupplier supplier) { final ConcurrentHashMap, T> cache = new ConcurrentHashMap<>(1); - afterEach(() -> cache.clear()); + afterEach(cache::clear); return () -> { if (getCurrentSuiteBeingDeclared() == null) { - return cache.computeIfAbsent(supplier, s -> s.get()); + return cache.computeIfAbsent(supplier, Supplier::get); } throw new IllegalStateException("Cannot use the value from let() in a suite declaration. " + "It may only be used in the context of a running spec."); @@ -265,7 +289,7 @@ public Spectrum(final Class testClass) { Spectrum(final Description description, final com.greghaskins.spectrum.Block definitionBlock) { this.rootSuite = Suite.rootSuite(description); - beginDefintion(this.rootSuite, definitionBlock); + beginDefinition(this.rootSuite, definitionBlock); } @Override @@ -278,7 +302,7 @@ public void run(final RunNotifier notifier) { this.rootSuite.run(notifier); } - private static synchronized void beginDefintion(final Suite suite, + private static synchronized void beginDefinition(final Suite suite, final com.greghaskins.spectrum.Block definitionBlock) { suiteStack.push(suite); try { diff --git a/src/main/java/com/greghaskins/spectrum/Suite.java b/src/main/java/com/greghaskins/spectrum/Suite.java index 0322265..e917b09 100644 --- a/src/main/java/com/greghaskins/spectrum/Suite.java +++ b/src/main/java/com/greghaskins/spectrum/Suite.java @@ -20,22 +20,47 @@ final class Suite implements Parent, Child { private final List children = new ArrayList<>(); private final Set focusedChildren = new HashSet<>(); + private final ChildRunner childRunner; + private final Description description; private final Parent parent; private boolean ignored; - static Suite rootSuite(final Description description) { - return new Suite(description, Parent.NONE); + + /** + * The strategy for running the children within the suite. + */ + @FunctionalInterface + interface ChildRunner { + void runChildren(final Suite suite, final RunNotifier notifier); } - private Suite(final Description description, final Parent parent) { + static Suite rootSuite(final Description description) { + return new Suite(description, Parent.NONE, Suite::defaultChildRunner); + } + + /** + * Constructs a suite. + * @param description the JUnit description + * @param parent parent item + * @param childRunner which child running strategy to use - this will normally be + * {@link #defaultChildRunner(Suite, RunNotifier)} which runs them all + * but can be substituted for a strategy that ignores all specs + * after a test failure {@link #abortOnFailureChildRunner(Suite, RunNotifier)} + */ + private Suite(final Description description, final Parent parent, final ChildRunner childRunner) { this.description = description; this.parent = parent; this.ignored = parent.isIgnored(); + this.childRunner = childRunner; } Suite addSuite(final String name) { - final Suite suite = new Suite(Description.createSuiteDescription(name), this); + return addSuite(name, Suite::defaultChildRunner); + } + + Suite addSuite(final String name, final ChildRunner childRunner) { + final Suite suite = new Suite(Description.createSuiteDescription(name), this, childRunner); suite.beforeAll.addBlock(this.beforeAll); suite.beforeEach.addBlock(this.beforeEach); suite.afterEach.addBlock(this.afterEach); @@ -44,6 +69,10 @@ Suite addSuite(final String name) { return suite; } + Suite addAbortingSuite(final String name) { + return addSuite(name, Suite::abortOnFailureChildRunner); + } + Spec addSpec(final String name, final Block block) { final Spec spec = createSpec(name, block); addChild(spec); @@ -131,7 +160,7 @@ public void run(final RunNotifier notifier) { } private void runChildren(final RunNotifier notifier) { - this.children.forEach((child) -> runChild(child, notifier)); + childRunner.runChildren(this, notifier); } private void runChild(final Child child, final RunNotifier notifier) { @@ -149,18 +178,36 @@ private void runAfterAll(final RunNotifier notifier) { @Override public Description getDescription() { final Description copy = this.description.childlessCopy(); - this.children.stream().forEach((child) -> copy.addChild(child.getDescription())); + this.children.forEach((child) -> copy.addChild(child.getDescription())); return copy; } @Override public int testCount() { - return this.children.stream().mapToInt((child) -> child.testCount()).sum(); + return this.children.stream().mapToInt(Child::testCount).sum(); } void removeAllChildren() { this.children.clear(); } + private static void defaultChildRunner(final Suite suite, final RunNotifier runNotifier) { + suite.children.forEach((child) -> suite.runChild(child, runNotifier)); + } + + private static void abortOnFailureChildRunner(final Suite suite, final RunNotifier runNotifier) { + FailureDetectingRunListener listener = new FailureDetectingRunListener(); + runNotifier.addListener(listener); + try { + for (Child child : suite.children) { + if (listener.hasFailedYet()) { + child.ignore(); + } + suite.runChild(child, runNotifier); + } + } finally { + runNotifier.removeListener(listener); + } + } } diff --git a/src/main/java/com/greghaskins/spectrum/Variable.java b/src/main/java/com/greghaskins/spectrum/Variable.java new file mode 100644 index 0000000..b74f1de --- /dev/null +++ b/src/main/java/com/greghaskins/spectrum/Variable.java @@ -0,0 +1,38 @@ +package com.greghaskins.spectrum; + +/** + * This box allows data passing between specs or steps. + */ +public class Variable { + // the boxed object + private T object; + + /** + * Default constructor for null object. + */ + public Variable() {} + + /** + * Construct with the object to box. + * @param object to box + */ + public Variable(T object) { + this.object = object; + } + + /** + * Retrieve the object. + * @return the object in the box + */ + public T get() { + return object; + } + + /** + * Change the object in the box. + * @param object new value + */ + public void set(T object) { + this.object = object; + } +} diff --git a/src/test/java/com/greghaskins/spectrum/SpectrumHelper.java b/src/test/java/com/greghaskins/spectrum/SpectrumHelper.java index 5510321..757b4ee 100644 --- a/src/test/java/com/greghaskins/spectrum/SpectrumHelper.java +++ b/src/test/java/com/greghaskins/spectrum/SpectrumHelper.java @@ -1,7 +1,5 @@ package com.greghaskins.spectrum; -import com.greghaskins.spectrum.Spectrum.Block; - import org.junit.runner.Description; import org.junit.runner.JUnitCore; import org.junit.runner.Request; diff --git a/src/test/java/given/a/spec/with/bdd/annotation/WhenDescribingTheSpec.java b/src/test/java/given/a/spec/with/bdd/annotation/WhenDescribingTheSpec.java new file mode 100644 index 0000000..1f6d43c --- /dev/null +++ b/src/test/java/given/a/spec/with/bdd/annotation/WhenDescribingTheSpec.java @@ -0,0 +1,94 @@ +package given.a.spec.with.bdd.annotation; + +import static com.greghaskins.spectrum.GherkinSyntax.and; +import static com.greghaskins.spectrum.GherkinSyntax.feature; +import static com.greghaskins.spectrum.GherkinSyntax.given; +import static com.greghaskins.spectrum.GherkinSyntax.scenario; +import static com.greghaskins.spectrum.GherkinSyntax.then; +import static com.greghaskins.spectrum.GherkinSyntax.when; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.core.Is.is; + +import com.greghaskins.spectrum.GherkinSyntax; +import com.greghaskins.spectrum.Spectrum; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.Description; + +import java.util.stream.Collectors; + +public class WhenDescribingTheSpec { + private Description mainDescription; + + @Before + public void before() throws Exception { + final Description rootDescription = + new Spectrum(getBddExampleSpec()).getDescription(); + this.mainDescription = rootDescription.getChildren().get(0); + } + + @Test + public void theTopLevelIsAFeature() throws Exception { + assertThat(this.mainDescription.getDisplayName(), + is("Feature: BDD semantics")); + } + + @Test + public void theNextLevelIsAScenario() throws Exception { + assertThat(this.mainDescription.getChildren().get(0).getDisplayName(), + is("Scenario: a named scenario with")); + } + + @Test + public void theScenarioHasGivenWhenThen() throws Exception { + assertThat(this.mainDescription.getChildren().get(0).getChildren() + .stream().map(Description::getDisplayName) + .collect(Collectors.toList()), + contains("Given some sort of given(Scenario: a named scenario with)", + "When some sort of when(Scenario: a named scenario with)", + "Then some sort of outcome(Scenario: a named scenario with)", + "And an and on the end(Scenario: a named scenario with)")); + } + + @Test + public void theSyntaxUtilityClassHasConstructorThatIsNeverUsed() throws Exception { + // the real code doesn't need it, but the test code can cover it with + // code coverage + new GherkinSyntax(); + } + + private static Class getBddExampleSpec() { + class Spec { + { + feature("BDD semantics", () -> { + + scenario("a named scenario with", () -> { + + given("some sort of given", () -> { + Assert.assertTrue(true); + }); + + when("some sort of when", () -> { + Assert.assertTrue(true); + }); + + then("some sort of outcome", () -> { + Assert.assertTrue(true); + }); + + and("an and on the end", () -> { + Assert.assertTrue(true); + }); + + }); + }); + } + } + + return Spec.class; + } + +} diff --git a/src/test/java/given/a/spec/with/bdd/annotation/WhenRunningTheSpec.java b/src/test/java/given/a/spec/with/bdd/annotation/WhenRunningTheSpec.java new file mode 100644 index 0000000..8b85cb0 --- /dev/null +++ b/src/test/java/given/a/spec/with/bdd/annotation/WhenRunningTheSpec.java @@ -0,0 +1,90 @@ +package given.a.spec.with.bdd.annotation; + +import static com.greghaskins.spectrum.GherkinSyntax.feature; +import static com.greghaskins.spectrum.GherkinSyntax.given; +import static com.greghaskins.spectrum.GherkinSyntax.scenario; +import static com.greghaskins.spectrum.GherkinSyntax.then; +import static com.greghaskins.spectrum.GherkinSyntax.when; +import static org.junit.Assert.assertFalse; +import static org.junit.Assume.assumeTrue; + +import com.greghaskins.spectrum.SpectrumHelper; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class WhenRunningTheSpec { + private static boolean thenRan = false; + + @Before + public void before() { + thenRan = false; + } + + @Test + public void bddStepFailureStopsTheSpec() throws Exception { + SpectrumHelper.run(getBddExampleWhichFailsSpec()); + assertFalse(thenRan); + } + + @Test + public void bddAssumptionFailureStopsTheSpec() throws Exception { + SpectrumHelper.run(getBddExampleWithAssumptionFailure()); + assertFalse(thenRan); + } + + private static Class getBddExampleWhichFailsSpec() { + class Spec { + { + feature("BDD steps stop at failure", () -> { + + scenario("failing at when", () -> { + + given("a passing given", () -> { + Assert.assertTrue(true); + }); + + when("the when fails", () -> { + Assert.assertTrue(false); + }); + + then("the then can't do its thing", () -> { + thenRan = true; + }); + + }); + }); + } + } + + return Spec.class; + } + + private static Class getBddExampleWithAssumptionFailure() { + class Spec { + { + feature("BDD steps stop at failure", () -> { + + scenario("failing at when", () -> { + + given("an assumption failure in step one", () -> { + assumeTrue(false); + }); + + when("the when can't do its thing", () -> { + thenRan = true; + }); + + then("the then can't do its thing", () -> { + thenRan = true; + }); + + }); + }); + } + } + + return Spec.class; + } +} diff --git a/src/test/java/specs/ExampleSpecs.java b/src/test/java/specs/ExampleSpecs.java index 409922a..bf22ae1 100644 --- a/src/test/java/specs/ExampleSpecs.java +++ b/src/test/java/specs/ExampleSpecs.java @@ -6,6 +6,7 @@ import static com.greghaskins.spectrum.Spectrum.beforeEach; import static com.greghaskins.spectrum.Spectrum.describe; import static com.greghaskins.spectrum.Spectrum.it; +import static junit.framework.TestCase.assertNull; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; @@ -15,6 +16,7 @@ import static org.junit.Assert.assertEquals; import com.greghaskins.spectrum.Spectrum; +import com.greghaskins.spectrum.Variable; import org.junit.runner.RunWith; @@ -133,5 +135,35 @@ public class ExampleSpecs { }); + + describe("The Variable convenience wrapper", () -> { + + final Variable counter = new Variable<>(); + + beforeEach(() -> { + counter.set(0); + }); + + beforeEach(() -> { + counter.set(counter.get() + 1); + }); + + it("lets you work around Java's requirement that closures only use `final` variables", () -> { + counter.set(counter.get() + 1); + assertThat(counter.get(), is(2)); + }); + + it("can optionally have an initial value set", () -> { + final Variable name = new Variable<>("Alice"); + assertThat(name.get(), is("Alice")); + }); + + it("has a null value if not specified", () -> { + final Variable name = new Variable<>(); + assertNull(name.get()); + }); + + }); + } } diff --git a/src/test/java/specs/FixturesSpec.java b/src/test/java/specs/FixturesSpec.java index e77331c..f3cf601 100644 --- a/src/test/java/specs/FixturesSpec.java +++ b/src/test/java/specs/FixturesSpec.java @@ -17,8 +17,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import com.greghaskins.spectrum.Block; import com.greghaskins.spectrum.Spectrum; -import com.greghaskins.spectrum.Spectrum.Block; import com.greghaskins.spectrum.Spectrum.ThrowingSupplier; import com.greghaskins.spectrum.SpectrumHelper; diff --git a/src/test/java/specs/GherkinExampleSpecs.java b/src/test/java/specs/GherkinExampleSpecs.java new file mode 100644 index 0000000..0b55ae7 --- /dev/null +++ b/src/test/java/specs/GherkinExampleSpecs.java @@ -0,0 +1,77 @@ +package specs; + +import static com.greghaskins.spectrum.GherkinSyntax.and; +import static com.greghaskins.spectrum.GherkinSyntax.feature; +import static com.greghaskins.spectrum.GherkinSyntax.given; +import static com.greghaskins.spectrum.GherkinSyntax.scenario; +import static com.greghaskins.spectrum.GherkinSyntax.then; +import static com.greghaskins.spectrum.GherkinSyntax.when; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import com.greghaskins.spectrum.Spectrum; +import com.greghaskins.spectrum.Variable; + +import org.junit.runner.RunWith; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Demonstrates the BDD syntax of Spectrum. + */ +@RunWith(Spectrum.class) +public class GherkinExampleSpecs { + { + feature("BDD", () -> { + scenario("allow Gherkin like syntax", () -> { + final AtomicInteger integer = new AtomicInteger(); + given("we start with a given", () -> { + integer.set(12); + }); + when("we have a when to execute the system", () -> { + integer.incrementAndGet(); + }); + then("we can assert the outcome", () -> { + assertThat(integer.get(), is(13)); + }); + }); + + scenario("uses boxes within the scenario where data is passed between steps", () -> { + Variable theData = new Variable<>(); + + given("the data is set", () -> { + theData.set("Hello world"); + }); + + when("the data is modified", () -> { + theData.set(theData.get() + "!"); + }); + + then("the data can be seen with the addition", () -> { + assertThat(theData.get(), is("Hello world!")); + }); + + and("the data is still available", () -> { + assertNotNull(theData.get()); + }); + }); + + scenario("uses default value from box", () -> { + Variable theData = new Variable<>("Hello world"); + + given("the data is set correctly", () -> { + assertThat(theData.get(), is("Hello world")); + }); + + when("the data is modified", () -> { + theData.set(theData.get() + "!"); + }); + + then("the data can be seen with the addition", () -> { + assertThat(theData.get(), is("Hello world!")); + }); + }); + }); + } +}