Skip to content

Commit

Permalink
Blog: Patterns for Structuring a Unit Test (#201)
Browse files Browse the repository at this point in the history
* Draft

* Edits

* Edits

* Edits
  • Loading branch information
emmercm authored Feb 4, 2022
1 parent 9f24757 commit b8cde4a
Showing 1 changed file with 171 additions and 0 deletions.
171 changes: 171 additions & 0 deletions src/blog/patterns-for-structuring-a-unit-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---

title: Patterns for Structuring a Unit Test
image: https://unsplash.com/photos/8-P12w-Ntwg
date: 2022-02-04T05:31:00
tags:
- testing

---

Unit tests should be testing acceptance criteria, not internal behavior. Here are some patterns to help accomplish that.

## Given-when-then

_Given-when-then_ is a way to write acceptance criteria scenarios in business requirements that then can be translated 1:1 to unit tests. This style stems from [behavior-driven development (BDD)](https://www.agilealliance.org/glossary/bdd) which specifies that tests should be written to test _behavior_ (as opposed to _state_). Tests should map so well to scenarios that they practically write themselves.

It's possible to use the _given-when-then_ test structure with acceptance criteria written in other ways, but there is a lot of benefit to be gained from tests matching acceptance criteria word for word.

Here are the three parts to each _given-when-then_ scenario (and therefore test):

- **Given** some context or state of the world
- **When** we invoke some action or behavior
- **Then** we should get some expected change

Some examples of good _given-when-then_ acceptance criteria using the [Gherkin](https://cucumber.io/docs/gherkin/reference/) syntax:

```text
Given a user that is logged out
When the user tries to authenticate with valid credentials
Then the user should be logged in
Given a user that is logged out
When the user tries to authenticate with invalid credentials
Then the user should be given an error
```

You can combine multiple expressions within each clause using "ands" like this:

```text
Given a user with $10 in gift card balance
And the price of an orange is $2
When that user places an order for 2 oranges
Then an order containing 2 oranges should be created
And that user should have $6 left on their gift card
```

Let's translate these scenarios into unit tests:

```javascript
const User = (username, password) => {
let loggedIn = false;
return {
isLoggedIn: () => loggedIn,
login: (usernameInput, passwordInput) => {
if (usernameInput !== username || passwordInput !== password) {
throw 'invalid username or password';
}
loggedIn = true;
}
};
};

const test = (func) => func();

test(() => {
// Given a user that is logged out
const user = User("admin", "letmein");

// When the user tries to authenticate with valid credentials
user.login("admin", "letmein");

// Then the user should be logged in
if (!user.isLoggedIn()) {
throw 'User was not logged in!';
}
});

test(() => {
// Given a user that is logged out
const user = User("admin", "letmein");

// When the user tries to authenticate with invalid credentials
try {
user.login("admin", "password123");
} catch(err) {
// Then the user should be given an error
return;
}

throw 'An error was not returned!';
});

console.log('All tests passed!');
```

```javascript
const User = (giftCardBalance) => {
return {
giftCardBalance: () => giftCardBalance,
purchase: (products) => {
const order = Order(products);
giftCardBalance -= order.total;
return order;
}
};
};

const Product = (name, price) => ({name, price});

const Order = (products) => ({
products,
total: products.reduce((total,product)=>total+product.price,0)
});

const test = (func) => func();

test(() => {
// Given a user with $10 in gift card balance
const user = User(10);

// And the price of an orange is $2a
const orange = Product("Orange", 2);

// When that user places an order for 2 oranges
const order = user.purchase([orange, orange]);

// Then an order containing 2 oranges should be created
if (order.products.length !== 2 || order.total !== 4) {
throw 'Invalid order produced!';
}

// And that user should have $6 left on their gift card
if (user.giftCardBalance() !== 6) {
throw 'User has an invalid gift card balance!';
}
});

console.log('All tests passed!');
```

Notice how each of these scenarios have only one "when". If we limit ourselves to only one behavior under test, we're forced to have a clearer purpose for the test.

There's a secondary benefit to writing tests this way - all of a domain's acceptance criteria lives forever in the code's tests, creating living documentation. It can be hard to know what the intended behavior of some systems is, but if the behavior is spelled out in specific language in tests then it should help alleviate some confusion.

The _given-when-then_ test style can be used with any testing framework, but there are some that explicitly encourage it with BDD: [Cucumber](https://cucumber.io/), [JBehave](https://cucumber.io/), and [SpecFlow](https://specflow.org/) to name a few.

## Arrange-act-assert

_Arrange-act-assert_ is mostly synonymous with _given-when-then_, but stems from [test-driven development (TDD)](https://www.agilealliance.org/glossary/tdd/) rather than BDD.

The three parts to an _arrange-act-assert_ test are:

- **Arrange** the data and state necessary for the test
- **Act** on the object or method under test
- **Assert** the expected results

The main value loss with using _arrange-act-assert_ is it tends to describe technical behavior and test internal state while _given-when-then_ encourages testing of functional behavior. Otherwise, there isn't a significant difference other than three "A" words are harder to distinguish at a glance.

_Arrange-act-assert_ should follow the same rules as _given-when-then_ such as only testing one behavior at a time, or only having one "act" expression.

I have seen a lot of _arrange-act-assert_ articles encourage mocking during "arrange" which can lead to the fallacy of testing _implementation_ rather than _behavior_. The debate of spies vs. mocks is too large to cover here.

## Setup-exercise-verify-teardown

Known as the "four-phase test", _setup-exercise-verify-teardown_ is nearly the same as _arrange-act-assert_ other than it explicitly calls out a "teardown" phase to reset the [system-under-test](https://en.wikipedia.org/wiki/System_under_test) to its pre-setup state.

If we try our best to create a [test pyramid](https://martinfowler.com/articles/practical-test-pyramid.html) where we have noticeably more unit tests than integration tests, or if we apply the principles of [hexagonal architecture](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) and use test doubles for our repositories, then the "teardown" step shouldn't be needed in most cases.

## Summary

Overall, the goal of these patterns is to help you write more readable and maintainable tests, one of the most important goals with software engineering. Each pattern was established at a different time and came from a different school of thought, but all have the same objectives.

0 comments on commit b8cde4a

Please sign in to comment.