Unit testing

The shoppingcart tutorial includes logic for basic unit testing using the mocha test framework and the chai assertion library. The testkit.js file includes tests for the event-sourced entity commands and events, internally tracking the state, and maintaining an event log.

1. Set up the testing framework

  1. Create a test directory in the shoppingcart directory.

    By default the mocha test framework looks for the test directory. It is a best practice to consistently name the test folder test.
  2. From the command line, download the testkit.js file into the test directory:

    curl -o ./test/testkit.js -L https://raw.githubusercontent.com/lightbend/akkaserverless-javascript-sdk/main/npm-js/create-akkasls-entity/template/base/test/testkit.js
  3. In the test directory, create a cart.test.js file.

  4. Add the import statements:

    import { MockEventSourcedEntity } from './testkit.js';
    import { expect } from 'chai';
    import cart from '../cart.js';
  5. Add a describe function to contain the shoppingcart unit tests:

    describe('Test Cart', () => {
    
    });
  6. Add the following constants in the describe function to reuse messages:

    const entityId = '1';
    const newItem = { 'userId': entityId, 'productId': 'turkey', 'name': 'delicious turkey', 'quantity': 2 }
    const removeItem = { 'userId': entityId, 'productId': 'turkey' }
  7. Add a child describe function:

    describe('Commands', () => {
      // Add tests here
    });
  8. Add test cases to validate an item added to the cart:

    it('Should add an item...', () => {
        const entity = new MockEventSourcedEntity(cart, entityId);
        const result = entity.handleCommand('AddItem', newItem);
    
        // The cart returns an empty message
        expect(result).to.be.empty;
    
        // There shouldn't be any errors
        expect(entity.error).to.be.undefined;
    
        // The new state of the entity should match the added item
        expect(entity.state.items[0].name).to.equal(newItem.name);
    
        // There should be one event
        expect(entity.events.length).to.be.equal(1)
        expect(entity.events[0].constructor.name).to.be.equal('ItemAdded')
    })
  9. Add test cases to validate removing an item from the cart:

    it('Should remove an item...', () => {
        const entity = new MockEventSourcedEntity(cart, entityId);
        let result = entity.handleCommand('AddItem', newItem);
        result = entity.handleCommand('RemoveItem', removeItem);
    
        // The cart returns an empty message
        expect(result).to.be.empty;
    
        // There shouldn't be any errors
        expect(entity.error).to.be.undefined;
    
        // There should be two event
        expect(entity.events.length).to.be.equal(2)
        expect(entity.events[0].constructor.name).to.be.equal('ItemAdded')
        expect(entity.events[1].constructor.name).to.be.equal('ItemRemoved')
    })
  10. Add test cases to validate details are returned from a cart:

    it('Should get cart details...', () => {
        const entity = new MockEventSourcedEntity(cart, entityId);
        let result = entity.handleCommand('AddItem', newItem);
        result = entity.handleCommand('GetCart', { userId: entityId });
    
        // The cart returns an empty message
        expect(result).to.not.be.empty;
    
        // There shouldn't be any errors
        expect(entity.error).to.be.undefined;
    })
  11. Add another child describe function named Events:

    describe('Test Cart', () => {
      describe('Commands', () => {
        // In the previous steps you added tests here
      });
    
      describe('Events', () => {
        // Now you'll add tests here
      });
    });
  12. Add a test case to validate the ItemAdded event:

    it('Should handle an item added...', () => {
        const entity = new MockEventSourcedEntity(cart, entityId);
    
        // Mock a new ItemAdded
        function ItemAdded(addItem) {
            this.item = {
                productId: addItem.productId,
                name: addItem.name,
                quantity: addItem.quantity
            }
        }
        const result = entity.handleEvent(new ItemAdded(newItem))
    
        // There shouldn't be any errors
        expect(entity.error).to.be.undefined;
    
        // The new state of the entity should match the new product
        expect(entity.state.items[0].name).to.equal(newItem.name);
    })
  13. Add a test case to validate the ItemRemoved event:

    it('Should handle an item removed...', () => {
        const entity = new MockEventSourcedEntity(cart, entityId);
    
        // Mock a new ItemAdded
        function ItemAdded(addItem) {
            this.item = {
                productId: addItem.productId,
                name: addItem.name,
                quantity: addItem.quantity
            }
        }
        let result = entity.handleEvent(new ItemAdded(newItem))
    
        // There shouldn't be any errors
        expect(entity.error).to.be.undefined;
    
        // Mock a new ItemRemoved
        function ItemRemoved(addItem) {
            this.productId = addItem.productId
        }
        result = entity.handleEvent(new ItemRemoved(newItem))
    
        expect(entity.events.length).to.be.equal(2)
        expect(entity.events[0].constructor.name).to.be.equal('ItemAdded')
        expect(entity.events[1].constructor.name).to.be.equal('ItemRemoved')
    })

The complete cart.test.js file

The completed cart.test.js example:

import { MockEventSourcedEntity } from './testkit.js';
import { expect } from 'chai';
import cart from '../cart.js';

describe('Test Warehouse', () => {
const entityId = '1';
const newItem = { 'userId': entityId, 'productId': 'turkey', 'name': 'delicious turkey', 'quantity': 2 }
const removeItem = { 'userId': entityId, 'productId': 'turkey' }

describe('Commands', () => {
it('Should add an item...', () => {
    const entity = new MockEventSourcedEntity(cart, entityId);
    const result = entity.handleCommand('AddItem', newItem);

    // The cart returns an empty message
    expect(result).to.be.empty;

    // There shouldn't be any errors
    expect(entity.error).to.be.undefined;

    // The new state of the entity should match the added item
    expect(entity.state.items[0].name).to.equal(newItem.name);

    // There should be one event
    expect(entity.events.length).to.be.equal(1)
    expect(entity.events[0].constructor.name).to.be.equal('ItemAdded')
})

it('Should remove an item...', () => {
    const entity = new MockEventSourcedEntity(cart, entityId);
    let result = entity.handleCommand('AddItem', newItem);
    result = entity.handleCommand('RemoveItem', removeItem);

    // The cart returns an empty message
    expect(result).to.be.empty;

    // There shouldn't be any errors
    expect(entity.error).to.be.undefined;

    // There should be two event
    expect(entity.events.length).to.be.equal(2)
    expect(entity.events[0].constructor.name).to.be.equal('ItemAdded')
    expect(entity.events[1].constructor.name).to.be.equal('ItemRemoved')
})

it('Should get cart details...', () => {
    const entity = new MockEventSourcedEntity(cart, entityId);
    let result = entity.handleCommand('AddItem', newItem);
    result = entity.handleCommand('GetCart', { userId: entityId });

    // The cart returns an empty message
    expect(result).to.not.be.empty;

    // There shouldn't be any errors
    expect(entity.error).to.be.undefined;
})
})
describe('Events', () => {
it('Should handle an item added...', () => {
    const entity = new MockEventSourcedEntity(cart, entityId);

    // Mock a new ItemAdded
    function ItemAdded(addItem) {
        this.item = {
            productId: addItem.productId,
            name: addItem.name,
            quantity: addItem.quantity
        }
    }
    const result = entity.handleEvent(new ItemAdded(newItem))

    // There shouldn't be any errors
    expect(entity.error).to.be.undefined;

    // The new state of the entity should match the new product
    expect(entity.state.items[0].name).to.equal(newItem.name);
})

it('Should handle an item removed...', () => {
    const entity = new MockEventSourcedEntity(cart, entityId);

    // Mock a new ItemAdded
    function ItemAdded(addItem) {
        this.item = {
            productId: addItem.productId,
            name: addItem.name,
            quantity: addItem.quantity
        }
    }
    let result = entity.handleEvent(new ItemAdded(newItem))

    // There shouldn't be any errors
    expect(entity.error).to.be.undefined;

    // Mock a new ItemRemoved
    function ItemRemoved(addItem) {
        this.productId = addItem.productId
    }
    result = entity.handleEvent(new ItemRemoved(newItem))

    expect(entity.events.length).to.be.equal(2)
    expect(entity.events[0].constructor.name).to.be.equal('ItemAdded')
    expect(entity.events[1].constructor.name).to.be.equal('ItemRemoved')
})
})
})

2. Run the unit tests

From the shoppingcart directory, enter npm run test.

The results of the unit tests are displayed in the terminal. If all tests pass the service is ready to deploy. If the results indicate that a test failed review the logic, make the fixes, and run the tests again, that is what unit testing is all about.