Improve Your Tests Like a Ninja

3 Simple Habits to Improve Your Tests

How can tests be your best documentation? What small changes can improve the contract between your code and its consumers? Learn how to improve your tests from a real-world example.

Yes, we write them to ensure fewer things break before you push changes (a.k.a regression). The thing is – tests can be more than just “tests”. They can be a living and breathing documentation of your contract with your users/consumers. They can be documentation for other developers.

Let’s review three simple habits that can improve your tests.

Writing a Test

Our story begins with an accordion component:

This accordion component has an expandmode API. It can either allow a single item or multiple items to be expanded at the same time.

In the test, we’d like to test the single and the multi.

This is the original test written by the accordion’s developer:

describe('non multi', () => {
it('should only allow one accordion items open at a time', async () => {
expect(accordionItem1.open).toBeFalsy();
expect(accordionItem2.open).toBeFalsy();
accordionItem1.open = true;
accordionItem2.open = true;
expect(accordionItem1.open).toBeFalsy();
expect(accordionItem2.open).toBeTruthy();
});
});
describe('multi', () => {
it('should allow all accordion items open when multi', async () => {
element.expandmode = 'multi';
expect(accordionItem1.expanded).toBeTruthy();
expect(accordionItem2.expanded).toBeFalsy();
accordionItem1.expanded = true;
accordionItem2.expanded = true;
expect(accordionItem1.expanded).toBeTruthy();
expect(accordionItem2.expanded).toBeTruthy();
});
});
view raw test.ts hosted with ❤ by GitHub

The describe in lines (lines 1 and 14) tell us we test the non multi and multi cases.

The tests themselves are also explained in the it use cases (lines 2 and 15).

The test for the single mode is as follows:

  1. Expect both items to be closed (open property of both items to be false)
  2. Action – set both items to true
  3. Assertion – expect the first item to be closed and the second item to be open

Sounds like we are testing what we want, right?

The test for the multi case is quite similar:

  1. Change expandmode to multi
  2. Expect item1 to be open and item2 to be closed
  3. Open both items
  4. Expect both items to be open

Tests what we want? Could be. Can we be more explicit in defining our API? Let’s see.

Improve Your Tests Step #1: Describe the API

The “describe” section doesn’t state the API used. We should group the two use cases under expandmode like this:

describe('expandmode', () => {
it('should only allow one accordion items open at a time', async () => {
expect(accordionItem1.open).toBeFalsy();
expect(accordionItem2.open).toBeFalsy();
accordionItem1.open = true;
accordionItem2.open = true;
expect(accordionItem1.open).toBeFalsy();
expect(accordionItem2.open).toBeTruthy();
});
it('should allow all accordion items open when multi', async () => {
element.expandmode = 'multi';
expect(accordionItem1.expanded).toBeTruthy();
expect(accordionItem2.expanded).toBeFalsy();
accordionItem1.expanded = true;
accordionItem2.expanded = true;
expect(accordionItem1.expanded).toBeTruthy();
expect(accordionItem2.expanded).toBeTruthy();
});
});

By grouping use cases according to their API’s (in this case, the expandmode property), we make it more apparent to the reader what the API is.

If we follow this small habit throughout our tests, our test files will look like this:

How the documentation looks like in our test for our module/component/class. Each API has its own describe in which we add its which are practically use cases

We will create something marvelous called: “Documentation”.

Improve Your Tests Step #2: Documenting the API’s Usage

In the single step, the API usage is implicit. We should strive to show explicitly how to use it:

describe('expandmode', () => {
it('should allow one accordion items open when set to “single”', async () => {
element.expandedmode = ‘single’;
expect(accordionItem1.open).toBeFalsy();
expect(accordionItem2.open).toBeFalsy();
accordionItem1.open = true;
accordionItem2.open = true;
expect(accordionItem1.open).toBeFalsy();
expect(accordionItem2.open).toBeTruthy();
});
it('should allow multiple items to open when set to “multi”', async () => {
element.expandmode = 'multi';
expect(accordionItem1.expanded).toBeTruthy();
expect(accordionItem2.expanded).toBeFalsy();
accordionItem1.expanded = true;
accordionItem2.expanded = true;
expect(accordionItem1.expanded).toBeTruthy();
expect(accordionItem2.expanded).toBeTruthy();
});
});

By stating the setup explicitly (in this simple case, element.expandmode = 'single'), we tell the reader: “This is how you use this API”.

Remember we are creating documentation? This is a live example of how the API is used right before we assert the usage works as expected.

Improve Your Tests Step #3: Creating Triple-A Tests

Many standards were developed to help us avoid common pitfalls.

The AAA pattern(Arrange, Act Assert) is one of them.

In its essence it states that the tests comprise of three parts:

  1. Arrangement – setup the scenario for the test.
  2. Action – the action that should trigger the use case tested.
  3. Assertion – our expectations for the action’s results in the given setup.

Here’s the code after a change to reflect the AAA pattern:

describe('expandmode', () => {
it('should allow one accordion items open when set to “single”', async () => {
element.expandedmode = ‘single’;
const accordionItem1OpenStateBefore = accordionItem1.open;
const accordionItem2OpenStateBefore = accordionItem2.open;
accordionItem1.open = true;
accordionItem2.open = true;
expect(accordionItem1OpenStateBefore).toBeFalsy();
expect(accordionItem2OpenStateBefore).toBeFalsy();
expect(accordionItem1.open).toBeFalsy();
expect(accordionItem2.open).toBeTruthy();
});
it('should allow multiple items to open when set to “multi”', async () => {
element.expandmode = 'multi';
const accordionItem1OpenStateBefore = accordionItem1.open;
const accordionItem2OpenStateBefore = accordionItem2.open;
accordionItem1.expanded = true;
accordionItem2.expanded = true;
expect(accordionItem1OpenStateBefore).toBeFalsy();
expect(accordionItem2OpenStateBefore).toBeFalsy();
expect(accordionItem1.expanded).toBeTruthy();
expect(accordionItem2.expanded).toBeTruthy();
});
});

Now all of the setup is being made at the top, the actions in the middle, and the expectations at the bottom.

This pattern repeats itself in all tests; the reader knows what to expect in every test case. This reduces the cognitive load on the reader in many cases.

Another benefit of the AAA pattern is to raise a red flag. Sometimes it would seem that the pattern cannot always be implemented. For example, a test might have more than one step (a.k.a multiple Actions). This could hint on several “smells” like:

  • We are testing more than one use-case and should consider splitting the test block.
  • Our implementation is too complex (it usually happens with TAD (Test After Development)).

Summary

The code review above shows a refactor to two very simple tests. After this short refactor, we can easily discern our API and how to use it:

expandmode API => has single and multi use cases => we can use it by setting the property with the string value. We also know what to expect from changing it.

When looking at bigger test suites, the benefits of these three steps will be much more noticeable:

  • Our API will be fully documented with usage examples
  • Our API will be safer to refactor and extend
  • We are less likely to miss use cases, and new use cases for API’s have a clear place
  • We increased readability project-wide (if said practices were used project-wide)

For more tests tips, check this link

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments