Software is one of the most complex tools invented for practical use. One misplaced character can break an entire application. So, careful testing is an essential requirement before publishing any code. In this article, you will learn about two fundamental types of software testing, unit testing and integration testing, and how your team can implement them in your CI/CD pipelines to validate your code quickly and deliver new features to your users with confidence.
DevOps teams and developers have introduced several approaches to software testing over the years. Most of these methods have found their way into one variation of the testing pyramid or another. In most cases, unit testing and integration testing make up the pyramid’s two base layers, emphasizing their importance in a comprehensive testing strategy:
While this forms a nice diagram, in reality there is no crisp boundary between unit tests, integration tests, and many other types of tests. Different test categories are not exclusive but rather complementary. So while developers may debate the relative importance of unit tests and integration tests, it is in your team’s best interest to find the ideal place to use both in your continuous integration pipelines. Before you can do that, though, you need to understand the basics of both types of testing.
What is unit testing?
Unit tests focus on a single part of a whole application in total isolation, usually, a single class or function. Ideally, the tested component is free of side effects so it is as easy to isolate and test as possible.
Realistically, DevOps cannot always reach this level of isolation. That is when testing starts to become more challenging.
Other factors can limit unit testing’s capabilities. For instance, in programming languages with access modifiers such as private or public, you cannot test the private functions. Special compiler instructions or flags will sometimes help get around these restrictions. Otherwise, you need to apply code changes to make these restricted helpers accessible for unit testing.
A key factor that makes unit testing a good choice is its execution speed. Since these tests should be side-effect-free, you will want to run them directly without any other system’s involvement. Ideally, this includes no dependencies on the underlying operating system, such as file system access or network capabilities. In practice, some dependencies may exist. Other dependencies can be swapped out to allow for testing in isolation. This process is called mocking.
Unit testing is also the heart of an advanced software development process called test-driven development. In the test-driven dev process, DevOps professionals and developers write tests before the actual implementation. The goal is to have the specification of a single unit roll out before its realization.
While the enforcement of such a contract can be appealing, there are notable downsides. The specification needs to be exact, and the test writers need to know at least part of the implementation from a conceptual point of view. This requirement contradicts some Agile principles.
Now that we have explored unit testing in detail, we can learn how integration testing differs.
What is integration testing?
We have learned that, in practice, the isolation property of unit tests may not be sufficient for some functions. In this case, one solution is to test how parts of the application work together as a whole. This approach is called integration testing.
Unlike unit testing, integration testing considers side effects from the beginning. These side effects may even be desirable.
For example, an integration test could use the connection to a database (a dependency in unit testing) to query and mutate the database as it usually would. You would need to prepare the database and read it out afterward correctly. DevOps often “mocks away” these external resources the way mocking is used in unit tests. This results in obscuring the failures caused by APIs beyond their control.
Integration testing helps find issues that are not obvious by examining the application’s or a specific unit’s implementation. Integration testing discovers defects in the interplay of several application parts. Sometimes, these defects can be challenging to track or reproduce.
While the lines between the various test categories are blurry, the key property of an integration test is that it deals with multiple parts of your application. While unit tests always take results from a single unit, such as a function call, integration tests may aggregate results from various parts and sources.
In an integration test, there is no need to mock away parts of the application. You can replace external systems, but the application works in an integrated way. This approach can be useful for verification in a CI/CD pipeline.
Unit testing and integration testing in CI/CD
Tests need to run to be effective. One of the great advantages of automated tests is that they can run unattended. Automating tests in CI/CD pipelines is considered a best practice, if not mandatory according to most DevOps principles.
There are multiple stages when the system can and should trigger tests. First, tests should run when someone pushes code to one of the main branches. This situation may be part of a pull request. In any case, you need to protect the actual merging of code into main branches to make sure that all tests pass before code is merged.
Set up CD tooling so code changes deploy only when all tests have passed. This setup can apply to any environment or just to the production environment. This failsafe is crucial to avoid shipping quick fixes for issues without properly checking for side effects. While the additional check may slow you down a bit, it is usually worth the extra time.
You may also want to run tests periodically against resources in production, or some other environment. This practice lets you know that everything is still up and running. Service monitoring is even more important to guard your production environment against unwanted disruptions.
Because your CI/CD pipelines should be fast, it makes sense to have most of the tests running as quickly as possible. Often, the fastest option is to use many unit tests, but the overall key metrics are coverage and relevance.
Development teams must create an efficient, reliable test setup for their projects, one that covers all relevant code paths. Make automatically running these tests in your CI/CD pipeline a high priority for your team. A combination of testing methods enhances test coverage and makes your software as bug-free as it can be.
Unit testing and integration testing are both important parts of successful software development. Although they serve different yet related purposes, one cannot replace the other. They complement each other nicely.
While writing unit tests is often faster, the reliability of integration tests tends to build more confidence for key stakeholders. Use both test categories to ensure that your application is working today and continues to work tomorrow.
Use your CI/CD tools to run your team’s tests automatically, triggered when something changes, at regular intervals, or on-demand. More tests mean more data, and more ways to make sure your team’s software applications remain stable in production. To see how an automated testing strategy can increase your team’s development velocity and eliminate costly and inefficient manual processes, sign up for a free CircleCI account today.