What is unit testing?
Unit testing refers to verifying the behavior of code at its most foundational level — the unit. A unit is the smallest portion of code (typically a function or method of an object) that can be isolated and tested independently. Usually developers test each unit against potential scenarios that might occur when users interact with the software.
Consider a function that capitalizes a string of letters in a text-editing program. Your unit test would assert that the code generates a capital letter after you input the corresponding lowercase letter. The test could also assert that the function raises an error when you input a number as an argument.
As the example above illustrates, the purpose of unit testing is to confirm that your code is working correctly. You are showing that your function will produce the desired outputs or perform the way you expect. For this reason, unit testing gives you confidence that your function will continue to operate correctly, even when other pieces of the code are changed during the development process.
Optimize development workflows. Try Aha! Develop.
Sometimes referred to as component testing, unit testing is the most granular level of software testing. Other stages typically include integration, system, and acceptance testing. Detecting defects in the code early through unit testing helps the development team save time and resources — plus bugs can be harder to find or isolate in later stages.
Here is an overview of the four levels of software testing, along with who is responsible for each:
Unit testing — verifying that your code is working at its most basic level, the unit
Integration testing — confirming that your units are working together to produce the expected output
System testing — ensuring that your code can run in the environment where it will be deployed
Acceptance testing — validating that the software you have built truly addresses users' needs and solves their problems
What are the benefits of unit testing?
"Working software over comprehensive documentation" is one of the four core values of the Agile Manifesto. Ensuring code correctness and resilience to change is vital to delivering functioning software that users can rely on to accomplish their goals. Unit testing helps you identify problems as you write code and verify that your code still works correctly as you refactor or add new functionality. In a way, unit testing is its own form of documentation — different unit tests enumerate the different ways that a function could fail or behave unexpectedly. This gives developers more information to help improve the code.
Here is a brief summary of the key benefits of unit testing:
Correctness — ensure your code behaves as you expect it to so you can produce reliable and high-quality code and minimize bugs
Safety — minimize risk so you can refactor your code without worrying about introducing regressions (when code that once worked no longer behaves as expected)
Efficiency — discover defects more quickly so you can fix them before problems compound and reduce the cost of change
Clarity — force you to think clearly about the assumptions your code is making
The process of devising unit tests forces you to think about functionality in terms of what the user's goals are. Safety and speed are also important benefits of unit testing. The later a bug is caught during the development process, the harder and more costly it can be to fix it. Unit testing enables you to make changes to the code without worrying that you will be breaking other sections of the code. This is commonly called "fearless refactoring." When bugs are caught early in the development process (before reaching QA or production), developers can make changes quickly to fix the issue — saving everyone time and money.
It is worth acknowledging that doing unit testing does not guarantee your code will be completely bug-free. Unit tests simply cannot identify all errors in the application — you are merely checking predictable edge cases. And while unit testing validates how the units themselves function, it will not uncover larger integration, system, or acceptance errors. Unit testing also requires a significant investment of time — you must run tests frequently and have the knowledge to write good unit tests across different types of code.
Who is responsible for unit testing?
The short answer: developers. While waterfall teams certainly benefit from doing unit testing, it is most commonly associated with agile methodologies.
Particularly, unit testing is the main principle behind test-driven development (TDD), a core agile practice. Developers who follow TDD write unit tests first, as promises for their future functions to fulfill. Then they write just enough code that will enable their unit tests to pass. Performing robust unit testing first (before writing the function itself) forces you to think about your inputs and outputs — and what your function is promising to do.
Two more agile methodologies are behavior-driven development (BDD) and extreme programming (XP). BDD is an extension of TDD that combines the test-first process with a focus on specifying software behavior (instead of just inputs and outputs). And TDD is one of the key practices of XP, an agile methodology that encourages pair programming, transparency, and collaboration. Developers following XP use an automated unit testing framework to accelerate their development progress.
No matter what methodology your team follows, every developer should write and run unit tests for their code.
What makes a good unit test?
Automated | Unit testing should be quick — done frequently (every few lines of code) and fast (30 seconds or less). Tests should also be run quickly with one command. If you are working in a continuous integration/continuous delivery (CI/CD) environment, schedule unit tests to run regularly and automatically reject any code that has a failing unit test. |
Consistent | Your unit tests should be deterministic — the same input will always produce the same output. In addition to writing assertions against the output, you will need to address any side effects of the code under test (using a mocking framework or golden data set to test against). Name your unit tests in a consistent way to ensure that your framework can properly run the tests. |
Isolated | Each unit test should be independent. Any changes to shared state must be cleaned up at the end of each test. Be mindful about set up and tear down methods so you are not unintentionally leaking state between tests. If your code depends on external side effects (such as making an API request), use a mock framework. This way, your code does not actually need to interact with the external resource. |
Understandable | Your unit tests should be short, clear, and readable. Write code that produces results that are immediately obvious and do not require you to do in-depth research to verify that the behavior is correct. Having understandable unit tests also serves as additional documentation for your code and its expected behavior — anyone looking at your unit tests will grasp what the code is trying to accomplish. |
Common unit testing frameworks
Unit testing frameworks typically refer to third-party tools (called libraries, frameworks, or packages) that make it easy for development teams to unit test their code. Examples of common programming languages that do not have built-in unit testing capabilities but have external testing libraries include C++, Java, and JavaScript.
The main point is that you have a variety of unit testing software options to choose from, depending on the needs of your team and the programming language you are writing in. Here are a few common open-source unit testing frameworks:
JUnit — a popular Java unit testing framework, based on creating test classes. JUnit has inspired many other "XUnit" libraries in various programming languages (such as NUnit and PHPUnit)
PyTest — originally created for the Python programming language. PyTest is based around functions as opposed to test classes
Boost Test — a C++ library included with the common Boost packages
RSpec — a Ruby framework based on the principles of BDD. In RSpec, each test case is a specification for the expected behavior of the code.
It is also worth noting that some programming languages allow you to unit test directly in the base language, without needing to rely on another library. For example, Python, Ruby, and Rust are popular languages that come with unit testing capabilities.
Aha! Develop gives development teams a way to visualize and customize workflows. Choose from a variety of extensions to track the status of your code and gain visibility into the team's overall progress.
You can also use extensions to integrate with external tooling for CI/CD. For example, if you use a tool like CircleCI to automatically run your suite of unit tests, you can install the CircleCI extension to track whether there were test failures.