BDD Spec Support
Behaviour-driven design (BDD) is not after-the-fact "let’s check the system works" testing, but instead is intended as a means to specify the required behaviour in conjunction with the domain expert. Once the feature has been implemented, it also provide a means by which the domain expert can check the feature has been implemented to spec. A BDD spec strictly comes before implementation, and is to guide the conversation and explore the domain problem. The fact that it also gives us an automated regression test is bonus.
Since domain experts are usually non-technical (at least, they are unlikely to be able to read or want to learn how to read JUnit/Java code), then applying BDD typically requires writing specifications in using structured English text and (ASCII) tables. The BDD tooling parses this text and uses it to actually interact with the system under test. As a byproduct the BDD frameworks generate readable output of some form; this is often an annotated version of the original specification, marked up to indicate which specifications passed, which have failed. This readable output is a form of "living documentation"; it captures the actual behaviour of the system, and so is guaranteed to be accurate.
There are many BDD tools out there; Apache Causeway provides an integration with Cucumber JVM (see also the github site):
How it works
At a high level, here’s how Cucumber works
-
specifications are written in the Gherkin DSL, following the "given/when/then" format.
-
Cucumber-JVM itself is a JUnit runner, and searches for feature files on the classpath.
-
These in turn are matched to step definitions through regular expressions.
It is the step definitions (also called "glue") that exercise the system.
The code that goes in step definitions is broadly the same as the code that goes in an integration test method. However one benefit of using step definitions (rather than integration tests) is that the step definitions are reusable across scenarios, so there may be less code overall to maintain.
For example, if you have a step definition that maps to "given an uncompleted todo item", then this can be used for all the scenarios that start with that as their precondition.
Maven Configuration
Dependency Management
If your application inherits from the Apache Causeway starter app (org.apache.causeway.app:causeway-app-starter-parent
) then that will define the version automatically:
<parent>
<groupId>org.apache.causeway.app</groupId>
<artifactId>causeway-app-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
Alternatively, import the core BOM. This is usually done in the top-level parent pom of your application:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.causeway.core</groupId>
<artifactId>causeway-core</artifactId>
<version>3.1.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
In addition, add an entry for the BOM of all the testing support libraries:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.causeway.testing</groupId>
<artifactId>causeway-testing</artifactId>
<scope>import</scope>
<type>pom</type>
<version>2.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Dependencies
In either the domain module(s) or webapp modules of your application, add the following dependency:
<dependencies>
<dependency>
<groupId>org.apache.causeway.testing</groupId>
<artifactId>causeway-testing-specsupport-applib</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
As a convenience, this dependency also brings in the Fixture Scripts and Fakedata libraries. If not required these can always be explicitly excluded.
The Parent POM configures the Maven surefire plugin in three separate executions for unit tests, integ tests and for BDD specs. This relies on a naming convention:
Classes named Not following this convention (intermixing unit tests with integ tests) may cause the latter to fail. |
Update AppManifest
In your application’s AppManifest
(top-level Spring @Configuration
used to bootstrap the app), import the CausewayModuleTestingSpecSupportApplib
module:
@Configuration
@Import({
...
CausewayModuleTestingSpecSupportApplib.class,
...
})
public class AppManifest {
}
Cucumber CLI
At the time of writing, the Maven Surefire does not support custom JUnit platform test engines. As a workaround, we use the Antrun plugin to execute the Cucumber CLI.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>cucumber-cli</id>
<phase>integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<echo message="Running Cucumber CLI" />
<java classname="io.cucumber.core.cli.Main"
fork="true"
failonerror="true"
newenvironment="true"
maxmemory="1024m"
classpathref="maven.test.classpath">
<arg value="--plugin" />
<arg value="json:${project.build.directory}/cucumber-no-clobber.json" />
</java>
</target>
</configuration>
</execution>
</executions>
</plugin>
This uses all of the step definitions found in the stepdefs
package, and writes the results to the cucumber-no-clobber.json
file.
see Cucumber-JVM documentat for the full set of arguments. |
Generated Report
BDD is all about creating a conversation with the domain expert, and that includes providing meaningful feedback as to whether the spec is passing or failing.
The SimpleApp's webapp
module uses a Maven plugin to generate a snazzy HTML website based on the contents of the .json
file emitted by the Cucumber CLI.
The plugin’s configuration is:
<plugin>
<groupId>net.masterthought</groupId>
<artifactId>maven-cucumber-reporting</artifactId>
<version>${maven-cucumber-reporting.version}</version>
<executions>
<execution>
<id>default</id>
<phase>post-integration-test</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<projectName>SimpleApp</projectName>
<outputDirectory>${project.build.directory}</outputDirectory>
<inputDirectory>${project.build.directory}</inputDirectory>
<jsonFiles>
<param>**/cucumber-no-clobber.json</param>
</jsonFiles>
<skip>${skipBDD}</skip>
</configuration>
</execution>
</executions>
</plugin>
Note how this reads the same file that was generated by Cucumber CLI.
The report generated by SimpleApp looks like this:
The idea is that this could then be published to a webserver to create an information radiator.
Writing a BDD spec
BDD specifications contain:
-
a
Xxx.feature
file, describing the feature and the scenarios (given/when/then)s that constitute its acceptance criteria -
a
RunCucumberTest
class file to run the specifications (all boilerplate). This will run all.feature
files in the same package or subpackages, but is basically just boilerplate. -
one or several
XxxStepDef
classes constituting the step definitions to be matched against.The step definitions are intended to be reused across features. We therefore recommend that they reside in a separate package, and are organized by the entity type upon which they act.
For example, given a feature that involves
Customer
andOrder
, have the step definitions pertaining toCustomer
reside inCustomerStepDef
, and the step definitions pertaining toOrder
reside inOrderStepDef
.
The SimpleApp starter app provides some BDD specs, so we’ll use them to understand how this all works.
Bootstrapping
The RunCucumberSpecs
class is annotated with the @Cucumber
JUnit 5 platform test engine to discover the features and step defs (glue).
The class itself is trivial:
package domainapp.webapp.bdd;
import io.cucumber.junit.platform.engine.Cucumber;
@Cucumber (1)
public class RunCucumberSpecs {
}
1 | The tests are run through JUnit 5, as a custom platform engine |
The Cucumber engine is configured using JUnit 5’s standard mechanism, namely the junit-platform.properties
file (in the root package):
cucumber.publish.quiet=true (1)
cucumber.filter.tags=not @backlog and not @ignore (2)
cucumber.glue=domainapp.webapp.bdd (3)
cucumber.plugin=pretty, html:target/cucumber-reports/cucumber-html.json (4)
1 | disable verbose cucumber report publishing information |
2 | tag based test filtering |
3 | restrict classpath search for @Cucumber annotated classes |
4 | activate built-in reporting plugins: pretty to console and html to filesystem |
We also use two "infrastructure" step definitions to bootstrap and configure Spring. These are also boilerplate:
-
The
BootstrapStepDef
class actually starts the Spring application context:BootstrapStepDef.javapackage domainapp.webapp.bdd.stepdefs.infrastructure; (1) ... public class BootstrapStepDef extends ApplicationIntegTestAbstract { (2) @Before(order= PriorityPrecedence.FIRST) (3) public void bootstrap() { // empty (4) } }
1 in a subpackage of domainapp.webapp.bdd.stepdefs
.
Seejunit-platform.properties
, above.2 subclasses from the corresponding integration tests, see integ test support for more on this. 3 this @Before
runs before anything else4 there’s not anything to do (the heavy lifting is in the superclass) -
The
TransactionalStepDef
simulates Spring’s @Transactional attribute:TransactionalStepDef.javapackage domainapp.webapp.bdd.stepdefs.infrastructure; (1) ... public class TransactionalStepDef { (2) private Runnable afterScenario; @Before(order = PriorityPrecedence.EARLY) public void beforeScenario(){ //open InteractionSession to be closed after scenario (see below) interactionService.openInteraction(new InitialisationSession()); val txTemplate = new TransactionTemplate(txMan); (3) val status = txTemplate.getTransactionManager().getTransaction(null); afterScenario = () -> { txTemplate.getTransactionManager().rollback(status); interactionService.closeSessionStack(); }; status.flush(); } @After public void afterScenario(){ if(afterScenario==null) { return; } afterScenario.run(); (4) afterScenario = null; } @Inject private PlatformTransactionManager txMan; (5) }
1 again, in a subpackage of the stepdefs
package.2 no need to subclass anything 3 uses Spring’s TransactionTemplate to wrap up the rest of the steps 4 rolls back the transaction at the end. 5 supporting services are automatically injected.
These two "infrastructure" step definitions could be combined into a single class, if desired.
Typical Usage
With the bootstrapping and infrastructure taken care of, let’s look at the actual spec and corresponding step defs.
Feature: List and Create New Simple Objects (1)
@DomainAppDemo (2)
Scenario: Existing simple objects can be listed and new ones created (1)
Given there are initially 10 simple objects (3)
When I create a new simple object (3)
Then there are 11 simple objects (3)
1 | Provide context, but not actually executed |
2 | Tag indicates the fixture to be run |
3 | Map onto step definitions |
We need a step definition to match the Cucumber tag to a fixture script.
package domainapp.webapp.bdd.stepdefs.fixtures; (1)
...
public class DomainAppDemoStepDef {
@Before(value="@DomainAppDemo", order= PriorityPrecedence.MIDPOINT) (2)
public void runDomainAppDemo() {
fixtureScripts.runFixtureScript(new DomainAppDemo(), null); (3)
}
@Inject private FixtureScripts fixtureScripts; (4)
}
1 | again, under the stepdefs package |
2 | specifies the tag to match |
3 | invokes the similarly named FixtureScript |
4 | The fixtureScripts service is injected automatically |
This will only activate for feature files tagged with "@DomainAppDemo".
Finally, the step definitions pertaining to SimpleObjects
domain service residein the SimpleObjectsSpecDef
class.
This is where the heavy lifting gets done:
package domainapp.webapp.bdd.stepdefs.domain; (1)
...
public class SimpleObjectsStepDef {
@Inject protected SimpleObjects simpleObjects; (2)
@Given("^there (?:is|are).* (\\d+) simple object[s]?$") (3)
public void there_are_N_simple_objects(int n) {
final List<SimpleObject> list = wrap(simpleObjects).listAll(); (4)
assertThat(list.size(), is(n));
}
@When("^.*create (?:a|another) .*simple object$")
public void create_a_simple_object() {
wrap(simpleObjects).create(UUID.randomUUID().toString());
}
<T> T wrap(T domainObject) {
return wrapperFactory.wrap(domainObject);
}
@Inject protected WrapperFactory wrapperFactory; (5)
}
1 | again, under the stepdefs package |
2 | injected domain service being interacted with |
3 | regex to match to feature file specification. |
4 | code that interacts with the domain service. This is done using the WrapperFactory to simulate the UI. |
5 | supporting domain services |
The Scratchpad domain service is one way in which glue classes can pass state between each other. Or, for more type safety, you could develop your own custom domain services for each scenario, and inject these in as regular services. See this blog post for more details. |
Running from the IDE
IntelliJ IDEA (ultimate edition) has built-in support for running individual features:
Running the feature will automatically create a Run Configuration. It may however be necessary to tweak this Run Configuration before the feature file runs successfully:
There are (up to) three things to change:
-
the "Glue" property can be simplified to just the
domainapp.webapp.bdd.stepdefs
package -
the "Working directory" property should be set to
${MODULE_WORKING_DIR}
Note: at the time of writing this doesn’t seem to be in the drop-down, so just type it in
-
in the "Before launch", make sure that the domain entities are enhanced.