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:
Try one of our 300+ courses and learning paths: Pragmatic Unit Testing in Java 8 with JUnit 5.
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:
Black box and white box testing are two approaches to software tests, wherein:
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.
Some benefits of unit testing include:
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 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:
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:
Try one of our 300+ courses and learning paths: Pragmatic Unit Testing in Java 8 with JUnit 5.
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.
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.
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.
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.
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:
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
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:
The following figure illustrates the TDD development process:
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).
To reduce the chance of bugs, your test code should have little to no logical conditions or manual string concatenations.
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.
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.
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:
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!
Free Resources