Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

 

The enduring value of unit tests

SilverStripe developer Jess Champion recently presented a talk at Decompress 2018. She summarises her talk on unit testing and answers the question, why do we do automated testing?

Read post

We spend a lot of time thinking and talking about how to write better code, but much less time on how to write better tests. Today I’ll examine how unit tests are a critical component of robust, stable and maintainable software across the full lifecycle. We'll examine some of the theory around unit testing and explain how it adds business value and supports agile development practices. I will also give opinionated practical advice for writing robust test suites.

Why do we do automated testing?

Unit testing is a form of automated testing, so to start I thought we'd go over the benefits of automated testing in general.

  • To reduce the cost of testing

Manual testing is great, it provides a lot of value, but it’s labour intensive, expensive and it's relatively slow. Automated test coverage reduces the amount of testing that needs to be done manually. Automated testing is quick and relatively cheap. Although there’s an upfront cost to write the tests, they can be run again and again for practically nothing. Unit testing is a form of automated testing, so to start I thought we'd go over the benefits of automated testing in general.

  • To reduce the cost of change

By reducing the cost of testing we reduce the cost of change. Making it easier and less risky to make changes, refactor and build enhancements. Automated testing also helps with maintenance work such as framework upgrades or security patching, as they can be used to validate that the key functionality of the application still functions as expected after the upgrade.
Reducing the cost of change helps to increases the lifespan of your solution - allowing you to keep working with what you have for longer.

  • To facilitate continuous integration

A robust automated test suite is essential to Continuous Integration. It helps you to ship faster and more often, with greater confidence that there are no unintended breakages or side effects.

  • To increases quality and robustness of your code

    • Writing tests helps you find bugs before they get into production.
    • Writing easily testable code encourages looser coupling and more separation of concerns, resulting in more flexible and extendable code.
    • Tests are a form of documentation, they tell you how the code was intended to function in various circumstances.

Unit testing in context

This diagram; adapted from Martin Fowler’s test pyramid; illustrates a well-balanced test pyramid; made up of unit, integration and UI tests. Generally, the cost to create, maintain and run the test trends up. While the time taken, to run the tests trends down.

UI, unit and integration test triangle

Unit tests

Test an isolated unit of code (like a function or a class), they don’t rely on external resources, such as an API or database. Unit tests are both fast and cheap, which is why we look to them to create a solid base for our test harness. Saving the more complex test scenarios for integration or UI tests.

Integration Tests

Tests how the parts of the system work together. They may use a database, have network requests or talk to a real API.

UI tests

UI tests (or Functional, or end to end, or use case tests, or behaviour tests) test the application through automated UI interactions. They generally have fewer scenarios covering more functionality per test and often automate testing a realistic use case; for example, login, and then check your balance. They may rely on external resources.

What do I need to test?

Defining the scope of your tests can be one of the hardest things to figure out when you first start unit testing. I find taking a behaviour driven development approach to unit testing helps me focus my testing strategy: test the behaviours the code performs through the public interface.

Behaviour Driven Development

In BDD - You write tests (aka specs) for each behaviour that the code performs or needs to perform rather than for each method. Each spec describes - in natural language - the behaviour of the code being tested. Specs become a part of the documentation for the code base.

When tests fail, the description helps you identify what’s broken. BDD frameworks like jasmine make this really easy as specs are part of the syntax. Below is an example of jasmine syntax

describe('Behaviour Driven Development', ()=> {

it('should describe in natural language
the behaviour of the code under test',
() => {…}
);

it('should become a part of the
documentation of the code base',
() => {…}
);
};

Spec-ing up

Spec-ing up is the process of analysing the code under test to determine ‘what’ it does and writing out a list of empty tests for each behaviour.

When you’re spec-ing up - you want to try to cover all possible paths through the code. This means accounting for error handling, invalid parameters, unexpected data and edge cases. Testing these ‘sad’ paths helps us to iron out a lot of bugs and oversights before they get to production, making your solution more robust.

Once we have our complete list of specs, we figure out how to test those behaviours through our public interface.

Public Interface

The public interface is the part of your code that is publicly accessible. In the below example, public interface is the exported methods ‘addItemToCart’ and ‘getCartWithItems’, while ‘getTotal’ and ‘addToItems’ are private. We’re still going to test the behaviour of the private functions, but we’ll access them through the public interface.

const addToItems = (items) => {...};

const getTotal = (items) => {...};

export default class Cart {
constructor(items = []) {
this.items = items;
this.total = getTotal(items);
}
addItem(newItem){
this.items = getTotal(newItem, this.items);
this.total = getTotal(items);
}
};

If you’re testing components the public interface may include UI events like click and input. You can test these programmatically triggering the events, there are libraries such as enzyme for React or vue-test-utils to help with this.

We control the paths the tests take by piping specific values into the public interface which call our private functions. We then assert that the publicly visible properties were affected as we intended.

import Cart from './cartClass'
describe('A shopping cart', () => {
describe('create cart', () => {
it('creates an new empty cart', () => {
const testCart = new Cart([]);
expect(testCart.items.length).toBe(0);
expect(testCart.total).toBe(0);
});
it('creates an new cart with items', () => {...});
});
describe(add item to cart', () => {
it( 'adds item to an empty cart', () => {...});
it( 'adds item to cart with existing items', () => {...});
it( 'adds item to cart with existing items of that type', () => {...});
it( 'calculates cart total when adding items', () => {
const testCart = getCart(TEST_ITEMS);
const testItem = {
id: 2, price: 2.5, quantity: 5
};
testCart.addItem(testItem);
expect(update Cart.total).toBe(28.5);
});
});'
});

Care and feeding

Now that you’ve invested all this time and energy into your test harness–you’ll want to keep it in good condition. Some quick tips to close:

  • Have a team rule that you don’t merge or deploy unless the tests are green (passing). Nobody enjoys fixing tests someone else broke.
  • Your test harness is only as valuable as the team's trust in it. Intermittently failing tests or false negatives erode trust in the harness–fix it or remove it.
  • Use continuous integration or git hooks to automatically run your tests, rely on human diligence as little as possible.
  • And make sure to add instructions for running and debugging the tests to the project readme.

parody of the keep it green new zealand ad campaign

Decompress

The material in this blog was originally written for a presentation at decompress 2018  conference. That talk covers further topics including; test-driven development and test doubles. You can check out the video here (it was my first talk - I was pretty nervous).

Special thanks to SilverStripe for generously providing the travel sponsorship that helped me to attend and speak.

About the author
Jess Champion

With a background spanning product development, operations and project delivery, Jess has developed a passionate focus on the quality and maintainability of software. While she can happily work across the full stack, Jess has a passion for all things frontend, and is always looking for ways to leverage new technologies to craft immersive digital experiences.

Post your comment

Comments

No one has commented on this page yet.

RSS feed for comments on this page | RSS feed for all comments