The Role of Unit Testing in Modern Software Development

by Dmitry Kirsanov 29. November 2023 04:00

As we glide forward on the cutting edge of the technological era, software development practices mature and evolve. Among the myriad of practices that mark the life-cycle of software development, unit testing forms a cornerstone. This indispensable process, intertwined with the act of writing code itself, has been fundamental in shaping this industry.

Reflecting on the incipient stages of my career, at the dawn of the digital age, I recall a time when the concept of unit testing was nascent, untapped, and devoid of the supportive tools or libraries that we now take for granted. The process, though cogent in theory, was far from being systematized. Diligent developers would carefully write small code snippets, run them to make sure they functioned as desired, and repeat this process incrementally. It was a time-consuming and monotonous task, demanding exactness, but it was critical for assuring the essential functionality of the code.

The Arrival of Unit Testing Libraries

Fast forward to the digital landscape of today, and the vision is altogether different and far more dynamic. Today, we have access to a plethora of mature, feature-packed unit testing libraries. These tools empower developers to swiftly build test harnesses and progressively test their code with a simple push of a button. The crux of modern unit testing is the ability to test the smallest changes to code in a matter of milliseconds. This instantaneous feedback mechanism is an effective sentinel, keeping a constant check by validating established expectations or detecting new issues.

The evolution of unit testing tools has been nothing short of revolutionary—an epoch-making occurrence, if you will, that has indelibly transformed the face of the software development industry. It would be no exaggeration to state that the advent of these tools signals one of the most significant advancements in tech history. In fact, I'd go as far as to say that I'd rather regress to the traditional method of manually encoding instructions into a computer—through the arduous task of punching holes in cards—than forfeit the luxury and simplicity offered by unit testing libraries.

Not Just for Developers

It could be easy to fall into the trap of viewing unit testing as an exclusive realm of developers. However, this practice extends beyond the confines of the developer's world and into the realm of professional testers. It's worth noting that any endeavour towards testing, unit testing included, feeds into the broader mission of refining the product quality. Thus, the effectiveness of a team's unit testing strategy is an area focused upon by the entire team, including testers.

Adept testers, with a keen understanding of which parts of the system are adequately covered by unit tests and which parts aren't, can identify and prioritize areas where additional testing activities may be needed. Unit tests can also serve the dual purpose of documentation and offer valuable insights into how a system is implemented—a resource that a tester can leverage. Additionally, unit test libraries are often utilized for writing and running various types of test automation.

Demystifying Unit Testing with an Illustration

To shed more light on the topic, let's explore a unit test for a portion of a medication prescribing application. This application, written in Java, also uses Java for its unit testing—this use of the same language for coding and testing is a common practice.

Each method within this unit test is a self-contained test. They all collectively aim to validate that a code segment, called the duration parser, accurately converts a string describing a certain number of days into an equivalent integer.

If I were to make any changes to the method in question, I could easily rerun these test cases to ascertain if anything has been disrupted. If so, I would receive instant feedback and could either revert the change or attempt to remedy it.

Now, say I required a new feature. I could create a new test case that encapsulates and tests the feature before it's been implemented. This practice, dubbed "test-driven development", entails cycling between writing tests and writing code, with the tests providing guidance at each step. This approach serves as a roadmap for developers throughout an entire feature.

Unit tests can sometimes be quite granular, which can present a challenge when trying to discern the business functionality being tested. However, our example does give clear hints about the business functionality—the code we're testing is adeptly converting a text string portion into a prescription duration represented in integer days.

Unit Tests in Ideal World

I would say, that unit tests serve as a form of living documentation for the code they test. They are an essential cog in the software development wheel, contributing to the overall quality of the product. They offer valuable insights for both developers and testers, showing us what individual codes are designed to do and highlighting how well they're performing.

Unit tests are designed to check a specific scope, ranging from a minuscule code segment to a whole subsystem. However, "true unit testing", the ideal form that we strive to achieve, typically zeroes in on a relatively small snippet of code. There are times when a unit test that works a larger section of an application may be labelled a ‘component test’.

I’d like to emphasize an important point - unit test tools can serve a much broader spectrum than just unit testing. Tools providing unit testing capabilities could be utilized to run an end-to-end test interacting with the user interface of a deployed system. Despite being outside the strict definition of a unit test, the unit test library can be of immense help in writing and executing these tests.

Do You Actually Need Unit Tests?

Well, my friend, that is a big question, deserving the most common answer in software development world – “it depends!”. Which also means – not always. Unit tests are not an axiom, and you should apply common sense and avoid implementing some paradigm for the sake of “doing it by the book”.

You may not be fond of “test-driven development” approach (which, let’s be honest, doesn’t work in most places), and argue that there is no point in having unit tests in functions which are tightly integrated with external components – that’s what we have integration tests for. But there is one thing, in context of modern CI/CD, which make unit tests valuable – tests, that involve compatibility with 3rd party libraries.

For example – let’s imagine, that you have a JSON serializing library, and some functions depend on it. What would happen, if that library would stop working for you? Your unit test of the function that uses that library would fail, that’s what.

However, unit tests are executed during the CI phase, not CD. Meaning – when your project is built, not when it’s deployed, and often we’d like to test, if our application works, after it’s deployed in test environment. Said library could be missing, or be incompatible with some system settings, and we want to be able to detect such failure before our testers start wasting their time.

And that’s where integration test project comes into play, and we often may find, that functions in unit tests would be duplicated in integration test. And that’s where “depends” comes from – you certainly need either unit or integration test, but the question is – whether you need both.

My suggestion is – if your budget allows, have both. Even if unit test will cover just 1% of your code, it may save you time by failing earlier than that code is deployed to test environment. However, in many cases you can resort to integration tests with higher degree of efficiency and satisfaction.

A Developer's Best Friend

Unit tests are primarily designed to assist in the process of development. Their significance is heightened during regression testing, where they help to pinpoint bugs that may have surreptitiously crept into pre-existing features. However, this is more the exception than the rule. The reality is that a unit test failure usually implies the developer is working on that piece of the puzzle and expects it to fail—it isn't often that a unit test selflessly reveals an unsuspected regression bug. Therefore, the scope and depth of unit tests typically mean they are an underling in the bigger scheme of regression testing.

The primary interface for true unit tests is the system's code. They uniquely interact with the system code sans the need for it to be deployed. The beauty of true unit tests lies in their cohabitation with the actual code in the system's source code repository. The language of the unit test library typically aligns with the code being targeted for testing—a Java application would see Java unit tests, a C# application would see C# unit tests, and so on.

Unit testing is synonymous with custom-developed systems; it forms an integral part of the ecosystem. While it isn't necessary to create a unit test for each line of source code, any bespoke system should have comprehensive coverage. The crucial segments of the system—those high-impact areas—are the best suited for intensive unit testing.

The reverse side of this medal is that writing unit tests consumes time, which consumes project budget, which could be used for developing actual features. However, they simplify the coding process significantly, when the stars align. That is – when the function is large and complex, and / or when developer is slow at that particular task. Maintaining unit tests does come with a price tag attached. Whenever significant changes are made to the production code of a system, such as redesigns or major movements, the unit tests must be updated to fit the new context.

As we progress, we'll be delving into a particular kind of test that, although typically written using a unit test library, fundamentally differs from a unit test: user interface or end-to-end tests. So stay tuned for the next chapter of this unfolding saga of software testing!



blog comments powered by Disqus