What is Unit Testing? A Complete Guide
What Is Unit Testing?
Unit testing is a software development practice that involves testing individual units or components of a software application in isolation. A unit is the smallest testable part of an application, usually a single function, method, procedure, module, or class.
Together these code units form a complete application, and if they don’t work well individually, they definitely won’t work well together. Unit testing ensures that each component of the software works correctly on its own before integrating it into the larger system.
When Should Unit Testing Be Performed?
Unit testing is usually the very first level of testing, done before integration testing. The number of tests to perform in each cycle is huge, but the time it takes for each test is insignificant as these code units are relatively simple. Because of this, developers can quickly perform unit testing themselves.
Who Performs Unit Testing?
In certain teams, developers don’t want to allocate their limited bandwidth to do unit testing so that they can focus entirely on development. In these cases, QA engineers will take over unit testing and integrate this activity into their test plan, leveraging the existing testing tools to better execute and manage test results.
What Is the Purpose of Unit Testing?
Unit testing is crucial to the software testing process for several reasons:
- Early bug detection: Unit testing catches bugs in early stages of development, ensuring that no bug is left undetected for too long and dependencies among software components become more complex. One single bug in an individual piece of code can easily affect many parts of the entire system without unit testing. Fixing these entrenched bugs is incredibly expensive and time-consuming.
- Better code writing: If developers have to take on unit testing, they must also adopt coding best practices and ensure that the code they write is maintainable. This is because unit testing requires that each unit has a well-defined responsibility that can be tested in isolation.
- Simplifies debugging: When a unit test fails, it provides valuable information about the specific unit and location where the problem occurred. This narrows down the root cause of the issue, making debugging faster and more efficient.
- Provides documentation: Unit tests act as examples that demonstrate how individual units of code should be used and what behavior is expected from them. They provide the perfect documentation for the entire logic of the software. This is especially useful for knowledge transferring to new members and regression prevention.
Anatomy of a Unit Test
1. Test Fixtures
Test fixtures are the components of a unit test responsible for preparing the necessary environment to execute the test case. Also called the “test context,” they create the initial states for the unit under test to ensure a more controlled execution. Test fixture is highly important for automated unit tests because it provides a consistent environment to repeat the testing process.
For example, let’s say we have a blogging application and we want to test the Post Creation module. The test fixtures should include:
- Post database connection
- Sample post with titles, content, author information, etc.
- Temporary storage for handling post attachments
- Test configuration settings (default post visibility, formatting options, etc.)
- Test user account
- Sandbox environment (to isolate the test from the production environment and prevent tampering with actual blog data)
2. Test Case
A unit test case is simply a piece of code designed to verify the behavior of another unit of code, ensuring that the unit under test performs as expected and produces the desired results. Developers must also have an assertion to specifically define what those desired results are. For example, here is a unit test case for a function that calculates the sum of two numbers, a and b:
class MathTest extends TestCase
public function testSum()
$a = 5;
$b = 7;
$expectedResult = 12;
$result = Math::sum($a, $b);
The assertion used in this code is $this->assertEquals($expectedResult, $result); verifying that a + b indeed equals the expected result of 12.
3. Test Runner
The test runner is a framework to orchestrate the execution of multiple unit tests and also provide reporting and analysis of test results. It can scan the codebase or directories to file test cases and then execute them. The great thing is that test runners can run tests by priority while also managing the test environment and handling setup/teardown operations. With a test runner, the unit under test can be isolated from external dependencies.
4. Test Data
Test data should be chosen carefully to cover as many scenarios of that unit as possible, ensuring high test coverage. Generally, it is expected to prepare data for:
- Normal cases: typical and expected input values for that unit
- Boundary cases: input values at the boundary of the acceptable limit
- Invalid/Error cases: invalid input values to see how the unit responds to errors (by error messages or certain behavior)
- Corner cases: input values representing extreme scenarios that have significant impact on the unit or system
5. Mocking and Stubbing
Mocking and stubbing are essentially substitutes for real dependencies of the unit under test. In unit testing developers must focus on testing the specific unit in isolation, but in certain scenarios they’ll need two units to perform the test.
For example, we can have a User class that depends on an external EmailSender class to send email notifications. The User class has a method sendWelcomeEmail() which calls the EmailSender to send a welcome email to a newly registered user. To test the sendWelcomeEmail() method in isolation without actually sending emails, we can create a mock object of the EmailSender class. The developer then won’t have to worry if the external unit (the EmailSender) is working well or not. The unit under test is truly tested in isolation.
Characteristics of a Good Unit Test
Unit tests are generally:
- Fast: These tests only check very simple and limited-in-scope units, so they can be executed in milliseconds. A mature project can have up to thousands of unit tests.
- Isolated: They should be executed in isolation from external dependencies to ensure the most accurate results.
- Easily automated: Due to their simple nature, unit tests are perfect candidates for automated testing. Developers can employ leading testing tools to help them run unit tests better.
How To Do Unit Testing?
- Identify the unit: Determine the specific code unit to be tested: either a function, method, class, or any other isolated component. Read the code and brainstorm the logic needed to test it. In this step developers should also have an idea of the cases they need to test for that unit to ensure high test coverage.
- Choose the approach: Similar to many other testing types, there are two major approaches to unit testing:
- Prepare the test environment: Set up the mock objects, prepare test data, configure the dependencies, as well as any other required preconditions. A confident developer would isolate the function for a more rigorous testing process. This practice involves copying and pasting the code into a dedicated testing environment, separate from its original context. By isolating the code, unnecessary dependencies between the code being tested and other units or data spaces in the product are uncovered.
- Write and execute test case: If the developer chooses the automated approach, they’ll start writing the test case, usually with a Unit Test Framework. This framework (or a test runner) can be used to execute the test and produce results (whether it passed or failed).
- Debug, fix, and confirm: If a test case fails, developers must debug it to identify the root cause, fix the issues, then rerun the tests to confirm that the bugs have indeed been fixed.
Unit Testing Techniques
There are several unit testing techniques commonly used to ensure thorough test coverage, including:
- Black box testing: The internal structure and implementation details of the unit under test are not considered (similar to how the internals of a black box is not known). The tests focus on the external behavior and functionality of the unit. Test cases are designed based on the expected inputs, outputs, and specifications of the unit.
- White box testing: The internal structure, logic, and implementation of the unit is taken into account, which contrasts with black box testing. Test cases are designed to explore different paths within the unit, ensuring that all code branches and segments are tested.
Top 4 Unit Testing Tools
JUnit is an open-source unit testing tool in Java. It does not require the creation of class objects or the definition of the main method to run tests. It has an assertion library for evaluating test results. Annotations in JUnit are used to execute test methods. JUnit is commonly used to run automation suites with multiple test cases.
- Supports test-driven development.
- Integrates with Maven and Gradle.
- Executes tests in groups.
- Compatible with popular IDEs like NetBeans, Eclipse, IntelliJ, etc.
- Fixture feature provides an environment for running repeated tests.
- Using the @RunWith and @Suite annotations, we can run unit test cases as test suites.
- Provides test runners for executing test cases.
NUnit, an open-source unit testing framework based on .NET, inherits many of its features directly from JUnit. Like JUnit, NUnit offers robust support for Test-Driven Development (TDD) and shares similar functionalities. NUnit enables the execution of automated tests in batches through its console runner.
- A console runner provided by NUnit enables batch test execution.
- NUnit facilitates parallel test execution.
- Multiple assemblies are supported.
- Various attributes allow tests to be run with different parameters.
- Extensive support for Assertions is available.
- Data-driven testing is supported.
- Microsoft family languages such as .NET Core and Xamarin forms are supported.
TestNG, short for Test Next Generation, is a robust framework that offers comprehensive control over the testing and execution of unit test cases. It incorporates features from both JUnit and NUnit, providing support for various test categories such as unit, functional, and integration testing. TestNG stands out as one of the most powerful unit testing tools due to its user-friendly functionalities.
- Ability to execute test cases in parallel.
- Built-in exception handling mechanism.
- Support for testing integrated classes.
- Generation of HTML reports and logs.
- Capability to retrieve keywords/data from logs.
- Support for multi-threaded execution.
- Complete object-oriented nature with convenient annotations.
- XML-based configuration for all test settings.
PHPUnit is a programmer-oriented unit testing framework specifically designed for PHP. It adheres to the xUnit architecture commonly utilized by unit testing frameworks such as NUnit and JUnit. PHPUnit operates exclusively through command-line execution and does not have direct compatibility with web browsers.
- Comprehensive code coverage analysis and the ability to simulate mock objects.
- Facilitation of test-driven development practices.
- Integration with the xUnit library to enable logging functionalities.
- Support for object mocking.
- Introduction of new assertions such as assertXMLFileTag() and assertXMLNotTag().
- Incorporation of error handler support in the existing version.
- Flexibility in extending test cases according to the programmer's specific requirements.
- Generation of multiple test reports.
Test-Driven Development and Unit Testing
Test-Driven Development (TDD) and unit testing are two connected practices. The process of TDD involves writing automated unit tests prior to writing the code. These tests will surely fail, since there is no code written yet. After that, they will use the results from these tests to guide their code writing. Once they have developed the feature, they’ll re-execute the previously failed tests to confirm that their code indeed delivers the intended functionality.
TDD is a systematic development approach that consistently offers feedback, facilitating quick bug detection and debugging. Imagine a situation where many frustrated users complain about a major problem that makes the app extremely slow. In an effort to fix this issue, your team quickly releases a patch. Unfortunately, this rushed solution introduces an even bigger problem, resulting in a widespread system failure.
With Test-Driven Development, you can effectively prevent such incidents. Generally, developers that follow the TDD approach will go through a three-step process:
- Fail: Write unit tests that will surely fail because no code is written.
- Pass: Write code until those tests pass.
- Refactor: Improve the code, then continue to run unit tests for the next features.
Read More: TDD vs. BDD: A Comparison
Unit Testing Best Practices
- Unit tests should be fast: Usually unit tests are huge in quantity, and if they require a lot of time to execute, developers will be hesitant in taking on this task. The goal of having unit tests is to boost the developers’ confidence in the existing code so that they can proceed with the next features, so they should be short and straight to the point.
- Unit tests should be simple: Each unit test should focus on verifying a specific behavior or functionality (follow the “one assertion per test” rule). Structure your test on the AAA pattern to maintain clarity and readability in your unit tests. Choose descriptive, meaningful, but simple names for your test methods so that you have an easier time managing thousands of them.
- Unit tests should be executed in isolation: Code isolation is a highly recommended practice to eliminate any external influences. Test input data should also be controlled, so try to avoid using dynamically generated data that may influence test results. Also ensure that you have reset the state for each unit test run, so there can be no interference with previous tests.
- Test results should be highly consistent: The more deterministic your unit tests are, the better. In other words, their results should always be consistent, no matter what changes were made to the code or what order you run your tests in.
- Regularly refactor unit tests: Treat your unit test code with the same care and attention as your production code. Refactor tests when necessary to improve readability, maintainability, and adherence to best practices.
- Continuous integration and test automation: Incorporate unit tests into your continuous integration (CI) pipeline and automate their execution. This ensures that tests are run regularly, providing timely feedback on the health of your codebase.
Challenges of Unit Testing
Unit testing comes with a host of challenges for developers:
- Managing thousands of unit tests without a dedicated test management tool is a resource-intensive task.
- Writing test scripts and maintaining them across code updates is also time-consuming.
- Setting up test environment for a wide variety of tests requires effort.
- Unit testing activities need to be seamlessly integrated into the development workflow.
What Makes Katalon Platform Ideal for Unit Testing?
Katalon Platform is a modern, AI-augmented test automation and quality management platform for web, mobile, API, and desktop applications. It provides a unified platform for teams to plan, design, execute, and manage automated testing efforts. For unit testing in particular, Katalon Platform comes with exciting features:
- Low-code/Full-code test creation: Building test cases, which would typically take hours, can be completed in minutes using a keyword library and test recorder that allows for easy drag and drop. For advanced users, there is also the option to switch to scripting in Java and Groovy to create your own custom keywords that can be used across any test cases.
- Data-driven testing supported: Test data can be generated from different sources like Excel, CSV, or databases and effortlessly incorporated into your test scripts. Katalon provides support for capturing data snapshots to verify changes and binding Global Variables to manage test scripts at different development stages.
- Test artifact management: A built-in centralized object repository stores and provides access to all UI elements, objects, and locators you need for your tests. With just a few clicks, you can easily update these locators and properties across test cases when there are UI changes.
- Compatibility testing supported: Test suites, including end-to-end and regression tests, can be simultaneously executed on local and cloud browsers, devices, and operating systems. Katalon's integrated functionality allows for enhanced test coverage and reduces the need for excessive workarounds. You have the ability to seamlessly connect with tools like CircleCI, Jenkins, and GitLab to conveniently schedule or automatically trigger test cases within the CI/CD pipeline and containers.