Software Design - Unit Tests vs Integration Tests
Preface
Unit tests and integration tests are designed to serve two different purposes. They are not meant to substitute each other, and each type has a specific focus. However, developers tend to struggle to determine the border that seperates unit tests and integration tests as there is no set hard limit on either one.
What is a Unit Test?
Unit is somewhat of an ambiguous term, but in this situation it can be thought of as the smallest piece of code that can be isolated from the rest of the program. Typically, units are a method, or class, but in the end it comes down to your personal definition of a unit. The only thing to keep in mind is that while there’s no size limit on a unit test the smaller, the better.
Once a unit has been isolated from the program it is then subjected to some kind of check. This could be checking the value generated by a method, or ensuring an error is thrown when a class is instantiated incorrectly. Unit tests should be kept simple and a single test should only ever check one condition. A unit test should never rely on an external dependency, because at that point it’s no longer a single unit being tested. Below is an example of a unit test in TypeScript using the well known testing library Jest:
test('Initialized with an id of undefined.', () => {
let user: User = new User();
expect(user.id).toBeUndefined();
});
What is an Integration Test?
Integration tests can be thought of as larger tests that check to verify how components of a system interact with each other. This could be a business logic service communicating with the database, or checking for a specific http status code returned by the server when processing a http request.
Integration tests are ran less frequently, and therefore are permitted to take longer. These tests can help ensure the system is functional as a whole, and help weed out logic bugs that may be overlooked. The following is an example of a integration test in TypeScript using Jest:
test('Users are generated and inserted into the database.', () => {
let userReg: UserRegistration = new UserRegistration('Bert', 'hunter2');
let user: User = await Database.registerNewUser(userReg);
expect(user).not.toBeNull();
});
How to tell them apart?
Distinguishing the difference between a unit test and integration test can be difficult. In the end it comes down to the nature of the test. Unit tests fall under the ‘white box testing’ category. A white box test (also known as a ‘clear box test’) is a test where the underlying implementation of the subject is known, and is being checked for a consistent behaviour. Unit tests typically only serve a useful purpose to the developer who needs to verify a minor implementation detail.
Integration tests reside in the ‘black box testing’ category. A black box test is one where the underlying implementation of the subject is unknown. These tests verify the external functionality of a subject, and check to verify features are working as intended.
When to use them?
Unit tests need to be quick, as the faster they are, the more often the developer will be willing to run them. This is one of the primary reasons why unit tests should be kept as small as possible. No developer wants to constantly pause their work to kick off a unit test cycle that could take several minutes to complete.
On the other end, integration tests only need to be ran when checking in code to source control. These tests help verify that existing functionality is not compromised by new code, and if any tests fail the check-in can be rejected. This helps keep a higher standard of quality in source control, and helps keep the code in a state that can be released.
Integration tests are typically ran as part of a ‘continuous integration’ pipeline. This is a topic for another day however.