This article's content
The concepts of testing

Types of tests

  • Unit Tests test very specific, low-level pieces of functionality for example if a function returns the expected value.
  • Integration Tests ensure that the individual pieces of your application work together correctly. For example if a method invocation successfully calls an API which in turn stores a value in the database.
    • Single-service integration Tests test a single piece or service of your application end to end, independent of the other pieces. For example, to test our server API code we would mock the database and use an integration testing framework to make calls to the API instead of the front-end. The bad thing is that this approach is susceptible to outside changes, in other words: If the database API changes then the server API tests would still pass, because we mocked them out.
    • Boundary Integration Tests test the communication between different pieces of your application. For example testing that the front-end is calling the server API correctly, or that the server API is calling the database API correctly. Don’t use test doubles for this. Disadvantage is that tests run slower.
  • End-to-End (E2E) also referred to as functional tests are tests to ensure that your entire application works as seen from the viewpoint of a user. The user does not know about how the internals work, that’s why this is sometimes called Black Box Testing.

When it comes to speaking how many tests there should be for each test type then you can assume that you will write mostly Unit Tests, a moderate amount of Integration Tests and a relatively small amount of E2E Tests.

Test cycles

Each test starts off by writing a failing test, then you write production code to make the test pass and finally you refactor the code. This is called red-green-refactor.

When you bring Integration and Unit Tests together you proceed like this: Start by writing a failing Integration Test, then move into the inner cycle of Unit Tests and do red-green-refactor until the outer cycle (the Integration Test) passes. At this time you stop development until you write another failing integration test.

Test tools

Type of test toolDescription
Test launcher or runnersStart your tests in the browser or in NodeJS and show their progress and feedback
Testing structureArrange tests in readable and scalable way
AssertionsCheck if a test returns what you expect
Mocks, spies and stubsreplaces parts of the code under test with a fake implementation
CoverageReports how much of your code is covered by tests
Browser controllersSimulate user actions for E2E tests
Visual regressionCompare the visual appearance to its previous version
SnapshotCompare the structure of a current test with a previously recorded one

Testing terminology

Test spy

A Test Spy is a function that spies (or reports) on a function, but it does not change your function in any way. A spy is pretty much like placing console.log() in your function body. A spy is used to answer questions about a function because it records

  • if a function was notCalled
  • if a function got calledWith arguments at least one time
    • or calledWithExactly() arguments
    • or alwaysCalledWith() specific arguments
    • or that the first argument of the first call of a function was ‘foo’
      expect(spy.getCall(0).args[0]).to.equal('foo');
  • how many times a function got called (calledCount).
  • what values a function returned()
  • the value of the this-keyword
  • if a function threw() exceptions
const testSpy = sinon.spy(Crypto, 'randomBytes');

A Spy is great to report back information on a function, but it does not control the behavior of the function.

Test stub

A test stub is like a stunt double in an action movie. It looks the same but it keeps the original actor safe. In the testing world that means that we can replace any calls to external systems, such as databases or file systems, with a stub. The external system will not be invoked when we run the test. This is great to isolate the code under test from the code that should not be tested at the same time.

We can even go a step further and have stubs return specific values, throw an exception, resolve or reject a promise or call a separate fake function.

const testStub = sinon.stub(Crypto, 'randomBytes');

A Stub allows you to control/force how a function behaves.

Fakes

A Fake is a combination of a Spy and a Stub, but they are not a universal replacement because they come with differences. Fakes are immutable. Fakes do not automatically replace your real function with your fake function, instead you have to explicitly call a replace function (more about it later).

Mocks

With Mocks you write your expectations first, and then execute the function under test and finally verify if those expectations have been met. Mocks contain all the functionality of a Spy and a Stub and own functions on top.

Doubles

A Double is just a general term for Mock, Fake and Stub.

Fixture

The purpose of a test fixture is to ensure that there is a well known and fixed environment in which tests are run so that results are repeatable. Some people call this the test context.

Examples of fixtures:

  • Loading a database with a specific, known set of data
  • Erasing a hard disk and installing a known clean operating system installation
  • Copying a specific known set of files
  • Preparation of input data and set-up/creation of fake or mock objects

Rules to follow when testing

  • Never spy on the function under test, instead spy on functions inside the tested function.
  • Tests should not pass or fail based on an external system, instead use stubs to replace the external systems.
  • Tests should be RITE.
    • Readable, even more easy to read than your productive code
    • Isolated, without affecting results of other tests
    • Thorough, to test as many variations as possible. If you can break your production code without breaking your tests, then the tests are not thorough enough.
    • Explicit, meaning all information to run the test is readily available, there is no hidden information or shared state
  • Don’t use Test Doubles as a band-aid for bad code. Refactoring your functions to become pure functions is a good approach to prevent using Test Doubles.
  • Don’t mock what you don’t own, because third-party software changes constantly. Mocking a library, then updating the library version would lead to the situation that your tests pass (because of an outdated mock) while the production code breaks.
  • Never use test doubles in unit tests, because that strongly indicates that your production code is too tightly coupled and needs refactoring

About Author

Mathias Bothe To my job profile

I am Mathias, born 41 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 17 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.