1. Introduction
JGiven is a light-weight Java library that helps you to design a high-level, domain-specific language for writing BDD scenarios. In the spirit of Unix tools, it tries to do one thing and do it well, instead of trying to solve all problems. Thus, you still use your favorite assertion library and mocking library for writing your test implementations, but you use JGiven to write a readable abstraction layer on top of it.
1.1. Module Overview
JGiven consists of a couple of modules.
1.1.1. JGiven Core
The JGiven Core module contains the core implementation of JGiven and is always required.
1.1.2. JGiven JUnit
This module provides an integration into JUnit 4.x. If your test-runner of choice is JUnit then you use this module.
1.1.4. JGiven HTML5 Report
Allows you to generate an HTML5 report from the JSON files that JGiven generates during the test execution.
1.1.5. JGiven Spring
Provides an integration into the Spring framework, which basically means that stages classes can be treated as Spring beans.
2. Installation
JGiven is installed as any other Java library by putting its JAR file(s) into the classpath. Normally you will do that by using one of your favorite build and dependency management tools like Maven, Gradle, or Apache Ant + Ivy. Alternatively you can download the JAR files directly from Maven Central.
Depending on whether you are using JUnit or TestNG for executing tests, you have to use different dependencies.
2.1. JUnit
If you are using JUnit, you must depend on the jgiven-junit
artifact.
Note that jgiven-junit
does not directly depend on JUnit,
thus you also must have a dependency to JUnit itself.
JGiven requires at least JUnit 4.9, while the recommended version is 4.13.2.
2.2. TestNG
If you are using TestNG, you must depend on the jgiven-testng
artifact.
Note that jgiven-testng
does not directly depend on TestNG,
thus you also must have a dependency to TestNG itself.
3. Getting Started
JGiven can be used together with JUnit or TestNG, here we assume you are using JUnit.
3.1. Create a JUnit test class
First of all you create a JUnit test class that inherits from com.tngtech.jgiven.junit.ScenarioTest
:
import com.tngtech.jgiven.junit.ScenarioTest;
public class MyShinyJGivenTest
extends ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> {
}
The ScenarioTest
requires 3 type parameters. Each of these type parameters represents a stage of the Given-When-Then notation. Note that there is also the SimpleScenarioTest
class that only requires a single type parameter. In that case, all your scenario steps are defined in a single class.
For scenarios where you only want to differentiate between steps that modify state (setup in given()
, changes in when()
) and steps that assert state (then()
) you can use the DualScenarioTest
which takes two parameters: a combined GivenWhen
stage and a single Then
stage.
3.2. Create Given, When, and Then classes
To make your class compile, create the following three classes:
import com.tngtech.jgiven.Stage;
public class GivenSomeState extends Stage<GivenSomeState> {
public GivenSomeState some_state() {
return self();
}
}
import com.tngtech.jgiven.Stage;
public class WhenSomeAction extends Stage<WhenSomeAction> {
public WhenSomeAction some_action() {
return self();
}
}
import com.tngtech.jgiven.Stage;
public class ThenSomeOutcome extends Stage<ThenSomeOutcome> {
public ThenSomeOutcome some_outcome() {
return self();
}
}
JGiven does not require you to inherit from the Stage class, however, the Stage class already provides some useful methods like and() and self().
3.3. Write your first scenario
Now you can write your first scenario
import org.junit.Test;
import com.tngtech.jgiven.junit.ScenarioTest;
public class MyShinyJGivenTest
extends ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> {
@Test
public void something_should_happen() {
given().some_state();
when().some_action();
then().some_outcome();
}
}
3.4. Execute your scenario
The scenario is then executed like any other JUnit test, for example, by using your IDE or Maven:
$ mvn test
3.5. Using JUnit Rules directly instead of deriving from ScenarioTest
Sometimes it is not possible to derive your test class from the ScenarioTest
class, because you might already have a common base class that you cannot easily modify.
In that case you can directly use the JUnit Rules of JGiven. Instead of providing your stage classes as type parameters, you inject the stages into the test class with the @ScenarioStage
annotation.
public class UsingRulesTest {
@ClassRule
public static final JGivenClassRule writerRule = new JGivenClassRule();
@Rule
public final JGivenMethodRule scenarioRule = new JGivenMethodRule();
@ScenarioStage
GivenSomeState someState;
@ScenarioStage
WhenSomeAction someAction;
@ScenarioStage
ThenSomeOutcome someOutcome;
@Test
public void something_should_happen() {
someState.given().some_state();
someAction.when().some_action();
someOutcome.then().some_outcome();
}
}
Note that your stage classes have to inherit from the Stage
class in order to have the given(), when()
, and then()
methods.
You can now also define convenient methods in your test class to get cleaner scenarios if you like:
public GivenSomeState given() {
return someStage.given();
}
4. Report Generation
4.1. Plain Text Reports
By default JGiven outputs plain text reports to the console when executed. To disable plain text reports set the following Java system property:
jgiven.report.text=false
4.2. JSON Reports
By default JGiven will generate JSON reports into the jgiven-reports/json
directory. JGiven tries to autodetect when it is executed by the Maven surefire plugin and in that case generates the reports into target/jgiven-reports/json
. To disable JSON report generation set the following Java system property:
jgiven.report.enabled=false
In order to generate HTML reports, JSON reports are required. |
4.2.1. Change report directory
If you want to change the jgiven-reports/json
directory, respectively target/jgiven-reports/json
, set the following Java system property:
jgiven.report.dir=<targetDir>
If JGiven is executed by the Maven surefire plugin, this can be done by the systemPropertyVariables configuration (see: http://maven.apache.org/surefire/maven-surefire-plugin/examples/system-properties.html).
In case HTML Reports are being generated, the source directory for the JSON Reports needs to be set accordingly (see "HTML Report" for more). |
4.3. Dry Run
There is a dry run option, which just generates a report without actually executing the code. This might be helpful to generate a test report quickly without having to wait for the tests to be executed.
jgiven.report.dry-run=true
As the tests are not really executed and thus cannot fail, all tests will be reported as successful. |
4.4. HTML Report
To generate an HTML report you have to run the JGiven report generator
with the html
format option.
The reporter is part of the jgiven-html5-report
module.
The report generator can be executed on the command line as
follows (assuming that the jgiven-core
and the jgiven-html5-report
JAR
and all required dependencies are on the Java CLASSPATH)
java com.tngtech.jgiven.report.ReportGenerator \
--format=html \
[--sourceDir=<jsonreports>] \
[--targetDir=<targetDir>] \
To see the HTML report in action you can have a look at the HTML report of JGiven itself
4.4.1. Maven
For Maven there exists a plugin that can be used as follows:
<build>
<plugins>
<plugin>
<groupId>com.tngtech.jgiven</groupId>
<artifactId>jgiven-maven-plugin</artifactId>
<version>2.0.1</version>
<executions>
<execution>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
<configuration>
<format>html</format>
</configuration>
</plugin>
</plugins>
</build>
You can add the following configuration options (like the format configuration above) to customize the report. All of them are optional.
Option | Description |
---|---|
format |
The format of the generated report. Can be html or text. Default: html |
title |
The title of the generated report. Default: JGiven Report |
customCssFile |
Custom CSS file to customize the HTML report. Default: src/test/resources/jgiven/custom.css |
customJsFile |
Custom JS file to customize the HTML report. Default: src/test/resources/jgiven/custom.js |
excludeEmptyScenarios |
Whether or not to exclude empty scenarios, i.e. scenarios without any steps, from the report. Default: false |
outputDirectory |
Directory where the reports are generated to. Default: ${project.build.directory}/jgiven-reports/html |
sourceDirectory |
Directory to read the JSON report files from. Default: ${project.build.directory}/jgiven-reports/json |
Now run:
$ mvn verify
HTML reports are then generated into the target/jgiven-reports/html
directory. Note that the plugin relies on the existence of the JSON output, so if the property jgiven.reports.enabled
was set to false
, no output will be generated.
4.4.2. Gradle
There also exists a plugin for Gradle to make your life easier.
Add the following plugins section to your build.gradle
file or extend the one you have already accordingly:
plugins {
id "com.tngtech.jgiven.gradle-plugin" version "2.0.1"
}
When using Kotlin, make sure the JGiven Gradle plugin is configured after the kotlin("jvm")
plugin.
Alternatively you can configure the plugin as follows:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "com.tngtech.jgiven:jgiven-gradle-plugin:2.0.1"
}
}
apply plugin: "com.tngtech.jgiven.gradle-plugin"
Now run:
$ gradle test jgivenTestReport
HTML reports are then generated into the build/reports/jgiven/test/html/
directory. Note that the plugin relies on the existence of the JSON output, so if the property jgiven.reports.enabled
was set to false
, no output will be generated.
If you want that the HTML report is always generated after the tests
have been executed, you can configure the test
task in your Gradle
configuration file as follows:
test.finalizedBy jgivenTestReport
For additional information about the Gradle plugin refer to https://plugins.gradle.org/plugin/com.tngtech.jgiven.gradle-plugin
4.5. Configuration File
JGiven will optionally load a configuration properties file, defaulting to:
jgiven.properties
. The path to the configuration can be customized with the system property:
jgiven.config.path
The encoding for the file is assumed to be UTF-8
, but can be customized with the system property:
jgiven.config.charset
The following can be defined in the properties file:
jgiven.report.enabled=false
jgiven.report.dir=<targetDir>
jgiven.report.text=false
jgiven.report.text.color
jgiven.report.filterStackTrace=true
Configuration defined via Java system properties will take precedence over values in the configuration file.
4.6. Configuration class
Finally, JGiven allows to set a custom derivative of AbstractJGivenConfiguration
on the class level via the @JGivenConfiguration
annotation. Tag configuration, formatter configuration and a default @As provider
provider can be set there.
5. Stages and State Sharing
A JGiven scenario consists of multiple stages. Typically, there is a stage for each phase of a scenario: a given stage, a when stage and a then stage, however, it is also possible to just use one stage or use arbitrarily many stages. A stage is implemented by a stage class that contains methods representing the steps that can be used in the scenario. The big advantage of this modular concept is that stages can be easily reused by different scenarios. Stage classes will be proxied by JGiven, therefore, they must neither be final, nor sealed.
5.1. Step Methods
A stage class contains multiple step methods that can be called in a scenario and that will appear in the report. Every non-private, non-final, non-static method of a stage class can be used as a step method. There are no further requirements. In particular, no annotations are needed. Step methods should return the this
reference so that chaining of calls is possible. In addition, a step method should be written in snake_case so that JGiven knows the correct casing of each word of the step.
The following code shows a valid step method of the WhenCook
stage:
public WhenCook the_cook_mangles_everthing_to_a_dough() {
assertThat( cook ).isNotNull();
assertThat( ingredients ).isNotNull();
dough = cook.makeADough( ingredients );
return this;
}
JGiven removes the underlines so the step will appear as the cook mangles everything to a dough in the report.
5.1.1. Nested Steps
In general, JGiven does not resolve the contents of a step method for reporting. This behavior can be changed by adding the @NestedSteps
annotation to a step method. This is useful for grouping a large set of small scale steps
into a small set of higher order steps. For instance, the code
@NestedSteps
public NestedStage I_fill_out_the_registration_form_with_valid_values() {
return I_enter_a_name("Franky")
.and().I_enter_a_email_address("franky@acme.com")
.and().I_enter_a_password("password1234")
.and().I_enter_a_repeated_password("password1234");
}
will result in the report
I fill out the registration form
I enter a name Franky
And I enter a email address franky@acme.com
And I enter a password password1234
And I enter a repeated password password1234
The nested steps will appear as a collapsible group in the HTML report.
5.2. Overriding the Default Reporting
Sometimes it is necessary to override the default way of the step reporting. For example, if you want to use special characters that are not allowed in Java methods names, such as (
, +
, or %
, you can then use the @As
annotation to specify what JGiven should put into the report.
For example, if you define the following step method:
@As( "$ % are added" )
public WhenCalculator $_percent_are_added( int percent ) {
return self();
}
The step will appear as 10 % are added
in the report, when invoked with 10
.
If you need full control over the step text you can inject the CurrentStep
interface into the stage by using the @ScenarioState
annotation.
You can then use the setName
method to change the steps text.
Note, however, that this does not work well when used within parametrized scenarios, because JGiven will not be able anymore
to generate data tables. In general, the preferred way to change the step name should be the @As
annotation.
@ScenarioState
CurrentStep currentStep;
@ScenarioState
String internalData;
public MyStage some_step() {
currentStep.setName( "some step " + internalData );
return self();
}
In addition, the @As
annotation allows declaring an AsProvider
that handles the step name generation. A default provider can be set via @JGivenConfiguration
annotation in the class referenced there.
5.3. Completely Hide Steps
Steps can be completely hidden from the report by using the @Hidden
annotation. This is sometimes useful if you need a technical method call within a scenario, which should not appear in the report.
For example:
@Hidden
public void prepareRocketSimulator() {
rocketSimulator = createRocketSimulator();
}
Note that it is useful to write hidden methods in CamelCase
to make it immediately visible in the scenario that these methods will not appear in the report.
5.3.1. Method Parameters
The @Hidden
annotation is also applicable for method parameters that shall not appear in the report.
For example:
public void setup_rocket(@Hidden String rocketName, @Hidden String rocketDescription) {
this.rocketName = rocketName;
this.rocketDescription = rocketDescription;
}
5.4. Extended Descriptions
Steps can get an extended description with the @ExtendedDescription
annotation. You can use this to give additional information about the step to the reader of the report. In the HTML report this information is shown in a tooltip.
Example:
@ExtendedDescription("Actually uses a rocket simulator")
public RocketMethods launch_rocket() {
rocketLaunched = rocketSimulator.launchRocket();
return this;
}
5.5. Intro Words
If the predefined introductionary words in the Stage
class are not enough for you and you want to define additional ones you can use the @IntroWord
annotation on a step method.
Example:
@IntroWord
public SELF however() {
return self();
}
Note that you can combine @IntroWord
with the @As
annotation. To define a ,
as an introductionary word, for example, you can define:
@IntroWord
@As(",")
public SELF comma() {
return self();
}
5.6. Filler Words
Filler words can be used to build a common and reusable vocabulary for your tests. This enables you to write beautifully fluent tests whilst at the same time keeping your stage methods clear and concise.
The @FillerWord
annotation can also be added to stage methods where you want to maintain the continuation of
sentences in reports.
For example, given the following filler words:
@FillerWord
public SELF a() {
return self();
}
@FillerWord
public SELF an() {
return self();
}
@FillerWord
public SELF and() {
return self();
}
@FillerWord
public SELF some() {
return self();
}
@FillerWord
public SELF the() {
return self();
}
You can write test stages such as:
given().the().ingredients()
.an().egg()
.some().milk()
.and().the().ingredient( "flour" );
This would generate the following report:
Given the ingredients
an egg
some milk
and the ingredient flour
Filler words can also be joined to other words in sentences. Using joinToPreviousWord
and joinToNextWord
you can create new words without trailing and leading whitespace respectively. A typical use case for using these
attributes is adding punctuation to your tests.
For example, given the following filler words:
@As(",")
@FillerWord(joinToPreviousWord = true)
public SELF comma() {
return self();
}
@As(":")
@FillerWord(joinToPreviousWord = true)
public SELF colon() {
return self();
}
@As("(")
@FillerWord(joinToNextWord = true)
public SELF open_bracket() {
return self();
}
@As(")")
@FillerWord(joinToPreviousWord = true)
public SELF close_bracket() {
return self();
}
You could write test stages such as:
given().a().open_bracket().clean().close_bracket().worksurface().comma().a().bowl().and().the().ingredients().colon()
.an().egg()
.some().milk()
.the().ingredient( "flour" );
This would generate the following report:
Given a (clean) worksurface, a bowl and the ingredients:
an egg
some milk
the ingredient flour
Please note that the annotations @IntroWord
and @FillerWord
are mutually exclusive. @IntroWord
takes precedence
over @FillerWord
; where there are occurrences of methods having both annotations @FillerWord
will be ignored.
5.7. State Sharing
Very often it is necessary to share state between steps.
As long as the steps are implemented in the same Stage class you can just use the fields of the Stage class.
But what can you do if your steps are defined in different Stage classes?
In this case you just define the same field in both Stage classes.
Once in the Stage class that provides the value of the field and once in the Stage class that needs the value
of the field.
Both fields also have to be annotated with the special annotation @ScenarioState
to tell JGiven that
this field will be used for state sharing between stages.
The values of these fields are shared between all stages that have the same field.
For example, to be able to access the value of the ingredients field of the GivenIngredients stage in the WhenCook
stage one has to annotate that field accordingly:
package com.tngtech.jgiven.examples.pancakes.test.steps;
import java.util.ArrayList;
import java.util.List;
import com.tngtech.jgiven.Stage;
import com.tngtech.jgiven.annotation.ProvidedScenarioState;
public class GivenIngredients extends Stage<GivenIngredients> {
@ProvidedScenarioState
List<String> ingredients = new ArrayList<String>();
public GivenIngredients an_egg() {
return the_ingredient( "egg" );
}
public GivenIngredients the_ingredient( String ingredient ) {
ingredients.add( ingredient );
return this;
}
public GivenIngredients some_milk() {
return the_ingredient( "milk" );
}
}
@JGivenStage
public class WhenCook extends Stage<WhenCook> {
@Autowired
@ScenarioState
Cook cook;
@ExpectedScenarioState
List<String> ingredients;
@ProvidedScenarioState
Set<String> dough;
@ProvidedScenarioState
String meal;
public WhenCook the_cook_fries_the_dough_in_a_pan() {
assertThat( cook ).isNotNull();
assertThat( dough ).isNotNull();
meal = cook.fryDoughInAPan( dough );
return this;
}
Instead of the @ScenarioState
annotation one can also use @ExpectedScenarioState
and @ProvidedScenarioState
to indicate whether the state is expected by the stage or provided by the stage.
These function in exactly the same way as @ScenarioState
but are more descriptive about what the code is doing.
5.7.1. Type vs. Name Resolution
Scenario state fields are by default resolved by its type.
That is, you can only have one field of the same type as a scenario field.
Exceptions are types from the packages java.lang.*
and java.util.*
which are resolved by the name of the field.
To change the resolution strategy you can use the resolution
parameter of the @ScenarioState
annotations. For example, to use name instead of type resolution you can write
@ScenarioState(resolution = Resolution.NAME)
.
5.7.2. Value Validation
By default, JGiven will not validate whether the value of a field of a stage that expects a value,
was actually provided by a previous stage.
The reason for this is that typically not all fields are always required for all steps.
There might be scenarios where only a part of the fields are really necessary for the steps of the scenario.
However, sometimes you know that a certain field value is needed for all steps of a stage.
In this case you can set the required
attribute of the @ScenarioState
or @ExpectedScenarioState
annotation to true
.
JGiven will then validate that a previous stage had provided the value and will throw an exception otherwise.
5.8. Having More Than 3 Stages
In many cases three stages are typically enough to write a scenario. However, sometimes more than three are required. JGiven provides two mechanism for that: stage injection and dynamic adding of stages.
5.8.1. Stage Injection
Additional stages can be injected into a test class by declaring a field with the additional stage and annotate it with @ScenarioStage
.
Please note that a lifecycle model that reuses the same instance to execute multiple test methods is only partially compatible with JGiven. In particular, in such a model stages for different scenarios will be injected into the same field. This can lead to errors when attempting to run tests in parallel.
Example
In the following example we inject an additional stage GivenAdditionalState
into the test class and use it in the test.
public class MyInjectedJGivenTest extends
ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> {
@ScenarioStage
GivenAdditionalState additionalState;
@Test
public void something_should_happen() {
given().some_state();
additionalState
.and().some_additional_state();
when().some_action();
then().some_outcome();
}
}
Note that the field access will not be visible in the report. Thus the resulting report will look as follows:
Scenario: something should happen
Given some state And some additional state When some action Then some outcome
Also note that you should not forget to first invoke an intro method, like and()
or given()
on the injected stage before calling the step method.
5.8.2. Dynamic Addition of Stages
The disadvantage of injecting a stage into a test class is that this stage will be used for all tests of that class. This might result in an overhead if the stage contains @BeforeScenario
or @AfterScenario
methods, because these methods will also be executed in the injected stages. If an additional stage is only required for a single test method you should instead dynamically add that stage to the scenario by using the addStage
method.
Example
import org.junit.Test;
import com.tngtech.jgiven.junit.ScenarioTest;
public class MyDynamicallyAddedTest extends
ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> {
@Test
public void something_should_happen() {
GivenAdditionalState additionalState = addStage(GivenAdditionalState.class);
given().some_state();
additionalState
.and().some_additional_state();
when().some_action();
then().some_outcome();
}
}
5.9. Subclassing of Stages
In practice, it often makes sense to have a hierarchy of stage classes. Your top stage class can implement common steps that you require very often, while subclasses implement more specialized steps.
One problem with subclassing stage classes is to keep the fluent interface intact. Let’s have an example:
public class GivenCommonSteps extends Stage<GivenCommonSteps> {
public GivenCommonSteps my_common_step() {
return this;
}
Now assume that we create a subclass of GivenCommonSteps
:
public class GivenSpecialSteps extends GivenCommonSteps {
public GivenSpecialSteps my_special_step() {
return this;
}
}
If you now want to use the GivenSpecialSteps
stage, you will get problems when you want to chain multiple step methods:
@Test
public void subclassing_of_stages_should_work() {
given().my_common_step()
.and().cant_do_this();
}
This code will not compile, because my_common_step()
returns GivenCommonSteps
and not GivenSpecialSteps
.
Luckily this problem can be fixed with generic types. First you have to change the GivenCommonSteps
class as follows:
public class GivenCommonStepsFixed<SELF extends GivenCommonStepsFixed<SELF>> extends Stage<SELF> {
public SELF my_common_step() {
return self();
}
}
That is, you give GivenCommonSteps
a type parameter SELF
that can be specialized by subclasses. This type is also used as return type for my_common_step()
. Instead of returning this
you return self()
, which is implemented in the Stage
class.
Now your subclass must be change as well:
public class GivenSpecialStepsFixed<SELF extends GivenSpecialStepsFixed<SELF>>
extends GivenCommonStepsFixed<SELF> {
public SELF my_special_step() {
return self();
}
}
6. Life-Cycle Methods
6.1. Scenario Life-Cycle
The life-cycle of a scenario is as follows.
-
An instance for each stage class is created
-
The
before()
methods of all scenario rules of all stages are called -
The
@BeforeScenario
-annotated methods of all stages are called -
For each stage:
-
Values are injected into all scenario state fields
-
The
@BeforeStage
-annotated methods of the stage are called -
The steps of the stage are executed
-
The
@AfterStage
-annotated methods of the stage are called -
The values of all scenario state fields are extracted.
-
-
The
@AfterScenario
-annotated methods of all stages are called. -
The
after()
methods of all scenario rules of all stages are called.
6.1.1. Integration into Test Frameworks' Lifecycles
The relative ordering of JGiven’s lifecycle methods to that of the embedding test framework depends on how JGiven is hooked into it. For JUnit4, JGiven’s before and after methods are called later than their respective framework counterparts. Especially the @After
method is executed ahead of an @AfterScenario
method or an after()
method of a scenario rule. For JUnit5 and TestNg, JGiven’s before()
method is executed later than the before method and JGiven’s after()
method is executed ahead of the frameworks' counterparts.
JGivens lifecycle works best with a per-method lifecycle of the Test Class. For such a model single and parallel execution of scenarios is fully supported. For a per-class lifecycle single-threaded execution is fully supported and parallel execution has been shown to work for Test Classes that inherit from Scenario Classes.
6.2. @BeforeScenario / @AfterScenario
6.2.1. @BeforeScenario
Methods annotated with @BeforeScenario
are executed before any step of any stage of a scenario.
This is useful for some general scenario setup.
As an example, you could initialize a WebDriver instance for Selenium tests or set up a database connection.
Note that such methods are executed before the injection of values.
Thus, the annotated code must not depend on scenario state fields.
@ProvidedScenarioState
protected WebDriver webDriver;
@BeforeScenario
public void startBrowser() {
webDriver = new HtmlUnitDriver( true );
}
6.3. @ScenarioRule
@BeforeScenario
and @AfterScenario
methods are often used to set up and tear down some resource.
If you want to reuse such a resource in another stage, you would have to copy these methods into that stage.
To avoid that, JGiven offers a concept similar to JUnit rules.
The idea is to have a separate class that only contains the before and after methods.
This class can then be reused in multiple stages.
A scenario rule class can be any class that provides the methods before()
and after()
.
This is compatible with JUnit’s ExternalResource
class, which means that you can use classes like TemporaryFolder
from JUnit directly in JGiven.
6.4. @BeforeStage / @AfterStage
6.4.1. @BeforeStage
Methods annotated with @BeforeStage
are executed after injection, but before the first step method is called.
This is useful for setting up code that is required by all or most steps of a stage.
@BeforeStage
public void setup() {
// do something useful to set up the stage
}
6.4.2. @AfterStage
Analogous to @BeforeStage
methods, methods can be annotated with @AfterStage
.
These methods are executed after all steps of a stage have been executed, but before extracting the values of scenario state fields.
Thus, you can prepare state for following stages in an @AfterStage
method.
A typical use case for this is to apply the builder pattern to build a certain object using step methods and build the final object in an @AfterStage
method.
6.4.3. Example
public class MyStage {
protected CustomerBuilder customerBuilder;
@ProvidedScenarioState
protected Customer customer;
public MyStage a_customer() {
customerBuilder = new CustomerBuilder();
return this;
}
public MyStage the_customer_has_name( String name ) {
customerBuilder.withName( name );
return this;
}
@AfterStage
public void buildCustomer() {
if (customerBuilder != null) {
customer = customerBuilder.build();
}
}
}
6.4.4. Repeatable Stage Methods
The @BeforeStage
and @AfterStage
methods come with an optional parameter that allows to execute these methods each time you enter or leave the stage, respectively.
@BeforeStage(repeatable=true)
void setup(){
//do something useful several times
}
The process of entering or leaving a stage is determined by a change of the active stage class.
Hence, in order for a repeated execution to work, a different stage has had to be executed in between two invocations of the same stage.
Therefore, the following setup only executes the @BeforeStage
once.
class RepeatableStage extends Stage<RepeatableStage>{
@BeforeStage(repeatable=true)
void beforeStageMethod(){ /*...*/}
public void given_method(){/*...*/}
public void when_method() {/*...*/}
}
class UseRepeatableStage {
//...
@Test
public void test(){
//@BeforeStage will only be executed before this invocation
repeatableStage.given().given_method();
//Still the same stage class, no @BeforeStage execution here
repeatableStage.when().when_method();
}
}
Likewise, @AfterStage
methods in such scenarios will only be executed after the last invocation of the stage.
7. Parameterized Steps
Step methods can have parameters. Parameters are formatted in reports by using the String.valueOf method, applied to the arguments. The formatted arguments are added to the end of the step description.
given().the_ingredient( "flour" ); // Given the ingredient flour
given().multiple_arguments( 5, 6 ); // Given multiple arguments 5 6
7.1. Parameters within a sentence
To place parameters within a sentence instead of the end of the sentence you can use the $ character.
given().$_eggs( 5 );
In the generated report $
is replaced with the corresponding formatted parameter. So the generated report will look as follows:
Given 5 eggs
If there are more parameters than $
characters, the remaining parameters are added to the end of the sentence.
If a $
should not be treated as a placeholder for a parameter, but printed verbatim, you can write $$
, which will appear as a single $
in the report.
7.2. Custom Annotations
The @As
annotation can be used to override the shown sentence. To reference arguments you can use the three different interoperable options which apply to every description in JGiven. For more examples look up JGiven StepFormatter Tests
Use $
to access the arguments in natural order.
@As ( "the $ fresh eggs and the $ cooked rice bowls" )
public SELF $_eggs_and_$_rice_bowls( int eggs, int riceBowls ) { ... }
Or enumerate them $1, $2, …
to have a direct reference:
@As ( "the $1 fresh eggs and the $2 cooked rice bowls" )
public SELF $_eggs_and_$_rice_bowls( int eggs, int riceBowls ) { ... }
Or reference them via the argument names:
@As ( "the $eggs fresh eggs and the $riceBowls cooked rice bowls" )
public SELF $_eggs_and_$_rice_bowls( int eggs, int riceBowls ) { ... }
The call to given().$_eggs_and_$_rice_bowls(5, 2)
will be shown as:
Given the 2 cooked rice bowls and the 5 fresh eggs
7.3. Extended Description
An extended description is shown if you hover above the step as tooltip, but hidden by default.
@ExtendedDescription ( "The $2 rice bowls were very delicious" )
public SELF $_eggs_and_$_rice_bowls( int eggs, int riceBowls ) { ... }
The call to given().$_eggs_and_$_rice_bowls(5, 2)
will still be shown as:
Given 5 eggs and 2 rice bowls
The extended description can also be set by using the CurrentStep
interface, which can be injected into a state with the @ScenarioState
annotation.
@ScenarioState
CurrentStep currentStep;
public SELF $_eggs_and_$_rice_bowls( int eggs, int riceBowls ) {
currentStep.setExtendedDescription( "The " + riceBowls + " rice bowls were very delicious" );
}
7.4. Parameter Formatting
Sometimes the toString()
representation of a parameter object does not fit well into the report. In these cases you have three possibilities:
-
Change the
toString()
implementation. This is often not possible or not desired, because it requires the modification of production code. However, sometimes this is appropriate. -
Provide a wrapper class for the parameter object that provides a different
toString()
method. This is useful for parameter objects that you use very often. -
Change the formatting of the parameter by using special JGiven annotations. This can be used in all other cases and also to change the formatting of primitive types.
7.5. The @Format annotation
The default formatting of a parameter can be overridden by using the @Format
annotation. It takes as a parameter a class that implements the ArgumentFormatter
interface. In addition, an optional array of arguments can be given to configure the customer formatter. For example, the built-in BooleanFormatter
can be used to format boolean
values:
public SELF the_machine_is_$(
@Format( value = BooleanFormatter.class, args = { "on", "off" } ) boolean onOrOff ) {
...
}
In this case, true
values will be formatted as on
and false
as off
.
7.6. Custom formatting annotations
As using the @Format
annotation is often cumbersome, especially if the same formatter is used in multiple places, one can define and use custom formatting annotations instead.
An example is the pre-defined @Quoted
annotation, which surrounds parameters with quotation marks. The annotation is defined as follows:
@Format( value = PrintfFormatter.class, args = "\"%s\"" )
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.PARAMETER, ElementType.ANNOTATION_TYPE } )
public @interface Quoted {}
As you can see, the annotation itself is annotated with the @Format
annotation as described above, which will be applied to all parameters that are annotated with @Quoted
.
7.6.1. Example
public SELF the_message_$_is_printed_to_the_console( @Quoted message ) { ... }
When invoked as
then().the_message_$_is_printed_to_the_console( "Hello World" );
Then this will result in the report as:
Then the message "Hello World" is printed to the console
7.6.2. The @AnnotationFormat annotation
Another pre-defined annotation is the @Formatf
annotation which uses the @AnnotationFormat
annotation to specify the formatter. Formatters of this kind implement the AnnotationArgumentFormatter
interface. This allows for very flexible formatters that can take the concrete arguments of the annotation into account.
@AnnotationFormat( value = PrintfAnnotationFormatter.class )
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.PARAMETER )
public @interface Formatf {
String value() default "%s";
}
7.7. The @POJOFormat annotation
When step parameter is a POJO, it may sometimes be useful to get a string representation using part or all of the fields composing this POJO.
The @POJOFormat
fulfills this need while providing a way to format fields values following customizable field formats.
Given the following POJO :
class CoffeeWithPrice {
String name;
double price_in_EUR;
CoffeeWithPrice(String name, double priceInEur) {
this.name = name;
this.price_in_EUR = priceInEur;
}
}
Then you can define a step method as follows:
public SELF the_coffee_price_$_is_registered( @POJOFormat(fieldFormats = {
@NamedFormat( name = "name", customFormatAnnotation = Quoted.class),
@NamedFormat( name = "price_in_EUR", format = @Format( value = PrintfFormatter.class, args = "%s EUR" ) )
} ) CoffeeWithPrice price ) {
...
}
where @NamedFormat
associates a format (classic @Format
or any custom format annotation) to a field by its name.
Finally, the step method can be called with an argument :
given().the_coffee_price_$_is_registered(new CoffeeWithPrice("Espresso", 2.0));
Then the report will look as follows:
Given the coffee price ["Espresso",2.0 EUR] is registered
For additional options, see the JavaDoc documentation of the @POJOFormat
annotation
7.7.1. Reuse a set of @NamedFormat definitions
When several steps use the same type of POJO in their parameters, it may be tedious to redefine this POJO fields format in each of these steps.
The solution in this case is to create a custom annotation where POJO fields formats will be declared once and for all.
This custom annotation will be itself annotated with the @NamedFormats
which will wrap as much @NamedFormat
as there are fields needing a specific formatting.
It can then further be referenced by any @POJOFormat
and @Table
annotations through their respective fieldsFormatSetAnnotation
attribute.
Given the following POJO :
class CoffeeWithPrice {
String name;
double price_in_EUR;
CoffeeWithPrice(String name, double priceInEur) {
this.name = name;
this.price_in_EUR = priceInEur;
}
}
Then you can specify a reusable set of formats for each field of this POJO through a new custom annotation :
@NamedFormats( {
@NamedFormat( name = "name", customFormatAnnotation = Quoted.class),
@NamedFormat( name = "price_in_EUR", format = @Format( value = PrintfFormatter.class, args = "%s EUR" ) )
} )
@Retention( RetentionPolicy.RUNTIME )
public @interface CoffeeWithPriceFieldsFormatSet {}
Then you will be able to reuse this custom named formats set annotation into the kind of steps below :
public SELF the_coffee_price_$_is_registered( @POJOFormat(fieldsFormatSetAnnotation = CoffeeWithPriceFieldsFormatSet.class ) CoffeeWithPrice price ) {
...
}
public SELF expected_coffee_price_for_name_$_is_$(@Quoted String coffeeName, @POJOFormat(fieldsFormatSetAnnotation = CoffeeWithPriceFieldsFormatSet.class ) CoffeeWithPrice price ) {
...
}
7.7.2. Field-level format definition
If you have full control over the POJO class, you can also specify fields format directly into the POJO class, at field level, by annotating POJO fields with any format (or chain of formats) of your choice.
JGiven will then make use of field-level format annotations within a @POJOFormat
or @Table
context of use.
Given the following POJO with field-level specified formats :
class CoffeeWithPrice {
@Quoted
String name;
@Format( value = PrintfFormatter.class, args = "%s EUR" )
double price_in_EUR;
CoffeeWithPrice(String name, double priceInEur) {
this.name = name;
this.price_in_EUR = priceInEur;
}
}
Then you can define a step method as follows:
public SELF the_coffee_price_$_is_registered(@POJOFormat CoffeeWithPrice price ) {
...
}
Finally, the step method can be called with an argument :
given().the_coffee_price_$_is_registered(new CoffeeWithPrice("Espresso", 2.0));
Then the report will look as follows:
Given the coffee price ["Espresso",2.0 EUR] is registered
Please note that @NamedFormat
specified at @POJOformat
or @Table
level have precedence over field-level defined formats.
7.8. Tables as Parameters
Sometimes information can be represented very concisely by using tables. JGiven supports this with the @Table
annotation for step parameters. Such parameters are then formatted as tables in the report. The types of such parameters can be:
-
A list of lists, where each inner list represents a single row and the first row represents the headers of the table.
-
A list of POJOs, where each POJO represents a row and the headers are inferred by the names of the fields of the POJO.
-
A single POJO, which is equivalent to a one-element list of POJOs.
7.8.1. Example
Given the following POJO:
class CoffeeWithPrice {
String name;
double price_in_EUR;
CoffeeWithPrice(String name, double priceInEur) {
this.name = name;
this.price_in_EUR = priceInEur;
}
}
Then you can define a step method as follows:
public SELF the_prices_of_the_coffees_are( @Table CoffeeWithPrice... prices ) {
...
}
Finally, the step method can be called with a list of arguments:
given().the_prices_of_the_coffees_are(
new CoffeeWithPrice("Espresso", 2.0),
new CoffeeWithPrice("Cappuccino", 2.5));
Then the report will look as follows:
Given the prices of the coffees are
| name | price in EUR | +------------+--------------+ | Espresso | 2.0 | | Cappuccino | 2.5 |
For additional options, see the JavaDoc documentation of the @Table
annotation
Also note that POJO fields formats can be specified thanks to the @Table#fieldsFormat
or @Table#fieldsFormatSetAnnotation
options.
See The @POJOFormat annotation section for more information about how to use these two options.
8. Parameterized Scenarios
JGiven scenarios can be parameterized. This is very useful for writing data-driven scenarios, where the scenarios itself are the same, but are executed with different example values.
Parameterization of Scenarios works with TestNG and JUnit, we only show it for JUnit. For TestNG it works analogous.
8.1. JUnit
JGiven supports several different ways to parameterize a JUnit test:
-
JUnit’s built-in Parametrized Runner
We use the JUnit Dataprovider in the following.
8.2. JUnit Dataprovider Runner
JUnit Dataprovider provides a JUnit test runner that enables the execution of paramterized test methods. It is similar to the way parameterized tests work in TestNG.
8.2.1. Example
@Test
@DataProvider( {
"1, 1",
"0, 2",
"1, 0"
} )
public void coffee_is_not_served( int coffees, int euros ) {
given().a_coffee_machine()
.and().the_coffee_costs_$_euros( 2 )
.and().there_are_$_coffees_left_in_the_machine( coffees );
when().I_insert_$_one_euro_coins( euros )
.and().I_press_the_coffee_button();
then().I_should_not_be_served_a_coffee();
}
The resulting report will then look as follows:
Coffee is not served
Given a coffee machine
And the coffee costs 2 euros
And there are <coffees> coffees left in the machine
When I insert <euros> one euro coins
And I press the coffee button
Then I should not be served a coffee
Cases:
| # | coffees | euros | Status |
+---+---------+-------+---------+
| 1 | 1 | 1 | Success |
| 2 | 0 | 2 | Success |
| 3 | 1 | 0 | Success |
8.3. Case As Description
If your test has multiple cases, you can use the @CaseAs
to provide additional information to the description for each case.
@Test
@DataProvider( {
"On the first run, 1, quite ok",
"And on the second run, 2, well-done"
} )
@CaseAs( "$1" )
public void coffee_making_gets_better( String description, int runNr, String result ) {
given().a_coffee_machine();
when().I_make_coffee_for_the_$_time( runNr );
then().the_result_is( result );
}
The resulting report will then look as follows:
Coffee making gets better
Given a coffee machine
When I make coffee for the <runNr> time
Then the result is <result>
Cases:
| # | Description | runNr | result | Status |
+---+-----------------------+-------+-----------+---------+
| 1 | On the first run | 1 | quite ok | Success |
| 2 | And on the second run | 2 | well-done | Success |
9. Tags
Tags are used to organize scenarios. A tag in JGiven is just a Java annotation that is itself annotated with the @IsTag
annotation. You can annotate whole test classes or single test methods with tag annotations. Tags then appear in the resulting report.
Let’s say you want to know which scenarios cover the coffee feature. To do so you define a new Java annotation:
@IsTag
@Retention( RetentionPolicy.RUNTIME )
public @interface ExampleCategory {}
Two things are important:
-
The annotation itself must be annotated with the
@IsTag
annotation to mark it as a JGiven tag. -
The annotation must have retention policy RUNTIME so that JGiven can recognize it at runtime.
To tag a scenario with the new tag, you just annotate the corresponding test method:
@ExampleSubCategory
@Test
public void tags_can_form_a_hierarchy() {
given().tags_annotated_with_tags();
when().the_report_is_generated();
then().the_tags_appear_in_a_hierarchy();
}
In the report the scenario will then be tagged with tag CoffeeFeature.
You can also annotate the whole test class, in which case all scenarios of that class will get the tag.
9.1. Descriptions
Tags can have descriptions. These descriptions appear in the report on the corresponding page for the tag. For example:
@IsTag( style = "background-color: darkgreen; color: white; font-weight: bold",
description = "Tags can be arbitrarily styled with the 'style' attribute of the '@IsTag' annotation. " +
"This tag shows how to apply such a custom style" )
@Retention( RetentionPolicy.RUNTIME )
public @interface TagsWithCustomStyle {}
9.2. Overriding the Name
It is possible to override the name of a tag by using the name
attribute of the IsTag
annotation. This allows you to have a different name for the tag than the actual type of the annotation. For example, if you want to have a tag Feature: Coffee
you can define the CoffeeFeature
annotation as follows:
@IsTag( name = "Feature: Coffee" )
@Retention( RetentionPolicy.RUNTIME )
public @interface CoffeeFeature { }
9.3. Values
Sometimes you do not want to always create a new annotation for each tag. Let’s say you organize your work in stories and for each story you want to know which scenarios have been written for that story. Instead of having a separate annotation for each story you can define a Story
annotation with a value()
method:
@IsTag( prependType = false )
@Retention( RetentionPolicy.RUNTIME )
public @interface CategoryWithValue {
String value();
}
Annotations with different values are treated as different tags in JGiven. So using the above annotation you can now mark scenarios to belong to certain stories:
@Test @Story("ACME-123")
public void scenarios_can_have_tags() {
...
}
In the report the tag will now be ACME-123
instead of Story
.
If you want that the type is prepended to the value in the report you can set the prependType
attribute of the IsTag
annotation to true
. In this case the tag will be shown as Story-ACME-123
. Note that this feature works in combination with the name
attribute.
Annotations with the same type but different values are grouped in the report. E.g. multiple @Story
tags with different values will be grouped under Story
.
9.3.1. Array Values
The value of a tag annotation can also be an array:
@IsTag
@Retention( RetentionPolicy.RUNTIME )
public @interface Story {
String[] value();
}
This allows you to give the same scenario multiple tags of the same type with different values:
@Test @Story( {"ACME-123", "ACME-456"} )
public void scenarios_can_have_tags() {
...
}
For each value, one tag will be generated, e.g. ACME-123
and ACME-456
. If you do not want that behavior you can set the explodeArray
attribute of @IsTag
to false
, in that case only one tag will be generated and the values will comma-separated, e.g. ACME-123,ACME-456
.
9.3.2. Value Dependent Description
When the description of a tag depends on its value you cannot simply set the description on the @IsTag
annotation, because it will be the same for all values.
Let’s assume you have an @Issue
tag and want to have a link to the corresponding GitHub issue in the description. To do so you can provide your own TagDescriptionGenerator
implementation that generates a description of a tag depending on its actual value:
public class IssueDescriptionGenerator implements TagDescriptionGenerator {
@Override
public String generateDescription( TagConfiguration tagConfiguration,
Annotation annotation, Object value ) {
return String.format(
"<a href='https://github.com/TNG/JGiven/issues/%s'>Issue %s</a>",
value, value );
}
}
The new IssueDescriptionGenerator
must now be configured for the @Issue
annotation using the descriptionGenerator
attribute of @IsTag
:
@IsTag( descriptionGenerator = IssueDescriptionGenerator.class )
@Retention( RetentionPolicy.RUNTIME )
public @interface Issue {
String[] value();
}
9.4. Overriding the Value
If you want to group several annotations with different types under a common name. You can combine the name
attribute with the value
attribute as follows:
@IsTag( name = "Feature", value = "Tag" )
@Retention( RetentionPolicy.RUNTIME )
public @interface FeatureTags { }
The tag will appear as Tag
in the report and all annotations with name Feature will be grouped together.
9.5. Hierarchical Tags
If your number of tags grow you typically want to organize your tags somehow. This is easily possible in JGiven by forming tag hierarchies. A tag hierarchy is defined by just annotating a tag annotation with other tags. Each of these tags will then become a parent tag in the hierarchy. For example, if you want to organize your features into ‘Core Features’ and ‘Secondary Features’ you can do so by first defining two tags @CoreFeature
and @SecondaryFeature
for each of these categories as you would define a normal JGiven tag. If you now want to define a feature tag as a Core Feature you just annotated that tag accordingly:
@CoreFeature
@IsTag
@Retention( RetentionPolicy.RUNTIME )
public @interface OneOfMyCoolCoreFeatures {}
10. Attachments
Steps can have attachments. This is useful, for example, to save screenshots during tests or to refer to larger files that should not be printed inline in the report.
10.1. Creating Attachments
There are several ways to create an attachment by using static factory methods of the Attachment
class. For example, you can create textual attachments from a given String
by using the fromText
method.
Attachment attachment = Attachment.fromText("Hello World", MediaType.PLAIN_TEXT);
10.1.1. Titles
Attachments can have an optional title which can be used by reports, for example, to show a tooltip. The title is set with the withTitle
method:
Attachment attachment = Attachment
.fromText("Hello World", MediaType.PLAIN_TEXT)
.withTitle("Some Title");
10.1.2. Binary Attachments
Binary attachments are internally stored by JGiven as Base64-encoded strings. If the binary content is already present as a Base64-encoded string one can just use that to create a binary attachment:
Attachment attachment = Attachment
.fromBase64("SGVsbG8gV29ybGQK", MediaType.application("jgiven"));
10.2. Adding Attachments to Steps
To add an attachment to a step you have to first inject the CurrentStep
class into your stage class by using @ExpectedScenarioState
.
@ExpectedScenarioState
CurrentStep currentStep;
Now you can use currentStep
inside step methods to add attachments using the addAttachment
method. The method takes as argument an instance of Attachment
.
public SELF my_step_with_attachment() {
Attachment attachment = ...
currentStep.addAttachment( attachment );
return self();
}
10.3. Example: Taking Screenshots with Selenium WebDriver
If you are using Selenium WebDriver and want to add screenshots to a JGiven step you can do so as follows:
String base64 = ( (TakesScreenshot) webDriver ).getScreenshotAs( OutputType.BASE64 );
currentStep.addAttachment( Attachment.fromBase64( base64, MediaType.PNG )
.withTitle( "Screenshot" ) );
For a full example, see the Html5AppStage class that is used by the JGiven tests.
11. Exception Handling
When writing JGiven scenarios you should know how JGiven handles exceptions. In general, JGiven captures all exceptions that are thrown in step methods. This is done, so that the steps following the erroneous step can still be executed by JGiven to show them in the report as skipped steps. After the whole scenario has been executed, JGiven will rethrow the exception that has been previously thrown, so that the overall test actually fails. This behavior is in general no problem as long as you do not really expect exceptions to happen.
When using TestNG as a test runner, JGiven does not catch exceptions. The reason is that rethrowing the exception at the end of the scenario is not working well together with the TestNG execution model. |
11.1. Expecting Exceptions
Let’s assume you want to verify that in the step method doing_some_action
a certain exception is thrown and you write the following scenario using JUnit:
// DOES NOT WORK!
@Test(expected = MyExpectedException.class)
public void an_expected_exception_should_be_thrown() {
given().some_erroneous_precondition();
when().doing_some_action();
}
This scenario has two drawbacks: first it will not work and second the generated report will not make clear that actually an exception is expected. It will not work, because the JUnit mechanism to check for an expected exception actually comes too early. As already explained above, JGiven will throw the exception itself, after the scenario has finished. As JGiven is actually implemented as a JUnit rule, throwing this exception will be after JUnit has checked for an expected exception. This technical issue can be fixed by using the ExpectedException
rule of JUnit:
@Rule
public ExpectedException rule = ExpectedException.none();
@Test
public void an_expected_exception_should_be_thrown() {
// will work, but is not visible in the report
rule.expect(MyExpectedException.class);
given().some_erroneous_precondition();
when().doing_some_action();
}
In this case, the actual verification of the exception is done after JGiven has thrown the exception and thus the ExpectedException
rule will get the exception.
11.2. A better approach
The above example has still the big disadvantage that it will not be visible in the report that an exception is actually expected. More helpful would be the following scenario:
@Test
public void an_expected_exception_should_be_thrown() {
given().some_erroneous_precondition();
when().doing_some_action();
then().an_exception_is_thrown();
}
Note, that this is scenario is still very technical and you should consider replacing the word ‘exception’ with a more domain specific term.
In order to realize the above scenario you have to explicitly catch the exception in the doing_some_action
step and store it into a scenario state field.
@ProvidedScenarioState
MyExpectedException someExpectedException;
public SELF doing_some_action() {
try {
...
} catch (MyExpectedException e) {
someExpectedException = e;
}
return self();
}
In the When-Stage you then just have to check whether the field is set:
@ExpectedScenarioState
MyExpectedException someExpectedException;
public SELF an_exception_is_thrown() {
assertThat( someExpectedException ).isNotNull();
return self();
}
12. Spock
Spock support is currently in an experimental state. This means that the API is not completely stable yet and that not all JGiven and Spock features might be supported yet. |
12.1. Install Dependency
Spock support is provided by the jgiven-spock
dependency.
12.2. Use JGiven with Spock
JGiven support for Spock is provided by extending ScenarioSpec<Given, When, Then>
and JGiven is enabled.
class SpockSpec extends ScenarioSpec<Given, When, Then> {
def "my scenario"() {
expect:
given().some_context()
when().some_action()
then().some_outcome()
}
}
12.3. Example Project
You find a complete example project on GitHub: https://github.com/TNG/JGiven/tree/master/example-projects/spock
13. Spring
Only two steps are required to use JGiven together with Spring or Spring-Boot. First, install the right dependency. Second, make your Spring Test Configuration aware of JGiven.
If you’re using JUnit, however, you need to inherit from a spring scenario class. Finally, if you want your test stages to act as Spring Beans, you need to add a special annotation to them.
13.1. Install Dependency
Spring support is provided by the jgiven-spring-junit4
and the jgiven-spring-junit5
dependencies,
depending on which JUnit version you are using. JUnit 5 is only supported with version 5 of Spring.
13.2. Configure Spring
In order to enable the JGiven Spring integration you have to tell Spring about the existence of JGiven.
13.2.1. Annotation-Based
If you are using the annotation-based configuration of Spring you can annotate your Spring
Test Configuration with the @EnableJGiven
annotation.
This is all you have to do to configure Spring for JGiven.
13.2.2. XML-Based
You can also configure JGiven with XML by adding the jgiven:annotation-driven
tag to your
Spring XML config.
Example
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jgiven="http://jgiven.org/jgiven-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://jgiven.org/jgiven-spring http://jgiven.org/schemas/jgiven-spring.xsd">
<jgiven:annotation-driven/>
</beans>
13.3. Use JGiven in JUnit-based Spring Tests
13.3.1. JUnit 4
In order to use JGiven in JUnit 4 based Spring tests, you should inherit from:
-
SpringRuleScenarioTest
-
SimpleSpringRuleScenarioTest
These base classes contain all the methods and JUnit rules necessary to run JGiven with Spring.
13.3.2. JUnit 5
To use JGiven in JUnit 5 based Spring Tests you should inherit from one of the provided tests classes:
-
SpringScenarioTest
-
SimpleSpringScenarioTest
Both classes contain all the annotations and methods necessary to run JGiven with Spring. The SimpleSpringScenarioTest
allows to have all stages in a single stage class, while the SpringScenarioTest
allows to have a class for each stage.
13.4. Stages as Spring Beans
In order to treat JGiven stages as Spring beans, e.g. if you want to inject
other Spring beans into your stages, you have to annotate
the stage class with the @JGivenStage
annotation.
13.5. Example Project
You find a complete example project, a SpringBoot webapp tested with JGiven, on GitHub: https://github.com/TNG/JGiven/tree/master/example-projects/spring-boot
14. Web Testing with Selenium
JGiven is a completely use-case agnostic testing library. So it is no surprise that you can also write Selenium-Tests with JGiven. In fact, JGiven can be considered as a perfect addition on top of Selenium: In Selenium you define the low-level interaction with the browser and HTML pages. In JGiven you define a high-level, domain specific language to specify your scenarios.
There is no additional integration module needed in order to use JGiven with Selenium
14.1. Page Objects vs Stages
It is possible to treat Selenium page objects directly as JGiven stages. This sometimes makes sense for simple cases, but from a code design perspective, it is better to separate these different concerns into multiple classes. The page object should be solely responsible for the low-level interaction with a HTML page. The stage class contains the high-level steps of the scenario and handles the state of the scenario.
14.2. Example Project
You find a complete example project on GitHub: https://github.com/TNG/JGiven/tree/master/example-projects/selenium
15. Android
JGiven can be used to test Android applications.
For unit-tests, which are not running on an Android device or emulator, you can just use jgiven-junit
.
You don’t have to do any special test setup to get that working.
15.1. Testing on an Device or Emulator (EXPERIMENTAL)
If you want to execute your tests on an Android device or emulator, for example when you want to use the Espresso test framework, then you need some additional setup.
This feature exists since version 0.14.0-RC1 and is still in an experimental status. This means that it is not tested very well yet and that the implementation might be changed in a backwards-incompatible way in future versions of JGiven. |
15.1.1. JGiven Android Dependency
First of all you need an additional project dependency to the jgiven-android
module:
dependencies {
androidTestCompile("com.tngtech.jgiven:jgiven-android:2.0.1")
}
15.1.2. Enable additional Permissions
JGiven writes JSON files during the execution of the tests to the external storage of the device or emulator.
Thus you have to enable the permission WRITE_EXTERNAL_STORAGE
when executing the tests.
You can do that by creating a file src/debug/AndroidManifest.xml
that enables that permission:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="the.package.of.my.app">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
15.1.3. AndroidJGivenTestRule
In your test classes you have to add the following additional JUnit rule:
@Rule
public AndroidJGivenTestRule androidJGivenTestRule = new AndroidJGivenTestRule(this.getScenario());
In addition, it is useful to expose the ActivityTestRule
to Stages by
using the @ScenarioState
annotation:
@Rule
@ScenarioState
public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);
15.1.4. Pull JGiven JSON files from device
When the test execution has finished, you need to pull the JGiven JSON files from the device to the local drive.
You can do that using the adb
tool. The following Gradle task does the job:
def targetDir = 'build/reports/jgiven/json'
def adb = android.getAdbExe().toString()
def reportsDir = '/storage/emulated/0/Download/jgiven-reports'
task cleanJGivenReports(type: Delete) {
delete targetDir
}
task pullJGivenReports(type: Exec, dependsOn: cleanJGivenReports) {
doFirst {
if (!file(targetDir).mkdirs()) {
println("Cannot create dir "+targetDir)
}
}
commandLine adb, 'pull', reportsDir, targetDir
doLast {
println("Pulled "+reportsDir+" to "+targetDir);
}
}
15.1.5. Generate HTML Report
Given the local JSON files, you can generate the HTML report as described in Report Generation
15.1.6. Taking Screen Shots in Espresso Tests
It is very useful to take screen shots during your tests and add them to the JGiven documentation.
When using the Espresso framework, you can use the following piece of code to take
a screenshot of an activity and turn it into a byte
array:
public class ScreenshotUtil {
public static byte[] takeScreenshot(Activity activity) {
View view = activity.getWindow().getDecorView().getRootView();
view.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
view.setDrawingCacheEnabled(false);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
return baos.toByteArray();
}
}
This byte array can then be added to a step of a JGiven report as usual:
public class MyStage {
@ScenarioState
CurrentStep currentStep;
@ScenarioState
ActivityTestRule<MainActivity> activityTestRule;
public Steps clicking_the_Click_Me_button() {
onView(withId(R.id.clickMeButton)).perform(click());
takeScreenshot();
return this;
}
@Hidden
public void takeScreenshot() {
currentStep.addAttachment(
Attachment.fromBinaryBytes(
ScreenshotUtil.takeScreenshot( activityTestRule.getActivity()),
MediaType.PNG).showDirectly()) ;
}
}
15.2. Example Project
You find a complete example project on GitHub: https://github.com/TNG/JGiven/tree/master/example-projects/android
16. JUnit 5
16.1. Known Issue
-
Dynamic tests are not reported at all. As dynamic tests in JUnit 5 do not provide life-cycle hooks, it is unclear at the moment, whether JGiven will ever support them.
16.2. Install Dependency
JUnit 5 support is provided by the jgiven-junit5
dependency.
16.3. Use JGiven with JUnit 5
JGiven support for JUnit 5 is provided by the JGivenExtension
JUnit 5 extension. You just annotate your JUnit 5 test class with @ExtendWith( JGivenExtension.class )
and JGiven is enabled.
If you just use the extension directly, you have to inject your stage classes by using
the @ScenarioStage
annotation.
@ExtendWith( JGivenExtension.class )
public class JUnit5Test {
@ScenarioStage
MyStage myStage;
@Test
public void my_scenario() {
myStage
.given().some_context()
.when().some_action()
.then().some_outcome();
}
}
Alternatively, you can use one of the test base classes ScenarioTest
,
SimpleScenarioTest
or DualScenarioTest
that provide additional convenience methods
and allows you to specify stage classes by using type parameters:
public class JGiven5ScenarioTest
extends ScenarioTest<GivenStage, WhenStage, ThenStage> {
@Test
public void JGiven_works_with_JUnit5() {
given().some_state();
when().some_action();
then().some_outcome();
}
}
Please note that JGiven is built around each scenario, i.e. test method, having its own test instance. Therefore, we cannot support JUnit’s per-class test instance lifecycle. An exception will be thrown if any such attempt is made.
16.4. Example Project
You find a complete example project on GitHub: https://github.com/TNG/JGiven/tree/master/example-projects/junit5