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)
1 With Yarn
2 With NPM

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 }) => {
});
  1. 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)

  2. The second parameter is the stage class you will create very soon.

  3. 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 :

  1. The first parameter is an object with the optional parameters (such as tags or scenario extended description) (Both are not implemeted yet).

  2. 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:

  1. The stage class

  2. The step method name

  3. 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 :

JsGiven is tested internally using those frameworks Build Status

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.

JsGiven is internally written with Flow.

With TypeScript

JsGiven includes build-in TypeScript definitions. You don’t have to install any type definitions.

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