Why Integration Testin' Is Real Important!

Howdy once again, partners! We trust you’ve all been well and that the last few months have been treatin’ you kindly. In our last blog post we discussed the importances of unit testin’ and provided y’all with a way of writin’ unit tests that will actual be of some use to you durin’ yer own development process. Here at Bearmint, we know that testin’ is the cornerstone of any well-built, reliable application. After all, if you don’t go through things with a fine-toothed comb, how on Earth will you ever discover them nasty issues that may spring up when you least suspect ‘em?

So now that you’ve gotten to grips with unit testin’, it’s now necessary to gain an appreciation of integration tests. So as unit testin’ is all about analyzin’ components in isolation, integration testin’ combines the different parts of an application and tests how they work together! The thing is, while yer components may work on their own, there’s nothin’ to say everythin’ will be hunky-dory once you connect ‘em all together. So naturally, this is where integration testin’ is real important…

So without further delay, let’s find out more about integration testin’ and learn how to write effective integration tests. Here we go!

Now What Exactly Is Integration Testin’ All About?

Integration testing involves combining the various units, modules and components of an application and analyzing their overall performance within the greater system to which they belong. However, in many cases, different developers end up contributing to and writing code for these modules. The main aim of integration testing is to closely examine the interfaces between these modules and expose any defects that may arise when these components work in conjunction and interact with one another.

In order to carry out integration testing, developers use test drivers and stubs which are types of dummy programs that serve as substitutes for any missing modules and simulate data communications between modules for the purposes of testing. It logically follows that integration testing is only conducted once unit testing is over and done with.

Carrying out integration testing is crucial in the world of modern IT and app development, especially when dynamic requirements and tight deadlines consistently come up. Even when each module is put through its paces in unit testing, some errors or anomalies may still exist. Therefore, in order to identify these errors and ensure that the modules work optimally when combined, integration testing is non-negotiable.

Some important reasons to conduct integration testing include the following:

  • Integrating separate modules into a working application - When different developers work on different modules, they bring their own understanding and logic to the development effort. It therefore follows that, upon combining these modules, functional or interpretive problems may arise. Integration testing can assist in ensuring that all of the integrated units function effectively as one combined unit and align with stated requirements. It also ensures that no errors exist between the different interfaces of different modules.
  • Ensuring the incorporation of changing requirements into the application - In many real-time application scenarios, requirements can (and do) change on a regular basis. As such, these new requirements may not undergo unit testing each and every time, which may result in overlooked defects and/or missing product features. Integration testing can compensate for these gaps, thus ensuring the inclusion and integration of new requirements into the final application.
  • Eliminating common issues missed during unit testing - Some modules that interact with third-party application program interfaces (APIs) need to undergo testing to ensure they work as intended. This may not occur during unit testing, so integration testing is entirely necessary.
  • Eliminating other common problems - Integration testing eliminates issues such as inadequate exception handling, API response generation, data formatting, erroneous external hardware interfaces, incorrect third-party service interfaces and error trapping.

So How Do We Do That in the Real World, Sir?

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 integration tests. In the following example, we’ll take a closer look at an integration test for BEP-054 (also known as a transfer transaction).

Creating a Dummy Transaction

When writing any integration 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 an Integration Test

Writing an integration test is pretty easy thanks 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, we’ll 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 executeTransaction to execute the transaction - this function carries out many processes, but rest assured that a real production environment is being reflected here
  • 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 (Integration)', ({ assert, beforeEach, it }) => {
beforeEach(async (context) => {
await arrangeTransactionHandlerSuite({
context,
makeHandler,
});
});
it('should validate, verify and run', async (context) => {
const { recipient, sender, transaction } = 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 executeTransaction({ context, transaction });
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 an integration test, we combine all of the above as follows. Now if something related to this functionality breaks, either you or your continuous integration can rapidly identify the issue when your tests execute (we highly recommend setting up continuous integration so that you can keep your sanity intact).

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 (Integration)', ({ assert, beforeEach, it }) => {
beforeEach(async (context) => {
await arrangeTransactionHandlerSuite({
context,
makeHandler,
});
});
it('should validate, verify and run', async (context) => {
const { recipient, sender, transaction } = 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 executeTransaction({ context, transaction });
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,
});
});
});

And That Is That, Amigos!

You may ask how the above example differs from the one we discussed in our unit testin’ blog since it looks like it basically does the exact same thing! In truth, the same thing is bein’ tested, but under two entirely different circumstances:

  • The unit test only ensured that the transaction works as expected within its own microcosm (what we’re sayin’ is that it simply performed a logic test)
  • The integration test goes much further as the entire transaction executes as it would when runnin’ a real blockchain with Bearmint - in other words, we test that the logic functions correctly, but we also ensure that account creation, the deduction of gas, the increasin’ of nonces and many other aspects execute as and when expected

And there you have it, friends! We hope you found everythin’ you just read real useful and trust that you now fully appreciate why integration testin’ is such an important part of software development. Should you have any questions or want to talk to us about anythin’ else, feel free to drop us a line on GitHub or any of our social media channels, and we’ll get back to you lickety split!