Testing the blockchain application

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

Sample code

View the complete sample code of this guide on GitHub in the Lisk SDK examples repository.

Prerequisites

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

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

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

/hello_client/test/
├── integration
├── network
├── unit
│   └── modules
│   │   └── hello
│   │       ├── commands
│   │       │   └── create_hello_command.spec.ts
│   │       └── module.spec.ts
│   └── plugins
│       └── hello_info
│           └── hello_info_plugin.spec.ts
├── utils
│   └── config.ts
├── _setup.js
├── .eslintrc.js
└── tsconfig.json

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 the following:

/hello_client/
yarn run test

The test results can then be viewed in the console:

yarn run v1.22.19
warning ../../../../../../package.json: No license field
$ jest --passWithNoTests
ts-jest[versions] (WARN) Version 5.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <5.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 5.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <5.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 5.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <5.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
 PASS  test/unit/modules/hello/module.spec.ts
 PASS  test/unit/plugins/hello_info/hello_info_plugin.spec.ts
 PASS  test/unit/modules/hello/commands/create_hello_command.spec.ts
 › 1 snapshot written.

Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 3 passed, 3 total
Tests:       16 todo, 2 passed, 18 total
Snapshots:   1 written, 1 total
Time:        2.513 s
Ran all test suites.
✨  Done in 4.21s.

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/hello/module.spec.ts
// import * as modules from '../../../src/app/modules/hello'

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

	describe('beforeTransactionsExecute', () => {
		it.todo('should execute before block execute');
	});
	describe('afterTransactionsExecute', () => {
		it.todo('should execute after block execute');
	});
	describe('beforeCommandExecute', () => {
		it.todo('should execute before transaction execute');
	});
	describe('afterCommandExecute', () => {
		it.todo('should execute after transaction execute');
	});
	describe('beforeTransactionsExecute', () => {
		it.todo('should execute before genesis execute');
	});
	describe('afterTransactionsExecute', () => {
		it.todo('should execute after genesis execute');
	});
});

The command test skeleton

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

test/unit/modules/hello/create_hello_command.spec.ts
import { HelloModule } from '../../../../../src/app/modules/hello/module';
import { CreateHelloCommand } from '../../../../../src/app/modules/test/commands/create_hello_command';

describe('CreateHelloCommand', () => {
	let command: CreateHelloCommand;

	beforeEach(() => {
        const hello = new HelloModule();
		command = new CreateHelloCommand(hello.stores, hello.events);
	});

	describe('constructor', () => {
		it('should have valid name', () => {
			expect(command.name).toEqual('createHello');
		});

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

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

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

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

Writing unit tests

This example shows how to write unit tests for the command from the previous guide How to create a command.

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

Unit tests for the CreateHello command

Imports

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

import { testing, codec, cryptography, Transaction, chain, db, VerifyStatus } from 'lisk-sdk';
import { CreateHelloCommand } from '../../../../../src/app/modules/hello/commands/create_hello_command';
import { CreateHelloParams, createHelloSchema } from '../../../../../src/app/modules/hello/schema';
import { ModuleConfig } from '../../../../../src/app/modules/hello/types';
import { HelloModule } from '../../../../../src/app/modules/hello/module';
import { CounterStore, counterKey } from '../../../../../src/app/modules/hello/stores/counter';
import { MessageStore } from '../../../../../src/app/modules/hello/stores/message';
The testing package imported from Lisk SDK contains several helper functions to create contexts for verify() and execute() hooks. You can also create fake block headers, etc. via the testing package and use it to write tests.

Testing the verify() function

As a reminder, the verify() function of the command CreateHelloCommand is shown below:

verify() function of create_hello_command.ts
public async verify(context: CommandVerifyContext<Params>): Promise<VerificationResult> {
    const wordList = context.params.message.split(" ");
    const found = this._blacklist.filter(value => wordList.includes(value));
    if (found.length > 0) {
        context.logger.info("==== FOUND: Message contains a blacklisted word ====");
        const error = Error(
            `Illegal word in hello message: ${found.toString()}`
        );
        return {
            status: VerifyStatus.FAIL,
            error,
        };
    }
    context.logger.info("==== NOT FOUND: Message contains no blacklisted words ====");
    return {
        status: VerifyStatus.OK
    };
}

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

  1. The VerifyStatus should fail if the hello message equals some illegal statement as defined in the module’s config.

  2. The VerifyStatus should pass if the hello message doesn’t contain any illegal statement.

The function createCommandVerifyContext() is used for both tests to create a context for the verify() function.

In the first test, where an error is expected, a context with an invalid command parameter with the helloString: 'badWord2' is created, whereas, in the second test, a valid Hello Lisk v6 string is passed.

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

If all tests pass, this verifies that the verify() function behaves exactly as expected.

Tests for verify() in create_hello_command.spec.ts
describe('verify', () => {
    it('should have an illegal message', async () => {
        const illegalParam = codec.encode(createHelloSchema, { 'message': "badWord2" })
        const transaction = new Transaction(getSampleTransaction(illegalParam));

        const context = testing
            .createTransactionContext({
                stateStore,
                transaction,
                header: testing.createFakeBlockHeader({}),
            })
            .createCommandVerifyContext<CreateHelloParams>(createHelloSchema);

        const result = await command.verify(context);
        expect(result.status).toBe(VerifyStatus.FAIL);
    });

    it('should have a legal message', async () => {
        const legalParam = codec.encode(createHelloSchema, { 'message': "Hello Lisk v6 " })
        const transaction = new Transaction(getSampleTransaction(legalParam));

        const context = testing
            .createTransactionContext({
                stateStore,
                transaction,
                header: testing.createFakeBlockHeader({}),
            })
            .createCommandVerifyContext<CreateHelloParams>(createHelloSchema);

        const result = await command.verify(context);
        expect(result.status).toBe(VerifyStatus.OK);
    });
});

Testing the execute() function

As a reminder, the execute() function of the command createHelloCommand is shown below:

execute() function of create_hello_command.ts
public async execute(context: CommandExecuteContext<Params>): Promise<void> {
    // 1. Get the account data of the sender of the Hello transaction.
    const { senderAddress } = context.transaction;
    // 2. Get message and counter stores.
    const messageSubstore = this.stores.get(MessageStore);
    const counterSubstore = this.stores.get(CounterStore);

    // 3. Save the Hello message to the message store, using the senderAddress as the key, and the message as value.
    await messageSubstore.set(context, senderAddress, {
        message: context.params.message,
    });

    // 3. Get the Hello counter from the counter store.
    let helloCounter: CounterStoreData;
    try {
        helloCounter = await counterSubstore.get(context, counterKey);
    } catch (error) {
        helloCounter = {
            counter: 0,
        }
    }
    // 5. Increment the Hello counter +1.
    helloCounter.counter += 1;

    // 6. Save the Hello counter to the counter store.
    await counterSubstore.set(context, counterKey, helloCounter);

    // 7. Emit a "New Hello" event
    const newHelloEvent = this.events.get(NewHelloEvent);
    newHelloEvent.add(context, {
        senderAddress: context.transaction.senderAddress,
        message: context.params.message
    }, [context.transaction.senderAddress]);
}

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

  1. The hello message is updated in the sender account with the specified hello string.

  2. The hello counter is incremented by one.

Similar to the unit tests for the verify() function, a context is prepared using createCommandExecuteContext() for the execute() 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 recommended to first prepare everything before in the beforeEach() hook and directly call the execute() function with the context in each test.

create_hello_command.spec.ts
describe('CreateHelloCommand', () => {

	const getSampleTransaction = (params: Buffer) => ({
		module: 'hello',
		command: CreateHelloCommand.name,
		senderPublicKey: Buffer.from("3bb9a44b71c83b95045486683fc198fe52dcf27b55291003590fcebff0a45d9a", 'hex'),
		nonce: BigInt(0),
		fee: BigInt(100000000),
		params,
		signatures: [cryptography.utils.getRandomBytes(64)],
	});

	let command: CreateHelloCommand;
	let stateStore: any;
	let counterStore: CounterStore;
	let messageStore: MessageStore;

	const config = {
		"blacklist": [
			"illegalWord1",
			"badWord2",
			"censoredWord3"
		]
	}

	beforeEach(async () => {
		const hello = new HelloModule();
		command = new CreateHelloCommand(hello.stores, hello.events);
		await command.init(config as ModuleConfig);
		stateStore = new chain.StateStore(new db.InMemoryDatabase());
		counterStore = hello.stores.get(CounterStore);
		messageStore = hello.stores.get(MessageStore);
	});
});

The beforeEach() can be used to initialize required values and objects before running the test suite.

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

create_hello_command.spec.ts
describe('execute', () => {
    it('should execute legal message', async () => {
        const message = { "message": "Hello from SDK!" };
        const params = codec.encode(createHelloSchema, message)
        const transaction = new Transaction(getSampleTransaction(params));

        const context = testing
            .createTransactionContext({
                stateStore,
                transaction,
                header: testing.createFakeBlockHeader({}),
            })
            .createCommandExecuteContext<CreateHelloParams>(createHelloSchema);

        await command.execute(context);
        const helloMessage = await messageStore.get(context, transaction.senderAddress);
        const helloCounter = await counterStore.get(context, counterKey);
        expect(helloCounter.counter).toBe(1);
        expect(helloMessage.message).toBe("Hello from SDK!");
    });

    it('should test counter value', async () => {
        const message = { "message": "Hello from SDK!" };
        const params = codec.encode(createHelloSchema, message)
        const transaction = new Transaction(getSampleTransaction(params));

        const context = testing
            .createTransactionContext({
                stateStore,
                transaction,
                header: testing.createFakeBlockHeader({}),
            })
            .createCommandExecuteContext<CreateHelloParams>(createHelloSchema);
        await counterStore.set(context, counterKey, { "counter": 10 })
        await command.execute(context);
        const helloMessage = await messageStore.get(context, transaction.senderAddress);
        const helloCounter = await counterStore.get(context, counterKey);
        expect(helloCounter.counter).toBe(11);
        expect(helloMessage.message).toBe("Hello from SDK!");
    });
});

Run the tests

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

/hello_client/
yarn run test

To see the output in the following format, update the verbose property to true in the jest.config.js file. If the logic and the tests of the command are implemented correctly, all tests should pass:

> hello_client@0.1.0 test
> jest --passWithNoTests

ts-jest[versions] (WARN) Version 5.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <5.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 5.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <5.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 5.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <5.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
 PASS  test/unit/modules/hello/module.spec.ts
  HelloModule
    constructor
      ✎ todo should have valid name
    beforeTransactionsExecute
      ✎ todo should execute before block execute
      ✎ todo should execute after genesis execute
    afterTransactionsExecute
      ✎ todo should execute after block execute
      ✎ todo should execute after genesis execute
    beforeCommandExecute
      ✎ todo should execute before transaction execute
    afterCommandExecute
      ✎ todo should execute after transaction execute

 PASS  test/unit/plugins/hello_info/hello_info_plugin.spec.ts
  HelloInfoPlugin
    name
      ✎ todo should have valid name
    nodeModulePath
      ✎ todo should have nodeModulePath
    events
      ✎ todo should fire an event
    load
      ✎ todo should load plugin
    unload
      ✎ todo should unload plugin

 PASS  test/unit/modules/hello/commands/create_hello_command.spec.ts
  CreateHelloCommand
    constructor
      ✓ should have valid name (4 ms)
      ✓ should have valid schema (1 ms)
    verify
      ✓ should have an illegal message (17 ms)
      ✓ should have a legal message (1 ms)
    execute
      ✓ should execute legal message (6 ms)
      ✓ should test counter value (1 ms)

Test Suites: 3 passed, 3 total
Tests:       12 todo, 6 passed, 18 total
Snapshots:   1 passed, 1 total
Time:        2.335 s
Ran all test suites.

The implementation of the unit tests for the command CreateHelloCommand is now complete.

Similarly to commands, it is also possible to write test cases for modules. For example, consider the fee module’s test script, where various unit tests are written for different hooks of a fee module. For more unit test examples, see the lisk-sdk/framework/test/unit directory.