Testing

Introduction

Core is built with testing in mind. In fact, support for testing with jest is included out of the box and a jest-preset.json file is already set up.

Core's __tests__ directory contains 4 main test directories: e2e, functional, integration and unit.

The 4 types of tests

Unit Tests

Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method.

They generally are the fastest tests in your suite. The speed at which they are executed is great but they give you no guarantee that all components work once they are stitched together, which is why need the previously mentioned integration tests.

Integration Tests

Integration tests may test a larger portion of your code, including how several objects interact with each other or external interaction like making an HTTP request to an API endpoint.

They are generally a bit slower compared to unit tests as they test the behaviour of larger parts of an application. However they guarantee that different parts of the application are working correctly together.

Functional Tests

Functional tests launch the whole application and apply some real-life scenarios : for example creating and sending a valid transaction, expecting it to be forged in the next block.

Those tests will ensure that the application is tested from the end-users perspective.

End-to-end tests

End-to-end (e2e) tests go further by spinning up real nodes on a local network, actually running the SXP blockchain on a test network (testnet) and executing scenarios on it.

They will be most useful to verify peer-to-peer interaction and running scenarios on a real network.

Code Organization

Before all, let's see and understand how the code is organized. When you open the swipechain repository, you should see the following directory structure:

/__tests__
/docker
/packages
/plugins
/scripts

For developing and testing, we are mainly interested in the packages directory as it contains the whole core code, and __tests__ directory as it contains the tests.

Have a look at the packages directory, to see how the application is divided into different packages :

/packages/core
/packages/core-api
/packages/core-blockchain
.......

We will now dig into the typical structure of a package. So let us look at /packages/core-blockchain as an example. It has a main folder named src :

/packages/core-blockchain/src

This folder contains the TypeScript code before it gets compiled to JavaScript via tsc.

Now the unit tests associated to this packages would be located in the following directory :

/__tests__/unit/core-blockchain

Now that we have an idea of how the code is organized, we can go inside the /__tests__/unit/core-blockchain folder and see how the tests are structured.

Tests Structure

We'll keep /__tests__/unit/core-blockchain as an example. Open the folder and you'll see something like this:

/machines
    blockchain.test.ts
/mocks
blockchain.test.ts
state-machine.test.ts
state-storage.test.ts

Matching the /src Folder

Important thing to note: except for special directories like mocks, the directory structure matches the /src structure. We want to keep it this way as much as possible to make it easy to identify what is being tested. If you have worked with Go this practice should be familiar.

Mocks

Most unit tests need mocks, hence the /mock folder where general mocking is set up (this folder contains mostly basic mocks corresponding to packages dependencies, for example core-blockchain is depending on core-container which will be mocked here).

Then in the actual test files we will be able to use the pre-defined mocks by importing the mock folder, but also modify or add new mocks as we need.

Utils

The main utils folder (/__tests__/utils) is a shared library for testing. It helps keep the tests clean and remove redundancy.

Have a look at it and don't hesitate to improve. Here are some examples of what you can find :

  • Network configuration files
  • Network fixtures (blocks, delegates public keys and secrets)
  • Generators: generate objects such as transactions

Jest Matchers

Core provides a variety of matchers for Jest that can be used in combination with expect().

If you plan to use them simply run yarn add @swipechain/core-jest-matchers --dev and include them with import "@swipechain/core-jest-matchers"; on top of your tests.

Transactions

toBeTransferType()

Assert that the given value is a transfer transaction.

expect({ type: 0 }).toBeTransferType();

toBeSecondSignatureType()

Assert that the given value is a second signature registration transaction.

expect({ type: 1 }).toBeSecondSignatureType();

toBeDelegateType()

Assert that the given value is a delegate registration transaction.

expect({ type: 2 }).toBeDelegateType();

toBeVoteType()

Assert that the given value is a vote transaction.

expect({ type: 3 }).toBeVoteType();

toBeMultiSignatureType()

Assert that the given value is a multi signature registration transaction.

expect({ type: 4 }).toBeMultiSignatureType();

toBeIpfsType()

Assert that the given value is an IPFS transaction.

expect({ type: 5 }).toBeIpfsType();

toBeTimelockTransferType()

Assert that the given value is a timelock transfer transaction.

expect({ type: 6 }).toBeTimelockTransferType();

toBeMultiPaymentType()

Assert that the given value is a multi payment transaction.

expect({ type: 7 }).toBeMultiPaymentType();

toBeDelegateResignationType()

Assert that the given value is a delegate resignation transaction.

expect({ type: 8 }).toBeDelegateResignationType();

toBeTransaction()

Assert that the given value is a transaction.

expect({
  version: 1,
  network: 23,
  type: 0,
  timestamp: 35672738,
  senderPublicKey:
    "03d7dfe44e771039334f4712fb95ad355254f674c8f5d286503199157b7bf7c357",
  fee: 10000000,
  vendorFieldHex: "5449443a2030",
  amount: 200000000,
  expiration: 0,
  recipientId: "AFzQCx5YpGg5vKMBg4xbuYbqkhvMkKfKe5",
  signature:
    "304502210096ec6e27176fa694638d6fff35d7a551b2ed8c479a7e03264026eea41a05edd702206c071c97d1c6cc3bfec64dfff808cb0d5dfe857803428efb80bf7717b85cb619",
  vendorField: "TID: 0",
  id: "a5e9e6039675563959a783fa672c0ffe65369168a1ecffa3c89bf82961d8dbad"
}).toBeTransaction();

toBeValidTransaction()

Assert that the given value is a valid transaction.

expect({
  version: 1,
  network: 23,
  type: 0,
  timestamp: 35672738,
  senderPublicKey:
    "03d7dfe44e771039334f4712fb95ad355254f674c8f5d286503199157b7bf7c357",
  fee: 10000000,
  vendorFieldHex: "5449443a2030",
  amount: 200000000,
  expiration: 0,
  recipientId: "AFzQCx5YpGg5vKMBg4xbuYbqkhvMkKfKe5",
  signature:
    "304502210096ec6e27176fa694638d6fff35d7a551b2ed8c479a7e03264026eea41a05edd702206c071c97d1c6cc3bfec64dfff808cb0d5dfe857803428efb80bf7717b85cb619",
  vendorField: "TID: 0",
  id: "a5e9e6039675563959a783fa672c0ffe65369168a1ecffa3c89bf82961d8dbad"
}).toBeValidTransaction();

Wallets

toBeAddress()

Assert that the given value is an address.

expect("DARiJqhogp2Lu6bxufUFQQMuMyZbxjCydN").toBeAddress();

toBePublicKey()

Assert that the given value is a public key.

expect(
  "022cca9529ec97a772156c152a00aad155ee6708243e65c9d211a589cb5d43234d"
).toBePublicKey();

toBeWallet()

Assert that the given value is a wallet.

expect({
  address: "DQ7VAW7u171hwDW75R1BqfHbA9yiKRCBSh",
  publicKey:
    "0310ad026647eed112d1a46145eed58b8c19c67c505a67f1199361a511ce7860c0"
}).toBeWallet();

toBeDelegate()

Assert that the given value is a delegate wallet.

expect({
  username: "swipechainxdev",
  address: "DQ7VAW7u171hwDW75R1BqfHbA9yiKRCBSh",
  publicKey:
    "0310ad026647eed112d1a46145eed58b8c19c67c505a67f1199361a511ce7860c0"
}).toBeDelegate();

Blocks

toBeValidArrayOfBlocks()

Assert that the given value is an array containing blocks.

expect([
  {
    blockSignature: "",
    createdAt: "",
    generatorPublicKey: "",
    height: "",
    id: "",
    numberOfTransactions: "",
    payloadHash: "",
    payloadLength: "",
    previousBlock: "",
    reward: "",
    timestamp: "",
    totalAmount: "",
    totalFee: "",
    transactions: "",
    updatedAt: "",
    version: ""
  }
]).toBeValidArrayOfBlocks();

toBeValidBlock()

Assert that the given value is a transfer transaction.

expect({
  blockSignature: "",
  createdAt: "",
  generatorPublicKey: "",
  height: "",
  id: "",
  numberOfTransactions: "",
  payloadHash: "",
  payloadLength: "",
  previousBlock: "",
  reward: "",
  timestamp: "",
  totalAmount: "",
  totalFee: "",
  transactions: "",
  updatedAt: "",
  version: ""
}).toBeValidBlock();

Peers

toBeValidArrayOfPeers()

Assert that the given value is an array containing peers.

expect([{ ip: "", port: "" }]).toBeValidArrayOfPeers();

toBeValidPeer()

Assert that the given value is a valid peer.

expect({ ip: "", port: "" }).toBeValidPeer();

Core API

toBeApiTransaction()

Assert that the given value is a transaction from an API response.

expect({
  id: "",
  blockid: "",
  type: "",
  timestamp: "",
  amount: "",
  fee: "",
  senderId: "",
  senderPublicKey: "",
  signature: "",
  asset: "",
  confirmations: ""
}).toBeApiTransaction();

toBePaginated()

Assert that the given value is a paginated API response.

expect({
  status: 200,
  headers: {},
  data: {
    meta: {
      pageCount: "",
      totalCount: "",
      next: "",
      previous: "",
      self: "",
      first: "",
      last: ""
    }
  }
}).toBePaginated();

toBeSuccessfulResponse()

Assert that the given value is a successful API response.

expect({
  status: 200,
  headers: {},
  data: {
    meta: {
      pageCount: "",
      totalCount: "",
      next: "",
      previous: "",
      self: "",
      first: "",
      last: ""
    }
  }
}).toBeSuccessfulResponse();

Guidelines for Writing Tests

Use utils Folder for Common Stuff

For testing, we are doing a lot of common things across the packages. Let us try to use the __tests__/utils folder as a shared library to avoid duplication.

Here are some things that are available in utils:

  • Container set up
  • Testnet configuration files
  • Testnet fixtures (blocks, delegates public keys and secrets)
  • Generators: generate objects such as transactions

There is still a lot to improve in utils, some things might also be outdated. Don't hesitate to make changes to improve it.

Do more than "basic" tests

When we write some new tests, generally we start by checking that the feature is working as expected in the general case, which is perfectly fine. However, please do not stop there, it is the edge cases we are worried about.

Go deeper and test it with different parameters. Ask yourself: in which case this could very well fail, such as a particular set of parameters? If I were to refactor the feature, what would I like to be tested then?

Contact Us

If you have anything to ask, suggest or want to have any talk about testing, don't hesitate to reach out to the team.

On Slack, you can contact Air1 as he is managing the tests.