Sign Up Login
Resource Center  >  Blog

Unit Testing: Definition, Examples, and Critical Best Practices

Publication:
May 24, 2022
Author:
Oliver Moradov

What Is Unit Testing?

A unit test is a type of software test that focuses on components of a software product. The purpose is to ensure that each unit of software code works as expected. A unit can be a function, method, module, object, or other entity in an application’s source code. 

The objective of a unit test is to test an entity in the code, ensure that it is coded correctly with no errors, and that it returns the expected outputs for all relevant inputs.

Unit tests are typically created by developers during the coding phase of a project, and are written as code that exists in the codebase alongside the application code it is testing. Many unit testing frameworks exist that help developers manage and execute unit tests. 

This is part of an extensive series of guides about CI/CD.

In this article:

How Unit Tests Work

Unit tests usually consist of three phases: 

  1. Planning—developers consider which units in the code they need to test, and how to execute all relevant functionality of each unit to test it effectively.
  2. Test cases and scripts—developers write the unit test code and prepare the scripts to execute the code.
  3. Unit testing and results—finally, the unit test runs and developers can identify errors or issues in the code and fix them.

Test-driven development (TDD) is a common approach to unit testing. It requires the developer to create the unit test first, before the application code actually exists. Naturally, that initial test will fail. Then the developer adds the relevant functionality to the application until the tests pass. TDD usually results in a high quality, consistent codebase.

Effective unit testing typically:

  • Runs each test case in an isolated manner, with “stubs” or “mocks” used to simulate external dependencies. This ensures the unit tests only considers the functionality of the current unit under test.
  • Does not test every line of code, focusing on critical features of the unit under test. In general, unit testing should focus on code that affects the behavior of the overall software product.
  • Verifies each test case using criteria determined in code, known as “assertions”. The testing framework uses these to run the test and report failed tests. 
  • Runs frequently and early in the development lifecycle.

When a software project has been thoroughly unit tested, developers know that each individual unit is efficient and error-free. The next step is to run integration tests that evaluate larger components of the program and how they interact.

How Does Unit Testing Compare to Other Types of Testing?

Unit Testing vs. Integration Testing

Integration testing involves testing software modules and the interaction between them. It tests groups of logically integrated modules.

Integration tests are also called thread testing, because they focus on communication between software components. Integration testing is important because most software projects consist of several independent, connected modules. 

The main difference between unit tests and integration tests is what and how they test: 

  • Unit tests test a single piece of code, while integration tests test modules of code to understand how they work individually and interact with each other.
  • Unit tests are fast and easy to run because they “mock out” external dependencies. Integration tests are more complex and require more resources to run because they must consider both internal and external dependencies (“real” dependencies).

Learn more in our detailed guide to unit testing vs. integration testing (coming soon)

Unit Testing vs. Functional Testing

Functional testing compares the capabilities of each software to the original specifications or user requirements, to ensure that it provides the desired output to end users.

Software developers use functional testing as a way to perform quality assurance (QA). Typically, if a system passes the functional tests, it is considered ready to release. Functional testing is important because it tries to closely mirror the real user experience, so it verifies that the application meets the customer’s requirements.

The difference between unit testing and functional testing can be summarized as follows:

  • Unit tests are designed to test single units of code in isolation. They are quick and easy to create, and help find and fix bugs early in the development cycle. They are typically run together with every software build. However, they are not a substitute for functional testing because they do not test the application end-to-end.
  • Functional testing aims to test the functionality of an entire application. It is time consuming to create and requires significant computing resources to run, but is highly useful for testing the entire application flow. Functional testing is an essential part of an automated test suite, but is typically used later in the development lifecycle, and run less frequently than unit tests.

Learn more in our detailed guide to unit testing vs. functional testing (coming soon)

Unit Testing vs Regression Testing

Regression testing is a type of software testing that evaluates whether a change in the application introduced defects. It is used to determine if code changes can harm or interfere with the way an application behaves or consumes resources. In general, unit tests are regression tests, but not all regression tests are unit tests. 

Unit tests are used by developers to verify the functionality of various components in their code. This ensures that all variables, functions, and objects work as expected.

Regression tests are primarily used after a programmer has completed a certain feature. Regression testing serves as a system-wide check to ensure that components that were not affected by a recent change continue to work as expected. It can include several types of tests. As part of a regression test suite, developers can run unit tests, to verify that individual features and variables behave as expected even after the change. 

Learn more in our detailed guide to unit testing vs. regression testing (coming soon)

Can You Use Unit Testing for Security?

It is common to create unit tests during development. However, these tests typically only test functionality and not other aspects of the code, such as security. Many organizations are adopting a “shift left” approach in which important aspects of a software project must be tested as early as possible in the software development lifecycle, when it is easy to remediate them. 

Writing security unit tests is a great way to shift left security, ensuring that developers catch security flaws in their software before a component even enters a testing environment – not to mention a production environment.

Security unit tests take the smallest testable unit of software in an application, and determine whether its security controls are effective. Developers should build security unit tests based on known security best practices for the programming language and framework, and the security controls identified during threat modeling.

Another best practice is to perform peer reviews between developers and application security specialists. Allowing peer review of selected test strategies and individual security tests helps detect edge cases and logical flaws that individual testers might miss. Peer reviews of testers are also a great opportunity for developers, testers, and security experts to learn from each other and expand their knowledge on latest threats and new development techniques.

Unit Testing Examples

Different systems support different types of unit tests. 

Android Unit Testing

Developers can run unit tests on Android devices or other computers. There are two main types of unit tests for Android.

Instrumented tests can run on any virtual or physical Android device. The developer builds and installs the application together with the test application, which can inject commands and read the application state. An instrumented test is typically a UI test that launches an application and interacts with it. 

A small instrumented test verifies the code’s functionality within a given framework feature (i.e., SQLite database). The developer might run these tests on different devices to assess how well the app integrates with different SQLite versions. 

Local unit tests run on the development server or computer. These are typically small, fast host-side tests that isolate the test subject from the other parts of the application. Big local unit tests involve running an Android simulator (i.e., Robolectric) locally on the machine. 

Here is an example of a typical UI interaction for an instrumented test. The tester clicks on the target element to verify that the UI displays another element:

// If the Start button is clicked
onView(withText("Start"))
    .perform(click())

// Then display the Hello message
onView(withText("Hello"))
    .check(matches(isDisplayed()))

The following snippet demonstrates part of a local, host-side unit test for a ViewModel:

// If given a ViewModel1 instance
val viewModel = ViewMode1(ExampleDataRepository)

// After loading data
viewModel.loadData()

// Expose data
assertTrue(viewModel.data != null)

Angular Unit Testing

Angular unit tests isolate code snippets to identify issues like malfunctions and incorrect logic. Executing a unit test in Angular can be especially challenging for a complex project with inadequately separated components. Angular helps developers write code in a manner that lets them test each application function separately.

Angular’s testing package offers two utilities: TestBed and async. TestBed is Angular’s main utility package.

The “describe” container includes multiple blocks, such as it, xit, and beforeEach. The “beforeEach” block runs first, but the rest of the blocks can run independently. The first block from the app.component.spec.ts file is beforeEach (within the describe container) and has to run before the other blocks. 

Angular declares the application module’s declaration from the app.module.ts file in the beforeEach block. The application component simulated/declared in beforeEach is the most important component for the testing environment. 

The system then calls the compileComponents element to compile all the component resources, such as styles and templates. The tester might not compile the component when using a web pack. The code should look like this:

beforeEach(async(() => {
   TestBed.configureTestingModule({
      declarations: [
         AppComponent
      ],
   }).compileComponents();
}));

Once the target component is declared in the beforeEach block, the tester can verify if the system created the component using the it block.

The fixture.debugElement.componentInstance element will create an instance of the AppComponent class. Testers can use toBeTruthy to test if the system truly creates the class instance:

it('creates the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
}));

The next block shows the access to the app component properties. By default, the system only adds the title property. The tester can easily verify the title’s consistency in the created component: 

it(`title should be 'angular-unit-test'`, async(() => {
     const fixture = TestBed.createComponent(AppComponent);
     const app = fixture.debugElement.componentInstance;
     expect(app.title).toEqual('angular-unit-test');
}));

The fourth block in the test string demonstrates the test’s behavior in a browser environment. Once the system creates a detectChanges component, it calls an instance of the component to simulate execution in the browser environment. After rendering the component, it is possible to access its child elements via the nativeElement object:

it('render title in h1 tag', async(() => {
   const fixture = TestBed.createComponent(AppComponent);
   fixture.detectChanges();
   const compiled = fixture.debugElement.nativeElement;
 expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-unit-test!');
}));

Learn more in our detailed guide to unit testing in Android (coming soon)

Node JS Unit Testing

Node.js allows developers to execute server-side JavaScript code. It is an open source platform that integrates with popular JavaScript testing frameworks such as Mocha. Testers can indicate that the code they inject is a test by inserting Mocha test API keywords.

For example, it() indicates that the code is a single test, while describe() indicates that it contains a group of test cases. There can be subgroups within a describe() test grouping. Each function takes two arguments: a description displayed in the test report and a callback function.

Here is an example or the most basic test suite with a single test case:

const {describe} = require('mocha');
const assert = require('assert');

describe('Simple test suite:', function() {
    it('1 === 1 should be true', function() {
        assert(1 === 1);
    });
});

The test’s output should look like this:

$ cd src/projects/IBM-Developer/Node.js/Course/Unit-9
$ ./node_modules/.bin/mocha test/example1.js

  Simple test suite:
    ✓ 1 === 1 should be true

  1 passing (5ms)

Mocha supports any assertion library. This example uses the Node assert module (a relatively less expressive library). 

Learn more in our detailed guide to unit testing in Node.js (coming soon)

React Native Unit Testing

React Native is an open source mobile app development framework for JavaScript-based applications. It has a built-in Jest testing framework. Developers can use Jest to ensure the correctness of their JavaScript codebase.

Jest is usually pre-installed on React Native applications as an out-of-the-box testing solution. The developer can easily open the package.json file and configure the Jest preset to React Native:

"scripts": {
            "test": "jest"
},
"jest": {
         "preset": "jest-react-native"
}

If, for example, the application has a function to add simple numbers, the tester can easily anticipate the correct result. It is easy to test by importing the sum function into the test file. The separate file containing the sum function might be called ExampleSumTest.js:

const ExampleSum = require('./ExampleSum');

test('ExampleSum equals 3', () => {
      expect(ExampleSum(1, 2).toBe(3);
});

The predicted Jest output should look like this:

PASS ./ExampleSumTest.js
✓ ExampleSum equals 3 (5ms)

Learn more in our detailed guide to React unit testing (coming soon)

See more examples in our guide: unit testing examples (coming soon)

Unit Testing Best Practices

Here are best practices you can use to make your unit testing more effective.

Learn about these and other best practices in our detailed guide to unit testing best practices (coming soon)

Write Readable Tests

Easy-to-read tests help other developers understand how code works, what it’s intended for, and what went wrong if a test fails. Readable tests tend to have less bugs, and if they do contain issues, they are much easier to troubleshoot without extensive debugging. 

Readability also improves the maintainability of tests, making it easier to update tests when the underlying code changes.

Another aspect of readability is that unit tests serve as documentation for describing and verifying various aspects of code unit behavior. So, making the tests clear and easy to read make it possible for new developers joining the team, or developers from other teams, to understand how the underlying code works.

Write Deterministic Tests

A deterministic test always passes (if there are no issues) or always fails (when issues exist) on the same piece of code. The result of the test should not change as long as you don’t change your code. By contrast, an unstable test is one that may pass or fail due to various conditions even if the code stays the same. 

Non-deterministic tests, also known as flaky tests, are not effective because they cannot be trusted by developers. They do not effectively report on bugs in the unit under test, and they can cause developers to ignore the result of unit tests (including those that are stable). 

To avoid non-deterministic testing, tests should be completely isolated and independent of other test cases. You can make tests deterministic by controlling external dependencies and environment values, such as calls to other functions, system time, and environment variables.

Unit Tests Should Be Automated

Make sure tests run in an automated process. This can be done daily, hourly, or through a continuous integration (CI) process. Everyone on the team should be able to access and view the reports. 

As a team, discuss the metrics you are interested in, such as code coverage, number of test runs, test failure rate, and unit test performance. Continuously monitor these metrics—a large change in a metric can indicate a regression in the codebase that should be dealt with immediately.

Don’t Use Multiple Asserts in a Single Unit Test

For unit tests to be effective and manageable, each test should have only one test case. That is, the test should have only one assertion. 

It sometimes appears that to properly test a feature, you need several assertions. The unit test might check each of these assertions, and if all of them pass, the test will pass. However, when the test fails, this makes it unclear what is the root cause of the bug. This also means that when one assertion fails, the others are not checked, which may leave unattended issues in the code.

Creating a separate test script for each assertion might seem tedious, but overall it saves time and effort and is more reliable. You can also use parameterized tests to run the same test multiple times with different values.

Unit Testing with Bright

Bright is a developer-first Dynamic Application Security Testing (DAST) scanner, the first of its kind to integrate into unit testing, revolutionizing the ability to shift security testing even further left. You can now start to test every component / function at the speed of unit tests, baking security testing across development and CI/CD pipelines to minimize security and technical debt, by scanning early and often, spearheaded by developers. With NO false positives, start trusting your scanner when testing your applications and APIs (SOAP, REST, GraphQL), built for modern technologies and architectures. Sign up now for a free account and read our docs to learn more.

See Our Additional Guides on Key CI/CD Topics

Together with our content partners, we have authored in-depth guides on several other topics that can also be useful as you explore the world of CI/CD.

Unit Testing Frameworks

GitHub Actions

Authored by Codefresh

CI CD Pipeline

Authored by NetApp

Related Articles:

Related topics

Dynamic Application Security Testing (DAST) is a crucial component in fortifying web applications against potential vulnerabilities. By taking a proactive stance, DAST systematically detects and addresses security flaws.

See more

By mapping Dynamic Application Security Testing (DAST) to the Payment Card Industry Data Security Standard (PCI DSS) requirements, organizations can

See more

What Is Mobile Application Security Testing?  Mobile application security testing is the process of assessing, analyzing, and evaluating the security

See more

Test Your Web App for 10,000+ Attacks

See Our Dynamic Application Security Testing (DAST) in Action

  • Find & fix vulnerabilities fast
  • Zero false positives
  • Developer friendly

and see how easy AppSec can be

Test Your Web App for 10,000+ Attacks

Integrate vulnerability testing into your DevOps pipeline. Find & fix vulnerabilities fast with zero false positives.
See Our Dynamic Application Security Testing (DAST) in Action
Testing variance Using Legacy Dast Using Dev-Centric Dast
% of orgs knowingly pushing vulnerable apps & APIs to prod 86% 50%
Time to remediate >Med vulns in prod 280 days <150 days
% of > Med vulns detected in CI, or earlier <5% ~55%
Dev time spent remediating vulns - Up to 60x faster
Happiness level of Engineering & AppSec teams - Significantly improved
Average cost of Data Breach (US) $7.86M $7.86M