Testing the application

How to use the test utility of the Lisk SDK to test your application.

Prerequisites

To use this guide, it is assumed that the following criteria have been met:

To conveniently test the functionality of modules, plugins, or assets, adjust the already generated test skeletons in the test folder of your application.

After generating a new module and asset, the corresponding skeletons for their unit tests can be found under test/unit/modules/nft:

/new_app/test/
├── _setup.js
├── commands
│   └── account
│       └── create.spec.ts
├── integration
├── network
├── tsconfig.json
├── unit
│   └── modules
│       └── nft
│           ├── assets
│           │   └── create_n_f_t_asset.spec.ts
│           └── nft_module.spec.ts
└── utils
    └── config.ts

Running the test suite

It is already possible to run the test at this point, though only the most basic tests will be implemented.

To run all test suites, execute:

/new_app/
yarn run test

The test results can then be viewed in the console:

yarn run v1.22.10
$ jest --passWithNoTests
 PASS  test/unit/plugins/nft_api/nft_api_plugin.spec.ts (7.656 s)
 PASS  test/unit/modules/my_module/my_module_module.spec.ts (7.726 s)
 PASS  test/unit/modules/nft/nft_module.spec.ts (7.703 s)
 PASS  test/unit/modules/nft/assets/create_n_f_t_asset.spec.ts (9.282 s)
 PASS  test/commands/account/create.spec.ts (10.092 s)

Test Suites: 5 passed, 5 total
Tests:       26 todo, 8 passed, 34 total
Snapshots:   1 passed, 1 total
Time:        11.388 s
Ran all test suites.
✨  Done in 14.60s.

If the tests of the asset fails, e.g. with the following error:

'asset' is declared but its value is never read.

It is most likely, because the validate() or apply() function isn’t implemented yet. To fix the error, either remove the unused variables or implement logic which uses them in the corresponding function.

The module test skeleton

The test skeleton of a module doesn’t contain any real tests in the beginning.

Use the existing structure to implement the tests required for the module, and add more tests as needed.

test/unit/modules/nft/nft_module.spec.ts
// import * as modules from '../../../src/app/modules/nft'

describe('NftModuleModule', () => {
	describe('constructor', () => {
		it.todo('should have valid id');
		it.todo('should have valid name');
	});

	describe('beforeBlockApply', () => {
		it.todo('should execute before block apply');
	});
	describe('afterBlockApply', () => {
		it.todo('should execute after block apply');
	});
	describe('beforeTransactionApply', () => {
		it.todo('should execute before transaction apply');
	});
	describe('afterTransactionApply', () => {
		it.todo('should execute after transaction apply');
	});
	describe('afterGenesisBlockApply', () => {
		it.todo('should execute after genesis apply');
	});
});

The asset test skeleton

The test skeleton for the asset already contains already a few simple tests right from the beginning. They were automatically created during the generation of the asset. The remainder of the tests will need to be created by the developer, to test all the custom logic of the asset which was implemented after the initialization of the application.

test/unit/modules/nft/create_n_f_t_asset.spec.ts
import { CreateNFTAsset } from '../../../../../src/app/modules/nft/assets/create_n_f_t_asset';

describe('CreateNFTAsset', () => {
  let transactionAsset: CreateNFTAsset;

	beforeEach(() => {
		transactionAsset = new CreateNFTAsset();
	});

	describe('constructor', () => {
		it('should have valid id', () => {
			expect(transactionAsset.id).toEqual(0);
		});

		it('should have valid name', () => {
			expect(transactionAsset.name).toEqual('createNFT');
		});

		it('should have valid schema', () => {
			expect(transactionAsset.schema).toMatchSnapshot();
		});
	});

	describe('validate', () => {
		describe('schema validation', () => {
            it.todo('should throw errors for invalid schema');
            it.todo('should be ok for valid schema');
        });
	});

	describe('apply', () => {
        describe('valid cases', () => {
            it.todo('should update the state store');
        });

        describe('invalid cases', () => {
            it.todo('should throw error');
        });
	});
});

Example: Unit tests for a transaction asset

This example shows how to write unit tests for the example asset from the previous guide Creating a new transaction asset.

For more information about the different features of the test suite, check out the reference page The Lisk SDK test suite

Imports

Add the following lines at the top of create_n_f_t_asset.spec.ts to import the required resources for the tests.

import { CreateNFTAsset } from '../../../../../src/app/modules/nft/assets/create_n_f_t_asset'; (1)
import { StateStore, ReducerHandler, testing } from 'lisk-sdk'; (2)
import { NftModule } from '../../../../../src/app/modules/nft/nft_module'; (3)
import {
    getAllNFTTokens,
    createNFTToken,
}  from "../../../../../src/app/modules/nft/nft"; (4)
1 CreateNFTAsset: The asset which is tested here.
2 testing contains the functions of the Lisk SDk test suite.
3 NftModule: is used in createDefaultAccount() to create a default account with the correct account properties.
4 getAllNFTTokens and createNFTToken are utility functions for the NFT module which are also used in the tests for the apply() function.

Testing the validate() function

As a reminder, the validate() function of the createNFT asset is shown below:

validate() function of create_n_f_t_asset.ts
validate({asset}) {
    if (asset.name === "Mewtwo") {
        throw new Error("Illegal NFT name: Mewtwo");
    }
};

To verify that the function is implemented correctly, write 2 tests to check if the following occurs:

  1. it throws an error if the NFT name equals "Mewtwo"

  2. it does not throw any error for a valid schema

The function createValidateAssetContext() is used for both tests to create a context for the validate() function.

In the first test, where an error is expected, a context with an invalid asset parameter with the name: 'Mewtwo' is created, whereas in the second test a valid name property is passed.

After the context is created, both tests will call the validate() function with the context and the result is checked.

If all tests pass, it is verified that the validate function behaves exactly as expected.

Tests for validate()
describe('validate', () => {
    describe('schema validation', () => {
        it('should throw error if nft name equals "Mewtwo"', () => {
            const context = testing.createValidateAssetContext({
                asset: { name: 'Mewtwo', initValue: 1, minPurchaseMargin: 10 },
                transaction: { senderAddress: Buffer.alloc(0) } as any,
            });

            expect(() => transactionAsset.validate(context)).toThrow(
                'Illegal NFT name: Mewtwo',
            );
        });
        it('should be ok for valid schema', () => {
            const context = testing.createValidateAssetContext({
                asset: { name: 'Squirtle', initValue: 1, minPurchaseMargin: 10 },
                transaction: { senderAddress: Buffer.alloc(0) } as any,
            });

            expect(() => transactionAsset.validate(context)).not.toThrow();
        });
    });
});

Testing the apply() function

As a reminder, the apply() function of the createNFT asset is shown below:

apply() function of create_n_f_t_asset.ts
async apply({ asset, stateStore, reducerHandler, transaction }) {
    // 4.verify if sender has enough balance
    const senderAddress = transaction.senderAddress;
    const senderAccount = await stateStore.account.get(senderAddress);

    // 5.create nft
    const nftToken = createNFTToken({
      name: asset.name,
      ownerAddress: senderAddress,
      nonce: transaction.nonce,
      value: asset.initValue,
      minPurchaseMargin: asset.minPurchaseMargin,
    });

    // 6.update sender account with unique nft id
    senderAccount.nft.ownNFTs.push(nftToken.id);
    await stateStore.account.set(senderAddress, senderAccount);

    // 7.debit tokens from sender account to create nft
    await reducerHandler.invoke("token:debit", {
      address: senderAddress,
      amount: asset.initValue,
    });

    // 8.save nfts
    const allTokens = await getAllNFTTokens(stateStore);
    allTokens.push(nftToken);
    await setAllNFTTokens(stateStore, allTokens);
}

To verify that the function is implemented correctly, write 4 tests to check if the following occurs:

For valid cases:

  1. it updates the sender account with a unique NFT ID

  2. it debits the initial value from the sender account

  3. it saves the new NFT to the database

For invalid cases:

  1. it throws an error, if the NFT name is already registered

Similar to the unit tests for the validate() function, a context is prepared using createApplyAssetContext() for the apply() function which can be passed to the function when calling it in each test.

As the context is the same for every test, it is convenient to prepare everything in the beforeEach() hook and directly call the apply() function with the context in each test.

create_n_f_t_asset.spec.ts
describe('apply', () => {
    let stateStore: StateStore;
    let reducerHandler: ReducerHandler;
    let account: any;
    let nftToken;
    let context;

    beforeEach(() => {

        // Create new account
        account = testing.fixtures.createDefaultAccount<NFTAccountProps>([NftModule]);

        // Create new NFT for account
        nftToken = createNFTToken({
            name: 'Squirtle',
            ownerAddress: account.address,
            nonce: BigInt(1),
            value: BigInt(1),
            minPurchaseMargin: 10
        });

        // Create state store mock with account
        stateStore = new testing.mocks.StateStoreMock({
            accounts: [account],
        });

        // Create reducer handler mock
        reducerHandler = testing.mocks.reducerHandlerMock;

        // Create context for the apply() function
        context = testing.createApplyAssetContext({
            stateStore,
            reducerHandler,
            asset: { name: 'Squirtle', initValue: BigInt(1), minPurchaseMargin: 10 },
            transaction: { senderAddress: account.address, nonce: BigInt(1) } as any,
        });

        // Tracks calls to stateStore.chain and the reducerHandler
        jest.spyOn(stateStore.chain, 'get');
        jest.spyOn(stateStore.chain, 'set');
        jest.spyOn(reducerHandler, 'invoke');
    });

});

Additionally, add the following interface to the test file:

export interface NFTAccountProps {
    nft: {
        ownNFTs: [];
    };
}

It is used in the beforeEach() hook to create a default account with valid account properties.

Valid cases

The tests for the valid cases test are implemented as shown below:

describe('valid cases', () => {
    it('should update sender account with unique nft id', async () => {
        await transactionAsset.apply(context);
        const updatedSender = await stateStore.account.get<NFTAccountProps>(account.address);

        expect(updatedSender.nft.ownNFTs.toString()).toEqual(nftToken.id.toString());
    });
    it('should debit the initial value from the sender account', async () => {
        await transactionAsset.apply(context);
        expect(reducerHandler.invoke).toHaveBeenCalledWith("token:debit", {
            address: account.address,
            amount: BigInt(1),
        });
    });
    it('should save the new NFT to the database', async () => {
        await transactionAsset.apply(context);
        const allTokens = await getAllNFTTokens(stateStore);
        expect(allTokens).toEqual( [nftToken]);
    });

});

Invalid cases

The test for the invalid cases test is implemented as shown below:

describe('invalid cases', () => {

    it('should throw error if name is already registered', async () => {
        await transactionAsset.apply(context);
        await expect(transactionAsset.apply(context)).rejects.toThrow(
            'The NFT name "Squirtle" is already registered',
        );
    });
});

Run the tests

After the tests have been implemented, run the test suite again to check if all tests pass successfully:

/new_app/
yarn run test

If the logic of the asset and the test for the asset was implemented correctly, all tests should pass:

yarn run v1.22.10
$ jest --passWithNoTests
 PASS  test/unit/modules/nft/nft_module.spec.ts
 PASS  test/unit/plugins/nft_api/nft_api_plugin.spec.ts
 PASS  test/unit/modules/my_module/my_module_module.spec.ts
 PASS  test/commands/account/create.spec.ts
 PASS  test/unit/modules/nft/assets/create_n_f_t_asset.spec.ts (6.606 s)

Test Suites: 5 passed, 5 total
Tests:       22 todo, 14 passed, 36 total
Snapshots:   1 passed, 1 total
Time:        7.99 s, estimated 11 s
Ran all test suites.
✨  Done in 10.70s.

The implementation of the unit tests for the createNFT asset is now complete.