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:
- Planning and setting up the environment
- Writing the test cases and scripts
- Executing test cases using a testing framework
- 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 Software Development.
In this article:
- How Unit Tests Work
- Why Unit Testing Is Important
- How Does Unit Testing Compare to Other Types of Testing?
- Can You Use Unit Testing for Security?
- Unit Testing Techniques
- Unit Testing Examples
- Unit Testing Best Practices
- Security Unit Testing with Bright Security
How Unit Tests Work
Unit tests usually consist of four phases:
- 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.
- Writing the test cases and scriptsâdevelopers write the unit test code and prepare the scripts to execute the code.
- Executing the unit testingâthe unit test runs and reveals how the code behaves for each test case.
- 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 Software Development 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 software development.
Environment Variables
Authored by Configu
- Leveraging Environment Variables in Python Programming
- Environment Variables: How to Use Them and 4 Critical Best PracticesÂ
- 4 Ways to Set Docker Compose Environment VariablesÂ
Code Documentation
Authored by Swimm
- Agile Documentation: Benefits and Best Practices
- Documentation as Code: Why You Need It & How to Get Started
- The Importance of Continuous Integration Documentation
Technical Documentation
Authored by Swimm
- What is Technical Documentation and How to Create it?Â
- Get started with technical documentation writing best practicesÂ
- Essential Resources for Technical Documentation TemplatesÂ
