Bright is now integrated with GitHub Copilot

Check it out! →
Product
Product overview

See how dev-centric DAST for the enterprise secures your business.

Web attacks

Continuous security testing for web applications at high-scale.

API attacks

Safeguard your APIs no matter how often you deploy.

Business logic attacks

Future-proof your security testing with green-flow exploitation testing.

LLM attacks

Next-gen security testing for LLM & Gen AI powered applications and add-ons.

Interfaces & extensions

Security testing throughout the SDLC - in your team’s native stack.

Integrations

Connecting your security stack & resolution processes seamlessly.

Docs

Getting started with Bright and implementing it in your enterprise stack.

Book a demo

We’ll show you how Bright’s DAST can secure your security posture.

Resources
Blog

Check out or insights & deep dives into the world of security testing.

Webinars & events

Upcoming & on-demand events and webinars from security experts.

Docs

Getting started with Bright and implementing it in your enterprise stack.

Case studies

Dive into DAST success stories from Bright customers.

Research

Download whitepapers & research on hot topics in the security field.

Company
About us

Who we are, where we came from, and our Bright vision for the future.

News

Bright news hot off the press.

Webinars & events

Upcoming & on-demand events and webinars from security experts.

We're hiring

Want to join the Bright team? See our open possitions.

Bug bounty

Found a security issue or vulnerability we should hear about? Let us know!

Contact us

Need some help getting started? Looking to collaborate? Talk to us.

Resources > Blog >
Unit Testing: Definition, Examples, and Critical Best Practices

Unit Testing: Definition, Examples, and Critical Best Practices

Nickolay Bakharev

What Is Unit Testing?

A unit test is a type of software test that focuses on testing individual components of a software product. Software developers and sometimes QA staff write unit tests during the development process.

The ‘units’ in a unit test can be functions, procedures, methods, objects, or other entities in an application’s source code. Each development team decides what unit is most suitable for understanding and testing their system. For example, object-oriented design tends to treat a class as the unit. Unit testing relies on mock objects to simulate other parts of code, or integrated systems, to ensure tests remain simple and predictable.

The main goal of unit testing is to ensure that each unit of the software performs as intended and meets requirements. Unit tests help make sure that software is working as expected before it is released. 

The main steps for carrying out unit tests are: 

  1. Planning and setting up the environment
  2. Writing the test cases and scripts
  3. Executing test cases using a testing framework
  4. Analyzing the results

Some advantages of unit testing include: 

  • Early detection of problems in the development cycle
  • Reduced cost
  • Test-driven development
  • More frequent releases
  • Enables easier code refactoring
  • Detects changes which may break a design contract
  • Reduced uncertainty
  • Documentation of system behavior

Learn more in our detailed guide to unit testing vs integration testing.

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

In this article:

How Unit Tests Work

Unit tests usually consist of four phases: 

  1. Planning and setting up the environment—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. Writing the test cases and scripts—developers write the unit test code and prepare the scripts to execute the code.
  3. Executing the unit testing—the unit test runs and reveals how the code behaves for each test case.
  4. Analyzing the results—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.

Benefits of Unit Testing

Advantages of unit testing include:

  • Detecting problems early in the development cycle—unit testing helps in identifying bugs and issues at an early stage of the software development cycle. This early detection is crucial as it allows for issues to be addressed before they escalate into more complex problems in later stages of development.
  • Reducing costs—by catching bugs early, unit testing can significantly reduce the cost of bug fixes. It is generally more expensive to fix bugs in later stages of development or after the software has been deployed.
  • Promoting test-driven development—unit testing is a core component of TDD, where tests are written before the actual code. This approach ensures that the codebase is designed to pass the tests, leading to better structured, more reliable, and easier to maintain code.
  • Enabling more frequent releases—with a comprehensive suite of unit tests, developers can make changes to the code with more confidence. This reduces the risks associated with new releases, thereby allowing for more frequent updates and improvements to the software.
  • Enabling code refactoring—unit tests provide a safety net that allows developers to refactor code with confidence. Knowing that changes can be quickly tested to ensure they don’t break existing functionality encourages improving and optimizing the code without fear of introducing bugs.
  • Detecting changes that break a design contract—unit tests can help in identifying changes in the code that may violate the intended design or contract of a system. This ensures that individual components of the software work as expected and in harmony with each other.
  • Reducing uncertainty—with a robust unit testing process, developers gain confidence in the quality and functionality of their code. This reduces uncertainty and guesswork, especially when making changes or adding new features.

Documenting system behavior—unit tests can serve as a form of documentation for the system. By reading the tests, other developers can understand what a particular piece of code is supposed to do, which is especially useful for onboarding new team members or for reference in future development.

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 Techniques

Structural Unit Testing

Structural testing is a white box testing technique in which a developer designs test cases based on the internal structure of the code, in a white box approach. The approach requires identifying all possible paths through the code. The tester selects test case inputs, executes them, and determines the appropriate output. 

Primary structural testing techniques include:

  • Statement, branch, and path testing—each statement, branch, or path in a program is executed by a test at least once. Statement testing is the most granular option.
  • Conditional testing—allows a developer to selectively determine the path executed by a test, by executing code based on value comparisons.
  • Expression testing—tests the application against different values of a regular expression.

Related content: Read our guide to unit testing javascript.

Functional Unit Testing

Functional unit testing is a black box testing technique for testing the functionality of an application component. 

Main functional techniques include:

  • Input domain testing—tests the size and type of input objects and compares objects to equivalence classes.
  • Boundary value analysis—tests are designed to check whether software correctly responds to inputs that go beyond boundary values.
  • Syntax checking—tests that check whether the software correctly interprets input syntax.
  • Equivalent partitioning—a software testing technique that divides the input data of a software unit into data partitions, applying test cases to each partition.

Error-based Techniques

Error-based unit tests should preferably be built by the developers who originally designed the code. Techniques include:

  • Fault seeding—putting known bugs into the code and testing until they are found.
  • Mutation testing—changing certain statements in the source code to see if the test code can detect errors. Mutation tests are expensive to run, especially in very large applications.
  • Historical test data—uses historical information from previous test case executions to calculate the priority of each test case.

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 angular.

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). 

Related content: Read our guide to unit testing in nodejs.

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 vue unit testing.

Unit Testing Best Practices

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

Learn more in our detailed guide to unit testing vs functional testing.

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.

Related content: Read our guide to unit testing best practices.

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 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.

CI/CD Pipeline

Authored by Codefresh

Jenkins

Authored by Codefresh

Software Deployment

Authored by Codefresh

Resources

IASTless IAST – The SAST to DAST Bridge

Streamline appsec with IASTless IAST. Simplify deployment, enhance accuracy, and boost your security posture by combining SAST and Bright’s DAST.

Bringing DAST security to AI-generated code

AI-generated code is basically the holy grail of developer tools of this decade. Think back to just over two years ago; every third article discussed how there weren’t enough engineers to answer demand; some companies even offered coding training for candidates wanting to make a career change. The demand for software and hardware innovation was

5 Examples of Zero Day Vulnerabilities and How to Protect Your Organization

A zero day vulnerability refers to a software security flaw that is unknown to those who should be mitigating it, including the vendor of the target software.

Get our newsletter