Introduction
JsGiven is a JavaScript library that helps you to design a high-level, domain-specific language for writing BDD scenarios. You still use your favorite javascript test runner, your favorite assertion library and mocking library for writing your test implementations, but you use JsGiven to write a readable abstraction layer on top of it.
It’s a JavaScript port of JGiven (written in Java). JsGiven keeps the JGiven philosophy, concepts and uses its html5 reporting tool.
Installation
JsGiven is released on NPM and can be installed with either NPM or Yarn.
JsGiven should usually be installed as a devDependency as it’s not directly contributing as project dependency.
$ yarn add -D js-given (1) $ npm install --save-dev js-given (2)
Requirements
JsGiven works with classic JS test runners (Jest, Jasmine, Mocha, Ava, Protractor).
JsGiven requires Node v6.x or more to run.
Getting started
Importing JsGiven
JsGiven functions or classes are available as named exports of the 'js-given' module.
import { scenario, scenarios, setupForRspec, Stage } from 'js-given';
Set up JsGiven
JsGiven needs to be setup in each test source file by calling a setup function.
It is necessary to do this in each test source file as some tests frameworks (Jest and probably Ava) actually run each test source file in a worker process in order to get parallel execution of tests. |
For Rspec inspired frameworks (Jest, Mocha, Jasmine, Protractor)
In frameworks inspired by RSpec, JsGiven must be setup by calling the setupForRspec() function. This function takes the describe and the it function from the test framework.
setupForRspec(describe, it);
For Ava framework
In Ava, JsGiven must be setup by calling the setupForAva() function. This function takes the test from Ava.
const test = require('ava');
setupForAva(test);
Create a scenario group
First of all you create a scenario group by calling the scenarios() function.
scenarios('sum', SumStage, ({ given, when, then }) => {
});
-
The first parameter is the group name, it identifies your scenario within the report.
-
You can use a "namespace" or "java package" naming with dots to introduce a hierarchy that will be presented in the html5 report (eg: analytics.funnels.tickets_sales)
-
-
The second parameter is the stage class you will create very soon.
-
The last parameter is a function that takes an object containing the given(), when(), then() methods and returns the scenarios object.
Create a Stage class
You now have to create a "Stage" class that extends the Stage base class.
class SumStage extends Stage {
a_number(value) {
this.number1 = value;
return this;
}
another_number(value) {
this.number2 = value;
return this;
}
they_are_summed() {
this.result = this.number1 + this.number2;
return this;
}
the_result_is(expectedResult) {
expect(this.result).toEqual(expectedResult);
return this;
}
}
A stage class contains multiple step methods that can be called in a scenario and that will appear in the report. Step methods are the heart of JsGiven. The test initialization, execution and assertions must be implemented within step methods.
Every non-static method of a stage class that returns the stage instance (this) can be used as a step method.
Step methods must return the this reference!
This way JsGiven knowns which methods should be included in the report. Internal private methods usually do not return this. Since there are is no concept of private methods in JavaScript, this is a major difference from JGiven. |
There are no further requirements.
In addition, a step method should be written in snake_case
This way JsGiven knows the correct casing of each word of the step. |
Write your first scenario
Now you can write your first scenario
scenarios('sum', SumStage, ({ given, when, then }) => {
return {
two_numbers_can_be_added: scenario({}, () => {
given()
.a_number(1)
.and()
.another_number(2);
when().they_are_summed();
then().the_result_is(3);
}),
};
});
The scenario() methods takes two parameters :
-
The first parameter is an object with the optional parameters (such as tags or scenario extended description) (Both are not implemeted yet).
-
The second parameter is the scenario description function.
Execute your scenario
You can execute your scenario by running your test runner as you usually run the tests.
$ jest PASS ./sum.test.js sum ✓ Two numbers can be added (9ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.7s, estimated 1s Ran all test suites.
Report generation
JsGiven produces internal reports in JSON, that are not meant to be presented to users.
JsGiven converts these internal reports to a fully usable JGiven report.
Setting up the test npm scripts
Due to the parallel nature of some test runners (Jest & Ava), there is no simple way to generate the reports after running the tests.
Therefore, you have to set up 3 npm scripts that will :
-
Clean the internal reports before starting the tests.
-
Run the tests with your usual test running and generate the report if the tests have failed
-
Generate the html5 report after the tests have successfully run.
You have to include the following scripts in you package.json file:
"scripts": {
"pretest": "jsgiven clean",
"test": "your_test_command || jsgiven report --fail", (1)
"posttest": "jsgiven report"
}
1 | Where your_test_command is your test runner command (mocha, jest, jasmine or another one). |
The jsgiven command is a CLI command tool provided by the module.
Step methods
Completely hide steps
Steps can be completely hidden from the report by using the @Hidden decorator. This is sometimes useful if you need a technical method call within a scenario, which should not appear in the report.
For example:
@Hidden
buildTechnicalObject(value) {
return this;
}
It is useful to write hidden methods in CamelCase
This will make it immediately visible in the scenario that these methods will not appear in the report. |
There is an alternative to decorators that you can use if you don’t want to use decorators. See Hide steps without decorators
In order to use hidden step with decorators, you have to setup decorators: See Configuration to use decorators
Hide steps without decorators
You can also declare the step methods you want to hide by using Hidden.addHiddenStep() static method instead of using the decorator.
class StageClass extends Stage {
aCompletelyHiddenStep(): this {
return this;
}
}
Hidden.addHiddenStep(StageClass, 'aCompletelyHiddenStep');
Stages and state sharing
In the previous example you have included all step methods in the same class. While this is a simple solution, it’s often suitable to have several stage classes.
Create Given/When/Then Stage classes
JsGiven allows to use 3 Stage classes.
You can declare the classes in your scenario
scenarios(
'sum',
[SumGivenStage, SumWhenStage, SumThenStage],
({ given, when, then }) => {
And use all the methods of each stage in the given(), when(), then() chains:
scenarios(
'sum',
[SumGivenStage, SumWhenStage, SumThenStage],
({ given, when, then }) => {
return {
two_numbers_can_be_added: scenario({}, () => {
given()
.a_number(1)
.and()
.another_number(2);
when().they_are_summed();
then().the_result_is(3);
}),
};
}
);
Sharing state between stages
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.
The recommended approach is to use a the special decorator @State. Both fields also have to be annotated with the special decorator @State to tell JsGiven 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 with the @Stage decoration.
class SumGivenStage extends Stage {
@State number1;
@State number2;
a_number(value) {
this.number1 = value;
return this;
}
another_number(value) {
this.number2 = value;
return this;
}
}
class SumWhenStage extends Stage {
@State number1;
@State number2;
@State result;
they_are_summed() {
this.result = this.number1 + this.number2;
return this;
}
}
class SumThenStage extends Stage {
@State result;
the_result_is(expectedResult) {
expect(this.result).toEqual(expectedResult);
return this;
}
}
There is an alternative to decorators that you can use if you don’t want to use decorators. See Using state sharing without decorators
In order to use state sharing with decorators, you have to setup decorators: See Configuration to use decorators
Using state sharing without decorators
You can declare the state properties to be shared using the State.addProperty() static method.
class SumGivenStage extends Stage {
a_number(value) {
this.number1 = value;
return this;
}
another_number(value) {
this.number2 = value;
return this;
}
}
State.addProperty(SumGivenStage, 'number1');
State.addProperty(SumGivenStage, 'number2');
class SumWhenStage extends Stage {
they_are_summed() {
this.result = this.number1 + this.number2;
return this;
}
}
State.addProperty(SumWhenStage, 'number1');
State.addProperty(SumWhenStage, 'number2');
State.addProperty(SumWhenStage, 'result');
class SumThenStage extends Stage {
the_result_is(expectedResult) {
expect(this.result).toEqual(expectedResult);
return this;
}
}
State.addProperty(SumThenStage, 'result');
Both fields in the different classes have to be marked as state properties with the State.addProperty() static method.
The values of these fields are shared between all stages that have the same state properties.
Parameterized steps
Step methods can have parameters. Parameters are formatted in reports by using the toString() 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
Parameters within a sentence
To place parameters within a sentence instead 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.
given().$$_$( 5); // Given $ 5
Parameters formatting
Sometimes the default toString() representation of a parameter does not fit well into the report. In these cases you have several possibilities:
-
Change the toString() implementation. This is often not possible or not desired, because it requires patching primitive types or the modification of production code.
-
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, but it makes the scenario code a bit more complex.
-
Change the formatting of the parameter by using special JSGiven annotations. This can be used in all other cases and also to change the formatting of primitive types.
Writing your formatter
You can write your own formatter using the buildParameterFormatter() function:
const LoudFormatter = buildParameterFormatter(
text => text.toUpperCase() + ' !!!'
);
You can then provide your formatter implementation that will convert the parameter to a string to be presented in the report
Using formatters
You can use your formatter as a decorator that takes the parameter name.
class MyStage extends Stage {
@LoudFormatter('value')
a_value(value) {
return this;
}
}
The decorator takes the parameter names (you can supply multiple parameter names as a rest parameter https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) )
When used in a scenario:
parameters_can_be_formatted: scenario({}, () => {
given().a_value('hello world');
// Will be converted to
// Given a value HELLO WORLD !!!
when();
then();
}),
There is an alternative to decorators that you can use if you don’t want to use decorators. See Using formatters without decorators
In order to use formatters with decorators, you have to setup decorators: See Configuration to use decorators
Using formatters without decorators
Instead of using the decorator, you can call the formatParameter() static method. This method takes three parameters:
-
The stage class
-
The step method name
-
The parameter names (you can supply multiple parameter names as a rest parameter https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters)
class MyStage extends Stage {
a_value(value) {
return this;
}
}
LoudFormatter.formatParameter(MyStage, 'a_value', 'value');
Parameterized formatters
Sometimes your formatter implementation may need a custom parameter that you will need to pass in each stage (a date format, a currency name …)
With higher order functions (https://en.wikipedia.org/wiki/Higher-order_function), you can make your formatter accept parameters: This way you will write a function that accepts parameters and will call buildParameterFormatter().
When using it, you first call your function, then call the decorator:
const LoudFormatter = bangCharacter =>
buildParameterFormatter(text => text.toUpperCase() + `${bangCharacter}`);
class MyStage extends Stage {
@LoudFormatter('!')('value')
a_value(value) {
return this;
}
}
This technique works as well, without decorators :
const LoudFormatter = bangCharacter =>
buildParameterFormatter(text => text.toUpperCase() + `${bangCharacter}`);
class MyStage extends Stage {
a_value(value) {
return this;
}
}
LoudFormatter('!').formatParameter(MyStage, 'a_value', 'value');
Providers formatters
JsGiven provides three default formatters:
Quoted
@Quoted('message')
the_message_$_is_printed_to_the_console(message) {
return this;
}
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
QuotedWith
QuotedWith is a generalization of the Quote formatter that allows you to choose your quote charater.
@QuotedWith("'")('message')
the_message_$_is_printed_to_the_console(message) {
return this;
}
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
NotFormatter
NotFormatter allows you to write english positive or negative sentence bases on boolean values
@NotFormatter('present')
the_message_is_$_displayed_to_the_user(present) {
return this;
}
When invoked with true:
then().the_message_is_$_displayed_to_the_user(true);
Then this will result in the report as:
Then the message is displayed to the user
When invoked with false:
then().the_message_is_$_displayed_to_the_user(false);
Then this will result in the report as:
Then the message is not displayed to the user
Parameterized scenarios
JsGiven 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.
As most JS test frameworks do not include build-in helpers for parameterized tests, JsGiven provides a simple API to include test data and handle the different cases execution.
Instead of providing a scenario method, you use the parametrized() function, which takes two arguments:
-
An array of parameters tuples (an array of array containing parameter values)
-
The scenario function that takes as many parameters as the tuple size
scenarios('parametrized-scenarios', DemoStage, ({ given, when, then }) => {
return {
scenarios_can_be_parametrized: scenario(
{},
parametrized([[1, 2], [2, 4], [3, 6]], (value, result) => {
given()
.a_number(value)
.and()
.another_number(value);
when().they_are_summed();
then().the_result_is(result);
})
),
};
});
When the scenario is run, the three cases are executed :
$ jest PASS ./parameterized-scenarios.test.js parametrized-scenarios ✓ Scenarios can be parametrized #1 (8ms) ✓ Scenarios can be parametrized #2 (1ms) ✓ Scenarios can be parametrized #3 (2ms) Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 0.71s, estimated 1s Ran all test suites matching "parameterized".
If you only have one parameter, you can use the parametrized1() function which accepts an array of single values instead of tuples:
scenarios('parametrized-scenarios', DemoStage, ({ given, when, then }) => {
return {
scenarios_can_be_parametrized_with_only_one_value: scenario(
{},
parametrized1([1, 2, 3], value => {
given()
.a_number(value)
.and()
.another_number(value);
when().they_are_summed();
then().they_are_successfully_added();
})
),
};
});
Usage with Flow or TypeScript
In order to ensure proper typing between the parameters and their uses in the scenario function, you should use the specialized parametrizedN() functions.
Depending on the number of parameters, you can use the specialized parameterizedN() function.
Here is an example of the parameterized2() function :
scenarios('parametrized-scenarios', DemoStage, ({ given, when, then }) => {
return {
scenarios_can_be_parametrized: scenario(
{},
parametrized2(
[[1, 2], [2, 4], [3, 6]],
(value: number, result: number) => {
given()
.a_number(value)
.and()
.another_number(value);
when().they_are_summed();
then().the_result_is(result);
}
)
),
};
});
This way, the type-checker will be able to detect type errors between the parameter values and their uses in the scenario & step methods.
Asynchronous testing
JsGiven supports asynchronous testing.
Asynchronous scenarios in JsGiven are written the same way synchronous scenarios are written :
-
Scenarios functions remain fully synchronous and are still calling step methods synchronously.
-
Step methods must still return the this reference.
However when a step method must perform some asynchronous work, it has to call the doAsync() function. This functions accepts a function that returns a promise (or an async function: as it’s the same signature).
Further step methods can be synchronous or asynchronous there is no need to use doAsync() in further step methods. JsGiven will continue the scenario execution and execute the further step methods once the asynchronous execution is done.
Example using async functions :
class AsyncStage extends Stage {
the_url(url) {
this.url = url;
return this;
}
making_an_http_request_to_that_url() {
doAsync(async () => {
const { statusCode } = await httpRequest(this.url);
this.statusCode = statusCode;
});
return this;
}
the_status_code_is(expectedStatusCode) {
expect(this.statusCode).toEqual(expectedStatusCode);
return this;
}
}
scenarios('async', AsyncStage, ({ given, when, then }) => {
return {
an_async_scenario_can_be_executed: scenario({}, () => {
given().the_url('https://jsgiven.org');
when().making_an_http_request_to_that_url();
then().the_status_code_is(200);
}),
};
});
Example using promises :
class PromiseStage extends Stage {
the_url(url) {
this.url = url;
return this;
}
making_an_http_request_to_that_url() {
doAsync(() => {
return httpRequest(this.url).then(({ statusCode }) => {
this.statusCode = statusCode;
});
});
return this;
}
the_status_code_is(expectedStatusCode) {
expect(this.statusCode).toEqual(expectedStatusCode);
return this;
}
}
scenarios('async', PromiseStage, ({ given, when, then }) => {
return {
an_async_scenario_with_promises_can_be_executed: scenario({}, () => {
given().the_url('https://jsgiven.org');
when().making_an_http_request_to_that_url();
then().the_status_code_is(200);
}),
};
});
Required configuration for asynchronous testing
JsGiven relies on promises.
With Babel
If you’re writing async/await code, make sure it is properly transpiled or you are using Node 8.
You will need to have a promise implementation (a native or polyfilled one) If you use Babel directly with your test runner, Babel already includes polyfills for missing implementations. If not ensure they are included.
With TypeScript
If you’re writing async/await code, make sure it is properly transpiled by TypeScript.
Depending on the target, you may have to include the "es2015.promise" library in you tsconfig.json
"lib": [
"es5",
"es2015.promise"
],
Using JsGiven
Supported Test runners
JsGiven supports the following test runners :
Configuration to use decorators
All step-related advanced features (state-sharing, hiding steps, formatters …) are best used with decorators. In order to enable decorators, some configuration is required.
With Babel
In order to use decorators, you have to include the following babel transform plugins in your babel configuration.
-
transform-decorators-legacy
-
transform-class-properties
{
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties", "transform-regenerator"]
}
You are not forced to use this as your .babelrc production configuration
You can use the decorators configuration only in your test setup (See https://babeljs.io/docs/usage/babelrc/#env-option). Read your test framework documentation to see how you can achieve this. |
With TypeScript
In order to use state sharing with decorators, you have to enable the "experimentalDecorators" option in your tsconfig.json
"experimentalDecorators": true
Type checkers
With Flow
JsGiven includes build-in support for the Flow type checker. You don’t have to install any type definitions.
A working example using Flow is provided : https://github.com/jsGiven/jsGiven/tree/master/examples/jest-es2015-flow
JsGiven is internally written with Flow.
With TypeScript
JsGiven includes build-in TypeScript definitions. You don’t have to install any type definitions.
A working example is provided : https://github.com/jsGiven/jsGiven/tree/master/examples/jest-typescript
Using JSGiven in pure ES5
Js Given is usable with ES5.
You can import JsGiven using regular require() calls:
var JsGiven = require('js-given');
var scenarios = JsGiven.scenarios;
var scenario = JsGiven.scenario;
var setupForRspec = JsGiven.setupForRspec;
var Stage = JsGiven.Stage;
You can declare the stage classes using classical prototypal inheritance.
function SumStage() {}
SumStage.prototype = {
a_number: function(value) {
this.number1 = value;
return this;
},
another_number: function(value) {
this.number2 = value;
return this;
},
they_are_summed: function() {
this.result = this.number1 + this.number2;
return this;
},
the_result_is: function(expectedResult) {
expect(this.result).to.equal(expectedResult);
return this;
},
};
Object.setPrototypeOf(SumStage.prototype, Stage.prototype);
Object.setPrototypeOf(SumStage, Stage);
You use JSGiven almost like in ES6
scenarios('sum', SumStage, function(it) {
return {
two_numbers_can_be_added: scenario({}, function() {
it
.given()
.a_number(1)
.and()
.another_number(2);
it.when().they_are_summed();
it.then().the_result_is(3);
}),
};
});
You can find a working example based on Mocha.
Fully working examples
Some examples are committed on the JsGiven repository
These examples are tested on each commit against the latest stable version and against the current code by the Travis CI integration