Unit Testin' the Right Way!

Why hello dear friends and welcome back to yet another rootin’ tootin’ Bearmint blog! As you probably expected, we’ve got all of that good content yer lookin’ fer! We’re still hard at work gettin’ things ready and in the process of writin’ and conductin’ some internal tests to make sure there ain’t no problems when we finally get everythin’ ready to go live - this is a pretty tedious and time-consumin’ process, but it’s important that we cover our bases so that we eliminate as many bugs as we possibly can before lettin’ all of you good people in on the action, so to speak.

So what with all that we have goin’ on, we decided now’s a good time to talk a little about testin’ - is it really that important? Why does it take so long? And how do you go about writin’ good tests anyways? Well amigos, this is exactly what we’re gonna focus on in today’s blog post. In truth, the importance of reliable testin’ cannot be overstated, so we’ll say it again and again: writin’ effective tests is of the utmost importance!

Naturally there are different types of tests, so the first logical point of departure concerns unit testin’. Let’s take a little time to talk about unit tests, how to go about writin’ ‘em and why it’s important that you put in the necessary work to ensure yer tests are actually useful! Here we go!

Now What’s All the Fuss About Unit Testin’?

One of the main benefits of unit tests is that they isolate a function, class or method and put a specific piece of code through its paces, all without having an impact on other parts of the system. Higher quality individual components result in enhanced overall system resilience - in other words, the end product is reliable, robust code. Unit tests also change the nature of the debugging process - for example, when trying to fix a bug, developers simply write a failing test and iterate until it passes, all without breaking a previous expectation.

This process cuts out the manual loop of traditional debugging through set up, re-create, pause and inspect. Having said this, changing code to make it unit testable means developers need to change their approach to their work - any code snippets written without unit tests may make the code in question untestable, at least as a single unit. As a general rule, developers that don’t feel the need to carry out unit testing tend to avoid change and risks.

Good unit tests lead to testable code which basically improves the overall quality of the end product - testable code has fewer defects, meaning fewer bug fixes, and therefore the more rapid completion of projects. In the event that software bugs do rear their ugly heads, unit tests allow for faster debugging as well as more rapid fixing and writing of code. Moreover, this is carried out so that the defect in question is less likely to occur again, thus bolstering code quality and velocity at the same time.

Although there are no silver bullets or magic recipes in software development, effective unit tests help speed up the development and testing process as well as some aspects related to functional requirements development (in some cases at least). Bearmint intends to simplify this process by offering an extensive suite of assistive tooling that makes writing tests rewarding encourages you to write as many as you can!

And How Does That Look in Real Life, Amigo?

So while the above is useful to developers, it does mean that there’s still a lot of work to be done. However, carrying out manual testing isn’t feasible once a certain threshold is reached. Fortunately Bearmint offers a wide variety of tools to help you write effective unit tests. In the following example, we’ll take a closer look at a unit test for BEP-054 (also known as a transfer transaction).

Creating a Dummy Transaction

When writing any unit test, you need to try replicating a production environment as closely as possible (however, if you go too far, you’ll end up creating an E2E test). In this instance, we need a way of generating a transaction that resembles or replicates a real transaction. In order to do this, we’ll make use of the faker function (which every transaction module should ideally include).

We call this function during tests to generate a transaction with fake (dummy) values. This function should return a value that is valid for the message property of a transaction and conforms to the types and values one would expect to appear for the transaction type in question.

import { BEP54Message } from './proto';
function fakeBEP54(data?: Partial<BEP54Message>) {
return BEP54Message.encode({
amount: data?.amount ?? `${1e8}`,
denomination: data?.denomination ?? 'BEAR',
message: data?.message ?? 'Hello, World!',
}).finish();
}
async function createTransaction({
context,
payload,
}: {
context: TransactionHandlerContext;
payload?: Record<string, string>;
}) {
// Arrange...
const sender = await makeAccount({
...context,
mnemonic: mnemonics[0],
});
sender.account.balances[denominations.token] = makeBigNumber(1e8);
sender.account.balances[denominations.gas] = makeBigNumber(1e8);
const recipient = await makeAccount({
...context,
mnemonic: mnemonics[1],
});
recipient.account.balances[denominations.token] = makeBigNumber(0);
await context.state.getAccounts().index([sender.account, recipient.account]);
// Act...
return makeTransactionHandlerContext({
context,
recipient: recipient.account,
sender: sender.account,
transaction: await makeTransaction({
message: {
content: fakeBEP54(payload),
handler: HANDLER,
version: VERSION,
},
recipient: recipient.keypair,
sender: sender.keypair,
}),
});
}

Writing a Unit Test

Writing an integration test is pretty simple due to the wide variety of assertions, testing utilities and other features Bearmint offers. The following test is as simple as the transaction it’s being used to test, but for the sake of clarity, let’s break down the essential steps taking place here:

  • We call beforeEach to set up a new test suite with a context that offers all kinds of features (such as state) that we can modify or access to an account factory
  • We call createTransaction to create a new dummy transaction and accounts for the sender and recipient
  • We assert that the sender balance is 100000000 BEAR prior to sending the transaction
  • We assert that the recipient balance is 0 BEAR prior to sending the transaction
  • We call assert.transaction.run.resolves to execute the transaction to apply mutations to both the sender and recipient
  • We assert that the sender balance is 0 BEAR after sending the transaction
  • We assert that the recipient balance is 100000000 BEAR after sending the transaction
If the number 100000000 confuses you, bear in mind that 1e8 is actually scientific notification for 100000000. In other words, while you may enter 1e8, upon execution, the protocol will treat this as 100000000.
describe<TransactionHandlerContext>('TX Transfer (Unit)', ({ assert, beforeEach, it }) => {
beforeEach(async (context) => {
await arrangeTransactionHandlerSuite({
context,
makeHandler,
});
});
it('should decrease the sender balance and increase the recipient balance', async (context) => {
const { ctx, recipient, sender } = await createTransaction({ context });
await assert.account.balance({
address: sender.address,
amount: `${1e8}`,
denomination: denominations.token,
state: context.state,
});
await assert.account.balance({
address: recipient.address,
amount: '0',
denomination: denominations.token,
state: context.state,
});
await assert.transaction.run.resolves(await ctx());
await assert.account.balance({
address: sender.address,
amount: '0',
denomination: denominations.token,
state: context.state,
});
await assert.account.balance({
address: recipient.address,
amount: `${1e8}`,
denomination: denominations.token,
state: context.state,
});
});
});

Combining All Elements

Now that we have a means of producing fake data that reflects a real world environment and writing a unit test, we combine all of the above as follows. Now if something related to this functionality breaks, you’ll be able to rapidly identify the issue when your tests execute.

import { describe } from '@bearmint/bep5';
import { denominations, mnemonics, TransactionHandlerContext } from '@bearmint/bep6';
import { makeBigNumber } from '@bearmint/bep12';
import { makeAccount, makeTransaction } from '@bearmint/bep7';
import {
arrangeTransactionHandlerSuite,
executeTransaction,
makeTransactionHandlerContext,
} from '@bearmint/bep53';
import { fakeBEP54 } from './factory';
import { HANDLER, makeHandler, VERSION } from './handler';
async function createTransaction({
context,
payload,
}: {
context: TransactionHandlerContext;
payload?: Record<string, string>;
}) {
// Arrange...
const sender = await makeAccount({
...context,
mnemonic: mnemonics[0],
});
sender.account.balances[denominations.token] = makeBigNumber(1e8);
sender.account.balances[denominations.gas] = makeBigNumber(1e8);
const recipient = await makeAccount({
...context,
mnemonic: mnemonics[1],
});
recipient.account.balances[denominations.token] = makeBigNumber(0);
await context.state.getAccounts().index([sender.account, recipient.account]);
// Act...
return makeTransactionHandlerContext({
context,
recipient: recipient.account,
sender: sender.account,
transaction: await makeTransaction({
message: {
content: fakeBEP54(payload),
handler: HANDLER,
version: VERSION,
},
recipient: recipient.keypair,
sender: sender.keypair,
}),
});
}
describe<TransactionHandlerContext>('TX Transfer (Unit)', ({ assert, beforeEach, it }) => {
beforeEach(async (context) => {
await arrangeTransactionHandlerSuite({
context,
makeHandler,
});
});
it('should decrease the sender balance and increase the recipient balance', async (context) => {
const { ctx, recipient, sender } = await createTransaction({ context });
await assert.account.balance({
address: sender.address,
amount: `${1e8}`,
denomination: denominations.token,
state: context.state,
});
await assert.account.balance({
address: recipient.address,
amount: '0',
denomination: denominations.token,
state: context.state,
});
await assert.transaction.run.resolves(await ctx());
await assert.account.balance({
address: sender.address,
amount: '0',
denomination: denominations.token,
state: context.state,
});
await assert.account.balance({
address: recipient.address,
amount: `${1e8}`,
denomination: denominations.token,
state: context.state,
});
});
});

So to Summarize, Pilgrims…

Writin’ a simple unit test should take a few minutes at most, partners - but the benefit is that, if any element fails to function as required, you can identify and address the issue in question real nice and quick! You now also know that the element functions correctly without havin’ to spin up an entire blockchain or simply hopin’ and prayin’ that it works!

While you’ll find unit tests like the one mentioned above simple, they can (and do) grow in complexity. This is just the beginnin’ though - later on we’ll discuss integration testin’ and learn how to create even more in-depth tests that simulate real world environments more accurately.

That’s it fer today friends! As always, if there’s anythin’ you need at all, please get in touch with us so that we can assist you and address yer problem directly. And don’t ferget to check in regularly fer new blogs and lore as Bearmint continues to grow over time!