Unit testing is one of the primary types of software testing. As is generally the case with software testing, unit testing can’t guarantee that no bugs will occur after your application is deployed. However, by testing the smallest repeatable pieces of code in your applications, unit testing helps catch bugs in the building blocks of your project before they affect your integrated application.
As an essential software development process, unit testing is a valuable skill to know as a developer. If we’re able to follow unit testing best practices, we’d already be unit testing during the development phase. However, this isn’t realistic for all software development projects. Therefore, unit testing can happen after your production code is written, and is essential to ensure that extended and refactored code remains functional.
Today, we’ll cover an overview on unit testing and unit testing best practices.
We’ll cover:
- An overview of unit testing
- What is unit testing?
- Benefits of unit testing
- Test planning
- Test coverage
- Unit testing frameworks
- 11 unit testing best practices
- Wrapping up and next steps
An overview of unit testing
What is unit testing?
Unit testing ensures that the units within your program are working as expected. Since the person who wrote a piece of code would have the greatest insight as to its expected behavior, it’s usually the developer’s responsibility to unit test. In combination with end-to-end tests and integration tests, unit testing helps ensure code quality early on in the development process.
A unit is the smallest piece of code in your program that is repeatable, testable, and functional. Units can be functions, classes, methods, and so on.
The previous figure depicts unit testing alongside other software testing levels, wherein each level has its own scope:
- Unit testing tests each software module.
- Integration testing tests the combined modules according to system design documents and functional requirements.
- Functional testing tests a program’s desired functionality based on requirement analysis documents and the user manual (i.e. ensuring the expected output for a given input). This is sometimes divided into system testing and acceptance testing.
Black box and white box testing are two approaches to software tests, wherein:
- Black box testing tests software behavior without knowing the implementation details of a software module. These tests are derived from the software specification document.
- White box testing tests the actual implementation of a software module., wherein the inner architecture of the program is known to the tester. These tests give us implementation insights to develop more comprehensive tests.
For unit testing, white box testing is usually the most suitable option, particularly when our modules are smaller and their code is easier to comprehend. On the other hand, black box testing is a good option for later stages of a project, when modules have been integrated to create a complex software.
Benefits of unit testing
Some benefits of unit testing include:
- Simplifies debugging: By testing the functionality of small units, you can catch bugs before they affect more units in the integrated application.
- Encourages more loosely coupled code: By intentionally reducing interdependencies between units, we get more loosely coupled code, a coding best practice.
- Faster than functional testing: As units are very small, you can run several unit tests in seconds (provided you’ve automated them).
- Minimize code regression: After code is refactored or extended, you can rerun all test suites to ensure your new or updated code doesn’t break existing functionality.
- Better code coverage: By breaking an application down into the smallest testable components, unit testing helps increase your code coverage.
Test planning
A test plan dictates a unit test’s complexity and design. Because we have limited time and budget for any development project, it’s not always possible to exhaustively unit test a project. Taking this into consideration, a test plan will determine which resources will be allocated to unit testing. A test plan is one of the key parts of the software development lifecycle.
Test coverage
Test coverage is a metric to determine how thoroughly we've unit tested our software. As a general rule, we want to maximize test coverage as much as possible.
Test coverage can be further classified as follows:
- Function coverage: The percentage of functions which have been tested
- Statement coverage: The percentage of statements that have been tested. Sometimes, it includes all the statements that have been executed at least once during the testing.
- Path coverage: The percentage of paths that have been tested. A software may have multiple paths (or branches) taken by it based on conditions, such as, user’s input, environment sensing, and other events.
Unit testing frameworks
A unit testing framework is software that enables us to quickly code unit tests and automate their execution. In the event that a test fails, frameworks can save the results or throw an assertion.
There are dozens of unit testing frameworks available for various programming languages. Some popular unit testing frameworks include Cunit, Moq, Cucumber, Selenium, Embunit, Sass True, HtmlUnit, and JUnit (one of the most widely used open source frameworks for the Java programming language).
We can save a great amount of time and resources with unit testing frameworks, which include the following features:
- Test suite: A set of test cases that are grouped for test execution purposes, which allows us to aggregate multiple similar unit tests.
- Test runner: A component which automates execution of a series of test cases and returns the test result to the user.
- Test fixture: A predefined state or fixed environment in which we run tests, ensuring that our tests are repeatable across multiple test runs.
11 unit testing best practices
1. Write tests for a number of scenarios
When writing a test case, be sure that you’re considering all possible scenarios. In other words, don’t just write a test for the happy path. Think about other scenarios as well, such as error handling.
2. Write good test names
A good unit test name should explicitly reflect the intent of the test case. Follow consistent naming conventions, and only use shorthand if they’re easily understood by a reader. Writing good test names supports code readability, which will make it easier for yourself and others to extend that code into the future.
3. Set up automated tests
Opt for automated unit testing with the help of a unit testing framework. An even better practice is automating tests in your continuous integration (CI/CD) pipeline.
The alternative to automated testing is manual testing, wherein we manually execute test cases and gather their results. As you can imagine, manually testing small units is incredibly tedious. It’s also less reliable. Automated tests are surely the way to go here.
4. Write deterministic tests
False positives and negatives are common in software testing, and we must be diligent in order to minimize them. The goal is to have consistent outputs for tests in order to verify the desired function. Unit tests should therefore be deterministic. In other words, as long as the test code isn’t changed, a deterministic test should have consistent behavior every time the test is run.
5. Arrange, Act, and Assert (AAA)
The AAA protocol is a recommended approach for structuring unit tests. As a unit testing best practice, it improves your test's readability by giving it a logical flow. AAA is sometimes referred to as the “Given/When/Then” protocol.
You can use the AAA protocol to structure your unit tests with the following steps:
- Arrange: Arrange the setup and initialization for the test
- Act: Act on the unit for the given test
- Assert: Assert or verify the outcome
The following code demonstrates the AAA structure in testing the absolute value function in Python:
def test_abs_for_a_negative_number():
# Arrange
negative = -2
# Act
answer = abs(negative)
# Assert
assert answer == 2
6. Write tests before or during development
Test-driven development (TDD) is a software development process through which we enhance our test cases and software code in parallel. In contrast to a typical development methodology, TDD involves writing test code before production code. TDD has several advantages, including increasing the code coverage of unit tests.
The process of TDD looks as follows:
- Develop a unit test for a desired feature
- Write the shortest possible code to pass that newly created test
- When the test passes, refactor your code to improve maintainability and readability
- If another feature is needed, repeat this process
The following figure illustrates the TDD development process:
7. One use case per unit test
Each test should focus on a single use case, and verify the output is as expected for that tested method. By focusing on one use case, you’ll have a clearer line of sight into the root problem in the event that a test fails (as opposed to testing for multiple use cases).
8. Avoid logic in tests
To reduce the chance of bugs, your test code should have little to no logical conditions or manual string concatenations.
9. Reduce test dependencies
Tests should not be dependent on each other. By reducing dependencies between units, test runners can simultaneously run tests on different pieces of code. A unit can be considered testable only if its dependencies are staged (i.e. stubs) within the test code. No real-world or external dependencies should affect the outcome of the test.
10. Aim for maximum test coverage
While we can aim for 100% test coverage, this might not be always desirable or possible. Such comprehensive testing may have budget and time requirements beyond our ability. In some cases, such comprehensive testing is theoretically impossible (i.e. undecidable). That being said, we should aim for the most possible coverage given our constraints.
11. Keep proper test documentation
Maintaining test documentation will help both developers and, in some cases, the end users (e.g. in the case of an API).
Testing documentation should fulfill the following criteria:
- Reviewable: A test by any given resource is reviewable by others.
- Repeatable: A test is documented such that it can be repeated multiple times. This enables us to verify that a bug is fixed in an updated piece of code by repeating the same test.
- Archivable: Tests and related bugs can be archived in documentation, serving as a valuable resource for future extensions of the project.
Wrapping up and next steps
Wherever you are in your coding journey, learning to unit test will ensure that you can check your balances in the applications that you develop. In some companies, unit testing is taken care of by quality assurance (QA) engineers, but for the rest, software developers are responsible for mastering this craft. By validating your code as you write it, unit testing is invaluable in ensuring your development project comes together with minimal bugs. If it seems like a tedious task to test your application on a small scale of units, remember that you have various testing frameworks at your disposal to streamline the process!
One of the most popular unit testing frameworks is JUnit. If you work with Java, you might consider diving into unit testing with our course, Pragmatic Unit Testing in Java 8 with JUnit 5. JUnit is one of the most popular unit testing frameworks. You can use JUnit to write test methods with special directives, automate test executions with test runners, indicate test failures with different assertions, and record your test results. This course includes tutorials and all the information you need to leverage JUnit to efficiently test and develop your Java applications.
Happy learning!
Continue learning about software testing on Educative
- Software testing 101: Get started with software testing types
- Performance testing tutorial: Automation, Gatling, and Jenkins
- Sass tutorial: Unit testing with Sass True
Start a discussion
How do you do unit testing? Was this article helpful? Let us know in the comments below!