4 Properties of Highly Testable Code

This is a guest post by Erik Dietrich

Not all code is created equal. It might sound silly and self-evident when put this way, but this is a truth that many unit-testing beginners fail to grasp. You can’t just add unit tests to any piece of code. If it were that easy, there wouldn’t be entire books dedicated to the subject.

If you want to test code, then it should be testable. How do you recognize a highly testable code base? Or even better, how do you turn your code base into a highly testable state if it’s currently not? To answer these questions, let’s look at four characteristics to strive for when coding.

Get TestRail FREE for 30 days!

TRY TESTRAIL TODAY

1. Low-Coupling

Highly Testable Code, Testable Code Base, Making Code Base Highly Testable, Low Coupling, Writing Maintainable Code, Separation Between Pure and Impure Code, Logic and Presentation, Code Simplicity, Improving the Testability of Code, Code Testability, Making Code More Testable. TestRail.

Want to be a millionaire? Well, follow these two easy steps:

  1. Be a person.
  2. Get a million dollars.

We know this is just a joke, but that’s precisely the way some people talk about “low-coupling.” They say, “Want to write maintainable code? Well, keep it low-coupled.” It’s funny how they don’t seem to grasp that a) a beginner won’t have the faintest idea of what “low-coupling” even means, and b) even if they had, that wouldn’t be enough for them to write low-coupled code in practice.

It’s time to fix that.

First of all, let’s define coupling. In software development, that means how much a given software artifact (a method, a class, or even a module) depends on another. With that in mind, “low-coupling” means that each part of your code should have the smallest possible amount of knowledge about other parts of the code.

When your code is high-coupled, its maintenance becomes expensive. You edit some method signature, and that creates a chain reaction. Sooner than you realize, you’ve pretty much touched the code of the whole application.

Let’s see a quick example. Imagine you’re writing an application that contains a ProductService class:

public class ProductService
{
// fields, properties, etc

public void SaveProduct(ProductToAddDTO productDTO)
{
Product product = MapToEntity(productDTO);
productRepository.Add(product);
productRepository.SaveChanges();
}

// more methods
}

The code above is just a toy. Please indulge me here and use your imagination to fill in the relevant code that’s lacking. Yes, the code works fine, but someone has now decided it needs logging. Fair enough, you say, and you change the code to something like this:

productRepository.Add(product);
productRepository.SaveChanges();
var logger = new FileLogger(@"logsapp-demo.log");
logger.Info($"The product with Id {product.Id} was saved.");

Is the code above wrong? Well, it might work, but it’s problematic. The code above is tightly coupled to the FileLogger class. It knows too much about it. First of all, it knows that the class even exists. It’s entirely possible (and even probable) that the requirements will change again in the future—the same way they just changed.

Two months from now, someone may decide that the code should log to a database instead of a file. Or worse, to a database besides logging to the file. The code above also knows about FileLogger’s constructor requiring a path. What if, in the future, it also starts requiring a Boolean flag to indicate whether it should create the file if it doesn’t exist?

Finally, we have a problem you should avoid like the plague: mixing different levels of abstraction. The line that saves to the repository is at the business level layer. Saving an item to the persistence store is something that’s of interest to the business.

You could make the argument that some types of logging are of interest to the business as well, and I’d definitely agree with you. But then we find something nasty: that file path. Things like paths, database string connections, XML config files, and the like are all stuff that belongs to what I like to call the infrastructure level. They are of no concern to the business whatsoever.

Oh, of course, the code above is just a simple example. In a seriously sized application, you’d have potentially hundreds of lines of code referencing the FileLogger. Every time a developer makes a change to it, that could lead to possibly hours of development work.

How to Fix That?
No need to despair, though. There’s a known—and I even dare say easy—fix to that problem, and it’s called dependency injection (DI). For some reason, DI sometimes gets a bad rep in certain software development circles. In my opinion, this is undeserved and comes from misconceptions about what DI really is. And what would that be? Glad you asked.

DI consists of passing the dependencies that a class needs through its constructor, in the form of an interface. That’s it. No need for fancy DI frameworks that perform complicated wirings based on XML config files. I’m not ditching them, mind you. They can be incredibly valuable if used correctly and can save you from writing lots of boilerplate code. But they’re not indispensable. You can make it work with Pure DI.

The Example, Now Fixed
Let’s rewrite the sample, using DI to create low-coupled code. First of all, we need an ILogger interface:

public interface ILogger
{
void Debug(string entry);
void Trace(string entry);
void Info(string entry);
void Warning(string entry);
void Error(string entry);
}

With the interface declaration in place, we’re now ready to write n implementations as needed. I’ll leave those as an exercise.

Finally, let’s edit the ProductService class:

public class ProductService
{
// fields, properties, etc

public ProductService(IProductRepository repo, ILogger logger)
{
this.repo = repo ?? throw new ArgumentNullException(nameof(repo));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public void SaveProduct(ProductToAddDTO productDTO)
{
Product product = MapToEntity(productDTO);
this.repo.Add(product);
this.repo.SaveChanges();
this.logger.Info($"The product with Id {product.Id} was saved.");
}

// more methods
}

You’ve surely noticed that I’m injecting not just the logger through the constructor of ProductService, but also the repository. The same reasoning applies: As long as the entity is being persisted, your business rules shouldn’t care about where they’re being persisted to.

With that, the ProductService class is now completely unit-testable, without the need to touch either the database or the file system. Sweet!

2. Clear Separation Between Pure and Impure Code

Highly Testable Code, Testable Code Base, Making Code Base Highly Testable, Low Coupling, Writing Maintainable Code, Separation Between Pure and Impure Code, Logic and Presentation, Code Simplicity, Improving the Testability of Code, Code Testability, Making Code More Testable. TestRail.

This concept is really integral to the functional programming paradigm. But there’s really no reason for us, object-oriented programmers, not to apply it and take advantage of it. So, what’s this all about?
When we talk about pure and impure code, we’re really talking about purity of functions. In a nutshell, a pure function is a function that neither consumes nor creates side effects. It’ll become clearer with an example. Consider the code below:

public int Sum(int a, int b)
{
return a + b;
}

Yeah, I know, adding two numbers feels like the laziest example I could come up with, but it’s such a perfect illustration of a pure function that I couldn’t let it pass. So, this is a pure function. What makes it so?

  • The only data it can access are the values it gets as parameters.
  • It doesn’t cause any external change. It doesn’t cause a change in the database, or in the file system, or in any external reality. Nothing gets displayed on the screen or printed on paper. It doesn’t mutate any variable, anywhere.

Now, let’s see an example of an impure function:

public string GetGreeting()
{
var now = DateTime.Now;
var hour = now.Hour;
var greeting = string.Empty;

if (hour >= 6 && hour < 12)
{
greeting = "Good morning!";
}
else if (hour >= 12 && hour < 18)
{
greeting = "Good afternoon!";
}
else
{
greeting = "Good evening!";
}

greeting += " Today is " + now.ToString("D");
}

The GetGreeting method is definitely impure. Why? It accesses data from a source other than its parameters (which don’t even exist). Put that way, it might not look like much, but the consequences of those differences can be tremendous.

A pure function is deterministic. For a given input, it will always return the same output. Which makes it safe. You can call it a million times with the confidence that nothing, anywhere, will change because of it. What follows is that pure functions are intrinsically testable.

On the other hand, think about how you would go about testing the GetGreeting function above.

Assert.AreEquals("ok, what goes here???", obj.GetGreeting());

It’s definitely possible, and there are even multiple approaches available, but it requires some thought.

Receive Popular Monthly Testing & QA Articles

Join 34,000 subscribers and receive carefully researched and popular article on software testing and QA. Top resources on becoming a better tester, learning new tools and building a team.




We will never share your email. 1-click unsubscribes.
articles

3. Separation Between Logic and Presentation

Highly Testable Code, Testable Code Base, Making Code Base Highly Testable, Low Coupling, Writing Maintainable Code, Separation Between Pure and Impure Code, Logic and Presentation, Code Simplicity, Improving the Testability of Code, Code Testability, Making Code More Testable. TestRail.

This one really shouldn’t come as a surprise to any software developer worth their salt who’s been at the trade for some years. A clear separation between business logic and presentation is a goal worth pursuing, even if you don’t have testability in mind.

Maintaining a stark separation between these two concerns allows you to treat the user interface layer as a plugin, swapping one type of interface for another as the need arises. Or you could have more than one UI, all using the same underlying API.

But the real benefit of separating logic and presentation comes down to testing. When you have all the business logic contained in libraries that don’t care about fragile concerns such as the UI, you’ll have unit tests that are as fast, robust, and reliable as possible.

4. Simplicity

Highly Testable Code, Testable Code Base, Making Code Base Highly Testable, Low Coupling, Writing Maintainable Code, Separation Between Pure and Impure Code, Logic and Presentation, Code Simplicity, Improving the Testability of Code, Code Testability, Making Code More Testable. TestRail.

Finally, let’s talk about simplicity. And I don’t mean simplicity in a vague, faux-profound way, that sounds as if it makes sense while you’re reading, but then leaves you scratching your head, wondering how on earth you can apply that in a practical way. No, I mean it in a very practical and measurable way. Simple code is code with low cyclomatic complexity.

Cyclomatic complexity, in short, is the number of all possible branches of execution in a given function or method. A method with high cyclomatic complexity will require a larger number of test cases to be properly tested.
If you lower the complexity of your code, the code will not only get more testable but also become cleaner, more readable, and easier to maintain.

Improve the Testability of Your Code Today

Highly Testable Code, Testable Code Base, Making Code Base Highly Testable, Low Coupling, Writing Maintainable Code, Separation Between Pure and Impure Code, Logic and Presentation, Code Simplicity, Improving the Testability of Code, Code Testability, Making Code More Testable. TestRail.

Paraphrasing the Anna Karenina principle, we could say:

Testable codebases are all alike; every untestable codebase is untestable in its own way.

If you want your code to be highly testable—and you wouldn’t have read until the end if you didn’t—apply the four principles we just discussed.

Model dependencies to external realities as interfaces you can inject where needed, so they can be faked when testing. Dumb down your presentation layer as much as humanly possible, by concentrating business logic in small, simple, and loosely coupled modules that don’t perform IO and are comprised mostly of pure code. Isolate whatever impure code you’re left with from the rest.

And no matter what, never stop learning and practicing. Happy coding and happy testing!

This is a guest post by Erik Dietrich, founder of DaedTech LLC, programmer, architect, IT management consultant, author, and technologist.

Test Automation – Anywhere, Anytime

Try Ranorex for free

In This Article:

Try a 30-day trial of TestRail today!

Share this article

Other Blogs

Accessibility Testing in Action: Tools, Techniques, and Success
Software Quality, Agile, Automation, Continuous Delivery

Accessibility Testing in Action: Tools, Techniques, and Success

In today’s digital world, accessibility is essential—not just a nice-to-have. December 3rd, the International Day of Persons with Disabilities, reminds us how crucial it is to create inclusive digital spaces. For over 1 billion people living with disabilities,...
User Acceptance Testing (UAT): Checklist, Types and Examples
Agile, Continuous Delivery, Software Quality

User Acceptance Testing (UAT): Checklist, Types and Examples

User Acceptance Testing (UAT) allows your target audience to validate that your product functions as expected before its release. It ensures that you correctly interpret the requirements, and implement them in alignment with what users want and expect. What is...
Complete Guide to Non-Functional Testing: 53 Types, Examples & Applications
Software Quality, Performance, Security

Complete Guide to Non-Functional Testing: 51 Types, Examples & Applications

Non-functional testing assesses critical aspects of a software application such as usability, performance, reliability, and security. Unlike functional testing, which validates that the software functions in alignment with functional requirements, non-function...