@hirez_io/jasmine-single
TypeScript icon, indicating that this package has built-in type declarations

1.1.6 • Public • Published

@hirez_io/jasmine-single 📃👌

A jasmine addon that helps you write 'Single-Action Tests' by breaking them into a "given / when / then" structure.

npm version npm downloads codecov Build and optionally publish lerna Code of Conduct License: MIT All Contributors


Table of Contents


Installation

yarn add -D @hirez_io/jasmine-single

or

npm install -D @hirez_io/jasmine-single

Using TypeScript?

CLICK HERE TO EXPAND

You should add @hirez_io/jasmine-single to your types property under compilerOptions in your tsconfig.json (or tsconfig.spec.json) like this:

// tsconfig.json or tsconfig.spec.json

{
  ...
  "compilerOptions": {
    "types": [
      "jasmine",
      "@hirez_io/jasmine-single", // 👈 ADD THIS

      // ...any other types you might have...
    ],
    ...
  }
  ...
}

ATTENTION: If you have typeRoots configured like this -

"compilerOptions": {
  "typeRoots": [
    "node_modules/@types"
  ],
}

You should add "node_modules" like this -

"compilerOptions": {
  "typeRoots": [
    "node_modules/@types",
    "node_modules/@hirez_io" // 👈 ADD THIS
  ],
}

or else it won't find @hirez_io/jasmine-single global types.

VS CODE USERS:

Add the above configuration (types and/or typeRoots) to your tsconfig.json specifically or else it would not recognize the global types.


Using karma?

CLICK HERE TO EXPAND

@hirez_io/jasmine-single has a dependency on @hirez_io/karma-jasmine-single which is a karma plugin (inspired by karma-jasmine-given) I rewrote to save you the hassle of loading the library script yourself.

So it will automatically installs @hirez_io/karma-jasmine-single for you 😎

Here's how to modify your karma.conf.js:

// karma.conf.js

module.exports = function(config) {
  config.set({

    plugins: [
      require('karma-jasmine'),
      require('@hirez_io/karma-jasmine-single'), // 👈 ADD THIS
      require('karma-chrome-launcher')
      // other plugins you might have...
    ],

    frameworks: [
      '@hirez_io/jasmine-single', // 👈 ADD THIS
      'jasmine',
      // other frameworks...
    ],

    // ...

What are "single-action" tests?

A single-action test is a test with only one action. (CAPTAIN OBVIOUS! 🦸‍♂️😅)

Normally, you setup the environment, you call an action and then you check the output.

What's an action?

Well... it can be a method call, a button click or anything else our test is checking.

The big idea here is that it should be only ONE ACTION PER TEST.


Why writing single-action tests is good?

Single action tests are more "Test Effective" compared to multi-action tests.

The benefits of single-action tests:

✅ Your tests will break less often (making them more effective)

✅ Whenever something breaks, you have only one "action" code to debug

✅ They promote better coverage (easier to see which cases are still missing)

✅ They give you better structure (every part of your test has a clear goal)


How to write single-action tests?

This library follows the natural "given, when and then" structure (some of you know it as "Arrange, Act, Assert").

This means every test has only 3 parts to it, no more.

describe('addTwo', () => {
  
  // This is where you setup your environment / inputs
  given('first number is 1', () => {
    const firstNum = 1;

    // This is where you call the action under test
    when('adding 2 to the first number', () => {
      const actualResult = addTwo(firstNum);

      // This is where you check the outcome
      then('result should be 3', () => {
        expect(actualResult).toEqual(3);
      });
    });
  });

});

It also prints a nicer test description when there's an error:

CONSOLE OUTPUT:
~~~~~~~~~~~~~~

GIVEN first number is 1
WHEN adding 2 to the first number
THEN result should be 3

What's wrong with using it() for single-action tests?

Did you know that the most common way of writing JavaScript tests dates back to 2005? 😅

Jasmine, which was created in 2009 was inspired by Ruby's testing framework - RSpec which was created in 2005.

Originally, RSpec introduced the syntax of "describe > context > it", where context was meant to be used as the "setup" part of the test.

Unfortunately, the context wasn't ported to Jasmine so we got used to writing our tests in the "describe > it" structure... which is more limited.


Here are a couple of limitations with the common it() structure:

❌ 1. It promotes partial or awkward descriptions of tests

The word "it" kinda forces you to begin the description with "should" which leads to focusing specifically on just the "outcome" part of the test (the then).

But if you want to add more context (like what should be the input that causes that outcome) things start to get messy.

Because there isn't a clear convention, people tend to invent their own on the fly which creates inconsistency.

Example:

it('should do X only when environment is Y and also called by Z But only if...you get the point', ()=> ...)

❌ 2. Nothing prevents you from writing multi-action tests

This mixes up testing structures and making them harder to understand

Example:

it('should transform the products', ()=> {

  // SETUP
  const fakeProducts = [...];

  // ACTION
  const result = classUnderTest.transformProducts(fakeProducts);

  // OUTCOME
  const transformedProducts = [...];
  expect(result).toEqual(transformedProducts);

  // ACTION
  const result2 = classUnderTest.transformProducts();

  // OUTCOME
  expect(result2).toEqual( [] );


  // this 👆 is a multi-action test.

})

❌ 3. Detailed descriptions can get out of date more easily

The farther the description is from the actual implementation the less likely you'll remember to update it when the test code changes

Example:

test('GIVEN valid products and metadata returned successfully WHEN destroying the products THEN they should get decorated', ()=> {

  const fakeProducts = [...];
  const fakeMetadata = [...];
  mySpy.getMetadata.and.returnValue(fakeMetadata);

  const result = classUnderTest.transformProducts(fakeProducts);

  const decoratedProducts = [...];
  expect(result).toEqual(decoratedProducts);

})

Did you spot the typo? 👆😅

(it should be "transforming" instead of "destroying")


Compare that to -

  given('valid products and metadata returned successfully', () => {
    const fakeProducts = [...];
    const fakeMetadata = [...];
    mySpy.getMetadata.and.returnValue(fakeMetadata);
    
    //        👇 --> easier to spot as it's closer to the implementation
    when('destroying the products', () => { 
      const result = classUnderTest.transformProducts(fakeProducts);

      then('they should get decorated', () => {
        const decoratedProducts = [...];
        expect(result).toEqual(decoratedProducts);
      });
    });
  });

Usage

▶ The basic testing structure

The basic structure is a nesting of these 3 functions:

given(description, () => {
  when(description, () => {
    then(description, () => {
 
    })
  })
})
  
    

EXAMPLE:

describe('addTwo', () => {
  
  // This is where you setup your environment / inputs
  given('first number is 1', () => {
    const firstNum = 1;

    // This is where you call the action under test
    when('adding 2 to the first number', () => {
      const actualResult = addTwo(firstNum);

      // This is where you check the outcome
      then('result should be 3', () => {
        expect(actualResult).toEqual(3);
      });
    });
  });

});

Under the hood it creates a regular it() test with a combination of all the descriptions:

CONSOLE OUTPUT:
~~~~~~~~~~~~~~

GIVEN first number is 1
WHEN adding 2 to the first number
THEN result should be 3

▶ Meaningful error messages

This library will throw an error if you deviate from the given > when > then structure.

So you won't be tempted to accidentally turn your single-action test into a multi-action one.

describe('addTwo', () => {
  
    // 👉 ATTENTION: You cannot start with a "when()" or a "then()"
    //                the test MUST start with a "given()"


  given('first number is 1', () => {
    const firstNum = 1;

    // 👉 ATTENTION: You cannot add here a "then()" function directly 
    //                or another "given()" function

    when('adding 2 to the first number', () => {
      const actualResult = addTwo(firstNum);

      // 👉 ATTENTION: You cannot add here a "given()" function
      //                or another "when()" function

      then('result should be 3', () => {
        expect(actualResult).toEqual(3);


        // 👉 ATTENTION: You cannot add here a "given()" function
        //                or a "when()" function or another "then()"
      });
    });
  });

});

async / await support

Example:

describe('addTwo', () => {

  given('first number is 1', () => {
    const firstNum = 1;
    //                                    👇
    when('adding 2 to the first number', async () => {
      const actualResult = await addTwo(firstNum);

      then('result should be 3', () => {
        expect(actualResult).toEqual(3);
      });
    });
  });

});

done() function support

The given function supports the (old) async callback way of using a done() function to signal when the test is completed.

describe('addTwo', () => {
  //                           👇
  given('first number is 1', (done) => {
    const firstNum = 1;

    when('adding 2 to the first number', () => {
      const actualResult = addTwo(firstNum, function callback() {

      then('result should be 3', () => {
          expect(actualResult).toEqual(3);
          done();
        });

      });
    });

  });

});

ℹ It also supports done(error) or done.fail(error) for throwing async errors.


Contributing

Want to contribute? Yayy! 🎉

Please read and follow our Contributing Guidelines to learn what are the right steps to take before contributing your time, effort and code.

Thanks 🙏


Code Of Conduct

Be kind to each other and please read our code of conduct.


Contributors ✨

Thanks goes to these wonderful people (emoji key):


Shai Reznik

💻 📖 🤔 🚇 🚧 🧑‍🏫 👀 ⚠️

Maarten Tibau

📖 🚇

Benjamin Gruenbaum

💻 🤔 🚧

This project follows the all-contributors specification. Contributions of any kind welcome!


License

MIT

Package Sidebar

Install

npm i @hirez_io/jasmine-single

Weekly Downloads

1

Version

1.1.6

License

MIT

Unpacked Size

58.7 kB

Total Files

14

Last publish

Collaborators

  • hirez.io