Example of role tests in JavaScript with Jest
Tags: programming, beginners, javascript, webdev
Published: 2023-01-23 16:05:05

---
title: Example of role tests in JavaScript with Jest
published: true
description: Applying the concept of role tests in JavaScript using Jest.
tags: 
  - programming
  - beginners
  - javascript
  - webdev
canonical_url: https://codesai.com/posts/2022/08/role-tests-jest
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89xqzvx2kyrou6ekqc4p.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-01-23 15:51 +0000
---
In this post we’ll show our last example applying the concept of role tests, this time in JavaScript using [Jest](https://jestjs.io/). Have a look at our [previous posts on this topic](https://codesai.com/publications/categories/#Role%20Testing).

This example comes from a deliberate practice session we did recently with some developers from [Audiense](https://audiense.com/) with whom we’re doing [Codesai’s Practice Program in JavaScript](https://github.com/Codesai/practice_program_js) twice a month.

Similar to what we did in our previous example of role tests in Java, we wrote the following tests to develop two different implementations of the `TransactionsRepository` port while solving the [Bank Kata](https://kata-log.rocks/banking-kata): the `InMemoryTransactionsRepository` and the `NodePersistTransactionRepository`.

These are their tests, respectively:

```javascript
import {InMemoryTransactionRepository} from "../../src/inMemoryTransactionRepository";
import {aDeposit, aWithdrawal, date} from "../helpers";

describe('inMemory transaction repository', () => {
  let repository;

  beforeEach(() => {
    repository = new InMemoryTransactionRepository();
  });

  test('should save a transaction', () => {
    const transaction = aDeposit(100, date(2022, 6, 8));

    repository.save(transaction);

    expect(repository.getAll()).toStrictEqual([transaction]);
  });

  test('should add a transaction', () => {
    const transaction = aDeposit(100, date(2022, 6, 8));
    writeTransactions([transaction]);
    const newTransaction = aDeposit(200, date(2022, 6, 9));

    repository.save(newTransaction);

    expect(repository.getAll()).toStrictEqual([transaction, newTransaction]);
  });

  test('should get empty list of transactions if none saved', () => {
    const retrievedTransactions = repository.getAll();

    expect(retrievedTransactions).toStrictEqual([])
  });

  test('should get all transactions', () => {
    const transactions = [
      aDeposit(100, date(2022, 6, 8)),
      aWithdrawal(200, date(2022, 6, 9))
    ];
    writeTransactions(transactions);

    const retrievedTransactions = repository.getAll();

    expect(retrievedTransactions).toStrictEqual(transactions)
  });

  function writeTransactions(transactions) {
    transactions.forEach(t => repository.save(t));
  }
});
```

```javascript
import crypto from "crypto";
import fs from 'fs';
import {aDeposit, aWithdrawal, date} from "../helpers";
import {NodePersistTransactionRepository} from "../../src/nodePersistTransactionRepository";

describe('node-persist transaction repository', () => {
  let repository;
  const filePath = `./tmp/${md5('transactions')}`;

  beforeEach(() => {
    fs.rmSync('./tmp', {force: true, recursive: true});
    fs.mkdirSync('./tmp');
    repository = new NodePersistTransactionRepository('./tmp');
  });

  afterEach(() => {
    fs.rmSync('./tmp', {force: true, recursive: true});
  });

  test('should save a transaction', () => {
    const transaction = aDeposit(100, date(2022, 6, 8));

    repository.save(transaction);

    expect(readTransactions()).toStrictEqual([transaction]);
  });

  test('should add a transaction', () => {
    const transaction = aDeposit(100, date(2022, 6, 8));
    writeTransactions([transaction]);
    const newTransaction = aDeposit(200, date(2022, 6, 9));

    repository.save(newTransaction);

    expect(readTransactions()).toStrictEqual([transaction, newTransaction]);
  });

  test('should get empty list of transactions if none saved', () => {
    const retrievedTransactions = repository.getAll();

    expect(retrievedTransactions).toStrictEqual([])
  });

  test('should get all transactions', () => {
    const transactions = [
      aDeposit(100, date(2022, 6, 8)),
      aWithdrawal(200, date(2022, 6, 9))
    ];
    writeTransactions(transactions);

    const retrievedTransactions = repository.getAll();

    expect(retrievedTransactions).toStrictEqual(transactions)
  });

  function readTransactions() {
    let content = fs.readFileSync(filePath, {encoding: 'utf-8'});
    return JSON.parse(content).value.map(t => ({...t, date: new Date(t.date)}));
  }

  function writeTransactions(transactions) {
    fs.writeFileSync(filePath, JSON.stringify({
      key: 'transactions',
      value: transactions
    }), {encoding: 'utf-8'});
  }

  function md5(key) {
    return crypto.createHash('md5').update(key).digest('hex');
  }
});
```

As what happened in our [previous post](https://dev.to/trikitrok/example-of-role-tests-in-java-with-junit-mkg), both tests contain the same test cases since both tests document and protect the contract of the same role, `TransactionsRepository`, which `InMemoryTransactionsRepository` and `NodePersistTransactionRepository` implement.

Again we’ll use the concept of *role tests* to remove that duplication, and make the contract of the role we are implementing more explicit.

Although [Jest](https://jestjs.io/) does not have something equivalent or similar to the [RSpec’s *shared examples* functionality](https://relishapp.com/rspec/rspec-core/v/3-10/docs/example-groups/shared-examples) we used in our [previous example in Ruby](https://dev.to/trikitrok/role-tests-for-interfaces-discovered-through-tdd-4e9l), we can get a very similar result by composing functions. 

First, we wrote the `behavesLikeATransactionRepository` function. This function contains all the test cases that document the role and protect its contract, and receives as a parameter a `testContext` object containing methods for all the operations that will vary in the different implementations of this integration test. 

```javascript
import {aDeposit, aWithdrawal, date} from "../helpers";

export function behavesLikeATransactionRepository(testContext) {
  return () => describe('behaves like a transaction repository', () => {
    let repository;
    beforeEach(() => {
      testContext.init();
      repository = testContext.getInstance();
    });

    afterEach(() => {
      testContext.clean();
    });

    test('should save a transaction', () => {
      const transaction = aDeposit(100, date(2022, 6, 8));

      repository.save(transaction);

      expect(testContext.readTransactions()).toStrictEqual([transaction]);
    });

    test('should add a transaction', () => {
      const transaction = aDeposit(100, date(2022, 6, 8));
      testContext.writeTransactions([transaction]);
      const newTransaction = aDeposit(200, date(2022, 6, 9));

      repository.save(newTransaction);

      expect(testContext.readTransactions()).toStrictEqual([transaction, newTransaction]);
    });

    test('should get empty list of transactions if none saved', () => {
      const retrievedTransactions = repository.getAll();

      expect(retrievedTransactions).toStrictEqual([])
    });

    test('should get all transactions', () => {
      const transactions = [
        aDeposit(100, date(2022, 6, 8)),
        aWithdrawal(200, date(2022, 6, 9))
      ];
      testContext.writeTransactions(transactions);

      const retrievedTransactions = repository.getAll();

      expect(retrievedTransactions).toStrictEqual(transactions)
    });
  });
}
```

Notice that in the case of [Jest](https://jestjs.io/) we are using *composition*, whereas  [we used inheritance in the case of Junit](https://dev.to/trikitrok/example-of-role-tests-in-java-with-junit-mkg).

Then, we called the `behavesLikeATransactionRepository` function from the previous tests and implemented a particular version of the methods of the `testContext` object for each test.

This is the new code of `InMemoryTransactionsRepositoryTest`:

```javascript
import {behavesLikeATransactionRepository} from "./transactionRepository.role";
import {InMemoryTransactionRepository} from "../../src/inMemoryTransactionRepository";

let repository;

describe('inMemory transaction repository', 
         behavesLikeATransactionRepository({
  init() {
    repository = new InMemoryTransactionRepository();
  },
  clean() {
  },
  getInstance() {
    return repository;
  },
  readTransactions() {
    return repository.getAll();
  },
  writeTransactions(transactions) {
    transactions.forEach(t => repository.save(t));
  }
}));
```

And this is the new code of `NodePersistTransactionRepository` after the refactoring:

```javascript
import crypto from "crypto";
import fs from 'fs';
import {NodePersistTransactionRepository} from "../../src/nodePersistTransactionRepository";
import {behavesLikeATransactionRepository} from "./transactionRepository.role";

const filePath = `./tmp/${md5('transactions')}`;

describe('node-persist transaction repository', 
          behavesLikeATransactionRepository({
  init() {
    fs.rmSync('./tmp', {force: true, recursive: true});
    fs.mkdirSync('./tmp');
  },
  clean() {
    fs.rmSync('./tmp', {force: true, recursive: true});
  },
  getInstance() {
    return new NodePersistTransactionRepository('./tmp');
  },
  readTransactions() {
    let content = fs.readFileSync(filePath, {encoding: 'utf-8'});
    return JSON.parse(content).value.map(t => ({...t, date: new Date(t.date)}));
  },
  writeTransactions(transactions) {
    fs.writeFileSync(filePath, JSON.stringify({
      key: 'transactions',
      value: transactions
    }), {encoding: 'utf-8'});
  }
}));

function md5(key) {
  return crypto.createHash('md5').update(key).digest('hex');
}
```

This new version of the tests not only reduces duplication, but also makes explicit and protects the behaviour of the `TransactionsRepository` role. It also makes less error prone the process of adding a new implementation of `TransactionsRepository` because just by using the `behavesLikeATransactionRepository` function, you’d get a checklist of the behaviour you need to implement in order to ensure substitutability, i.e., to ensure the  [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle) is not violated. 

These role tests using composition are also more readable than [the Junit ones](https://dev.to/trikitrok/example-of-role-tests-in-java-with-junit-mkg), in my opinion at least :)


<h2>Acknowledgements.</h2>

I’d like to thank [Audiense](https://audiense.com/)’s deliberate practice group for working with us on this kata, and my colleague [Rubén Díaz](https://twitter.com/rubendm23/) for co-facilitating the practice sessions with me.

Thanks to my Codesai colleagues for reading the initial drafts and giving me feedback, and to [Elina Sazonova](https://www.pexels.com/es-es/@elina-sazonova/) for the picture.

<h2>References.</h2>

* [Role tests for implementation of interfaces discovered through TDD](https://dev.to/trikitrok/role-tests-for-interfaces-discovered-through-tdd-4e9l), [Manuel Rivero](https://twitter.com/trikitrok)
* [Example of role tests in Java with Junit
](https://dev.to/trikitrok/example-of-role-tests-in-java-with-junit-mkg), [Manuel Rivero](https://twitter.com/trikitrok)
* [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle) 

Photo from [Elina Sazonova in Pexels](https://www.pexels.com/es-es/@elina-sazonova/)