The 5-minute guide to Software Testing concepts and strategy
Tags: testing, softwareengineering, learning, coding
Published: 2024-04-19 11:01:08

_Preface: I've recently taken it upon myself to establish automated testing practices at my current job. We're a small tech team in a growing startup and the margin for error was quite large, so up until now, manual testing sufficed and writing tests would've actually slowed us down._

_It has now come to the point where automated tests are worth investing time into, however. Thus, after my research, I've decided to put together a simple and brief guide that can serve as a starting point for others treading this path. Enjoy!_

---

# Unit vs Integration vs Contract vs White/Clear-box vs Black-box vs Acceptance vs End-to-End (E2E) testing... What does this all mean?

First, tests can be classified by scope or size of the _subject under test_ (SUT):

- **Unit testing**: testing the smallest blocks of code you can think of. Depending on language, these can be simple functions, or entire classes... No single definition exists, but consensus is to keep it small and limited to 1 task/operation/functionality.
- **Integration testing**: testing your components together (think 2 Units interacting with each other) or with external components (like calling a DB). External components are usually mocked.
- **Contract testing**: tests for your integration tests. If your integration tests no longer match the external dependencies, these will tell you.
- **Acceptance testing**: checking your _entire system_ behaves as expected, AKA fulfills requirements. For example: "If I press sign up and fill the form asking for my details, I would expect my user to be created with those details". This requires all components and external dependencies to be tested together.
- **End-to-end testing**: The difference with Acceptance tests isn't crystal-clear, and I think they are 2 sides of the same coin.

Second, they can be classified by the nature of how they test:

- **White/Clear-box testing**: testing that takes into account the internal workings of the code, meaning the focus is to try all the possible execution branches, usually measured by _code coverage_ (tested code/total code). They test _how_ the code works.
- **Black-box testing**: testing that only cares about inputs and outputs, disregarding implementation. They test _what_ the code does.

Generally Black-box testing is viewed as a more "productive" or "useful" way to test, but this is highly subjective.

# What strategy do I follow?

Generally speaking you should keep 3 things in mind:

**1. Test as much or as little as you need to give you a level of confidence in your code you're happy with.**
**2. Only test your code.**
**3. Keep tests out of the way.**


1 is self-explanatory.
2 means don't test external frameworks or dependencies. They should have their own tests.
3 means don't make tests cumbersome to run or maintain, or people will either skip many of them or not run them in the first place. They'll break and be an annoyance, increasing lead times.

## How?

1. **Prioritise**. Start with the core features, then expand as you see fit. It's better to have 20% of the code tested than nothing at all. Go for happy paths and most-common routes, then focus on edge-cases and other concerns.
2. **Black-box at first**. They tend to be far easier to write, and not tied to actual implementation, so they only break if your code stops doing what it's supposed to. By extension, they also require less changes (only changed when requirements change).
3. **Use the "Given, When, Then" syntax** (AKA "Arrange, Act, Assert"). _Given a user wants to log in, when they enter the wrong password, then they should see an error message and be denied entry._
4. **Keep a "commit suite" and a "deployment suite"**. The commit suite should be the one you run after every significant change and/or prior to a commit. It should only involve the code you're working with and be extremely fast. The deployment suite should be run before any deployment (likely as a PR check, or a pre-push git hook)
5. **Only tests public interfaces**. Do not test `private` methods, and even worse, don't make them `public`/`protected` just to test them. Only test what is reachable/callable from outside.

Have a lot of fast, easy to maintain and self-explanatory Unit tests. A few less Integration tests that ensure everything works together and a couple of Acceptance tests that are only run in the deployment suite or manually every day. Have Contract tests to ensure your Integration tests are still correct, and run those once a week or so and that's it, you're now successfully (and pragmatically) testing your code!

---

Disclaimer: Because I've made an effort to keep it brief, **I've obviously glossed over details and nuances and made over-simplifications**. This is meant as a primer, not the end-all-be-all ultimate guide to testing. For that, I recommend you check out some of the following sources:

1. [The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)
2. [Test Doubles](https://martinfowler.com/bliki/TestDouble.html)
3. [Given, When, Then](https://martinfowler.com/bliki/GivenWhenThen.html)
4. [Unit Test](https://martinfowler.com/bliki/UnitTest.html)
4. [Integration Test](https://martinfowler.com/bliki/IntegrationTest.html)
5. [Contract Test](https://martinfowler.com/bliki/ContractTest.html)
6. [Testing Best Practices](https://github.com/goldbergyoni/javascript-testing-best-practices)