arrow
Back to blog

Core Rules And Principles Of Writing Testable Code

clock

12 min read

Every developer knows that the testable code can make life easier. There are a lot of books and articles written about unit-testing. Particular attention is paid to Test-driven software development (TDD) as the best process for the development of hi-tech products. In my working routine, I face tons of problems with untestable code. It may happen even in those projects for which 100% test coverage is the main acceptance criteria.

I’d like to admit that a “Good code” and “Unit-testable code” aren’t always equivalent terms. Your code can be understandable, self-documented, but untestable at the same time. There’s 1 universal tip for writing a unit-testable code. You should just use the principles DRY, KISS, and SOLID, as I do in Dashdevs.

Unfortunately, developers may ignore them to benefit from timesaving or because of the lack of competence. Anyway, situations, when you need to write a test for a fundamentally untestable code, are more frequent than it might be wished.

Being a developer with more than 6 years of experience, I’d like to share my life hacks and acquired knowledge of testable code. In this article, I’ll try to list the most common problems and solutions for resolving them. Further, you may find the squeeze of my personal experience and source code examples from C#, MsTest, and Moq. Experience is the best teacher, and we, in Dashdevs, like to share how to learn by doing. Let’s run for a better code!

Operator new

In my opinion, operator new is the most horrible thing you may meet in the code awaiting unit tests. The value of this operator is doubtful because its usage is a violation of the Dependency inversion principle in SOLID.

Let’s refer a simple example:

public class CertificateManager
{
    public void CreateCertificate(CertificateDto certificateDto)
    {
            ...
        var certificate = new Certificate(certificateDto.Name, certificateDto.Url, certificateDto.Content);
        certificate.Upload();
    }
}

This method is bad for a single reason: it is not testable by any means.

It exists only to call Upload (). However, we cannot create a mock-object (mocking) Certificate; therefore, we cannot check if it was really “called.”

It’s impossible. There are 3 ways to solve the situation.

1. Straightforward. We can delegate Certificate instantiation to a factory method.

public class CertificateManager
{
    private CertificateCreator _certificateCreator;
 
    public CertificateManager(CertificateCreator certificateCreator)
    {
         _certificateCreator = certificateCreator;
    }
 
    public void CreateCertificate(CertificateDto certificateDto)
    {
            ...
 
        var certificate = _certificateCreator.Create(certificateDto.Name, certificateDto.Url, certificateDto.Content);
 
        сertificate.Upload();
    }
}

Now we can create the mock of _certificateCreator and certificate itself.

Pros: It doesn’t change the code structure.

Cons: This approach requires the creation of two new types. We’re complicating the code in order to create unit tests. It gets hard to understand the reason for the usage of a fabric method. It may cause a violation of the Single responsibility principle (SOLID).

2. Moving the instantiation to the upper level.

public class CertificateManager
{
    public void CreateCertificate(Certificate certificate)
    {
            ...
        certificate.Upload();
    }
}

In this example, we send Certificate as an argument and can mock it.

Pros: We simplify the method.

Cons: It requires changing the internal structure of the code. It may cause issues and require to rewrite tests for the calling method. This refactoring requires more time for implementation. Anyway, this approach is the best because it improves the architecture of the application in general.

3. Move of the functionality to the lower level.

public class CertificateManagerpublic class CertificateManager
{
    private CertificateUploader _certificateUploader;
 
    public CertificateManager(CertificateUploader certificateUploader)
    {
        _certificateUploader = certificateUploader;
    }
 
    public void CreateCertificate(CertificateDto certificateDto)
    {
            ...
 
        var certificate = new Certificate(certificateDto.Name, certificateDto.Url, certificateDto.Content);
 
        _certificateUploader.Upload(certificate);
    }
}

In this solution, Certificate becomes a flat model, and we don’t need to mock it. Instead, we mock CertificateUploader.

Pros: It doesn’t change the structure of the code but transforms Certificate to a flat model.

Cons: The test creation process becomes more complicated because we need to check an argument in certificateUploader. Upload for accordance with the template. It will look this way:

certificateUploader.Verify(x => x. Upload(
    It.Is
    (arg =>
        arg.Name == certificateDto.Name
            && arg.Url == certificateDto.Url
            && arg.Content == certificateDto.Content
    )
), Times.Once);

In the example above, the class has only 3 fields, that is why the comparison of that class with a template is not a complicated task. However, in real projects, there can be more than ten properties in one class. Some developers write some kinds of “half-tests” to simplify the process:

certificateUploader.Verify(x => x. Upload(It.IsAny), Times.Once);

Such an approach brings a small profit. It’s checking the fact of the call but ignores the instantiation of the argument. Besides, the comparison of a template requires that all testing object properties, which relates to a template, are public. Otherwise, we need to refuse the full testing or make the part of the class properties public, which interferes encapsulation. That’s why I don’t recommend using this approach unless its use is of critical necessity.

Access to the object properties

This issue is almost invisible, but it can cause a bunch of problems.

public void ProcessCertificate(Certificate certificate)
{
        ...
    var ownerName = certificate.Owner.Name;
        ...
}

“Just next to nothing” you may think. Right, it’s a tiny nuance. Unless this tiny problem transforms the process of Certificate mocking to the mind-boggling experience. To fix the issue with failing NullReferenceException, we need to mock Owner too. In general, it’s a bad practice to set up multilevel access to the class properties or methods. However, However, if there’s a case when you need to test the code with such a structure, you can use these solutions:

1. Transferring of responsibility for retrieving properties to an upper level.

public void ProcessCertificate(Certificate certificate, string ownerName)
{
    ...
}

Pros: It requires a minimal change in the structure of the code.

Cons: The call will look the following way:

_certificateManager.ProcessCertificate(certificate, certificate.Owner.Name);

We may hardly call it a good solution.

2. Extension of the class.

Add to Certificate a property or method that takes responsibility for multilevel access.

public class Certificate
{
        ...
    public virtual string OwnerName => this.Owner.Name;
        ...
}

or

public class Certificate
{
            ...
    public virtual string GetOwnerName()
    {
        return this.Owner.Name;
    }
            ...
}

The method will turn into this:

public void ProcessCertificate(Certificate certificate)
{
        ...
    var ownerName = certificate.OwnerName;
        ...
}

or

public void ProcessCertificate(Certificate certificate)
{
        ...
    var ownerName = certificate.GetOwnerName();
        ...
}

Accordingly.

Pros: There’s no need to change the structure of the code.

Cons: This approach has no obvious disadvantages. However, we shouldn’t add to the code of an added property or method, logic, not related to multi-level access. If there’s logic, we have to write additional tests to check it. If the class does not have an interface, it’s important not to forget to make the new property or method virtual.

Static

The usage of static makes you addicted to the mock-libraries (mockers). Some mockers can create mocks for static classes (AFAIK most of them are paid, but you may try to find a free one). The best solution is to avoid static classes and methods in your code at all. But it isn’t always possible, because a lot of well-known frameworks still use it nowadays. If you need to test a static one, you can wrap it with the regular class and mock it the usual way.

public static class StaticLegacy
{
    public static void StaticDo()
    {
        ...
    }
}
 
public interface ILegacyAdapter
{
    void Do();
}
 
public class LegacyAdapter : ILegacyAdapter
{
    public void Do()
    {
        StaticLegacy.StaticDo();
    }
}

Pros: You can hardly find one, but this approach allows testing of a method with a static call.

Cons: We’ll have to create two extra types for the unit tests.

Private methods

The private methods are the core of encapsulation. However, they can become a real headache during unit tests creation. I don’t appeal to refuse the usage of private methods at all, but you should think twice before creating them. Everything is fine if the private method is just a part of the public one. We can test it in the context of the public method. The problems appear when several public methods share one private.

public class SomeClass: ISomeClass
{
    private IHelper _helper;
 
    public SomeClass(IHelper helper)
    {
        _helper = helper;
    }
 
    public void Do1()
    {
                ...
        _helper.Do();
                ...
    }
 
    public void Do2()
    {
                ...
           _helper.Do();
                ...
    }
}

We can test PrivateDo in the context of both public methods, but it will lead to the duplication of tests and, consequently, to a doubling of the work when changing PrivateDo (as an alternative — to the complication of the unit test structure, which is undesirable). You can test PrivateDo in the context of only one of the public methods, but in this case, the unit test of another method loses its informativeness: as it is unclear if the testing method bugs itself or only its private part.

1. Turning private method into the public.

This solution allows us to test the method directly. The serious disadvantage of this approach is that it violates the encapsulation. Another issue we face is dependencies between Do1 and Do2 from PrivateDo. Some mockers allow resolving this issue by partly-mock (e.g., nSubstitude), while others do not have partial mocking functionality.

Nevertheless, we can reduce negative implications by not adding a newly opened method to the interface. For instance:

public interface ISomeClass
{
    public void Do1();
 
    public void Do2();
}
 
public class SomeClass: ISomeClass
{
    public virtual void Do1()
    {
            ...
        PrivateDo();
            ...
    }
 
    public virtual void Do2()
    {
            ...
        PrivateDo();
            ...
    }
 
    public virtual void PrivateDo()
    {
            ...
    }
}

In this case, ISomeClass clients won’t know about PrivateDo. It allows us to avoid illegal usage. The test won’t mock the interface ISomeClass, but SomeClass directly (therefore, its methods have become virtual).

2. Reflection.

Reflection allows us to test the private method without breaking encapsulation rules. Nevertheless, the test becomes more complicated, and its performance speed decreases. Some test frameworks include adapters that simplify reflective testing. For example, MsTest has a special class PrivateObject that allows us to call private methods in such a way:

var someClass = new SomeClass();
var obj = new PrivateObject(someClass);
obj.Invoke("PrivateDo");

But the problem with the dependency between Do1 and Do2 from PrivateDo won’t be solved using the selected approach.

3. Moving out responsibility.

A wiser solution for the private methods is to make it a public method of another class. For instance:

public class SomeClass: ISomeClass
{
    private IHelper _helper;
 
    public SomeClass(IHelper helper)
    {
        _helper = helper;
    }
 
    public void Do1()
    {
    
                ...
                
        _helper.Do();
        
                ...
                
    }
 
    public void Do2()
    {
    
                ...
                
           _helper.Do();
           
                ...
                
    }
}

Pros: It solves all the issues with Private method testing and decreases code connection.

Cons: If the private method requires private fields, moving it out to separate type breaks encapsulation. It happens because we need to send private fields as arguments.

This solution is unacceptable if the private method changes the inner state. For instance:

public class SomeClass: ISomeClass
{
    private string _privateString1;
 
    private string _privateString2;
 
    public void Do1()
    {
            ...
        PrivateDo();
            ...
    }
 
    public void Do2()
    {
            ...
        PrivateDo();
            ...
    }
 
    private void PrivateDo()
    {
            ...
        privateString1 = "qwe";
        privateString2 = "asd";
 
    }
}

In this particular case, we can do a small trick. We can save a private method in a class if we keep only the logic of the change of the state, but move all the rest to the separate class:

public class SomeClass: ISomeClass
{
    private string _privateString1;
 
    private string _privateString2;
 
    private IHelper _helper;
 
    public SomeClass(IHelper helper)
    {
        _helper = helper;
    }
 
    public void Do1()
    {
            ...
        PrivateDo();
            ...
    }
 
    public void Do2()
    {
            ...
        PrivateDo();
            ...
    }
 
    private void PrivateDo()
    {
            ...
        var result = _helper.Do();
 
        privateString1 = result.String1;
        privateString1 = result.String2;
    }
}

Class without interface

We can mock classes without an interface. But we need to keep in mind two things. First of all, the methods of such classes must be virtual. Otherwise, the part of your mock will work as a mock and another part as an original object. In this case, the lack of proper focus can cause the bug with low traceability. Fixing of such a bug can cost you a few extra hours. An example:

public class SomeClass
{
    public virtual void Do1()
    {
        ...
    }
 
    public virtual void Do2()
    {
        ...
    }
 
    public void Do3()
    {
        ...
    }
}

The mocks Do1 and Do2 will work as they are supposed to work. The mock method Do3 is useless because it calls the original method.

The second issue with classes without interfaces is the constructor. The mocking of the class requires the presence of default constructor (without parameters). Otherwise, you need to create mocks of parameters too.

In some cases, the constructor contains the logic. So, this logic is called in all tests for mocked classes. It is not a good approach in general. The solution is really simple — all mocked classes must contain the interface. There are no exceptions for this rule.

The only special case is sealed classes without interfaces. We can find them in some frameworks. They are unmockable by default. The only solution is to create a wrapper and mock it.

Logic in constructors

Logic in a constructor is a well-known antipattern. However, a lot of developers use it. Such an approach has a number of disadvantages. The most crucial for us is that the constructor logic will be called in every test again and again. The bug in the constructor breaks all tests of the class. There are 2 ways to avoid the issue.

1. Method Init.

It divides the process of initialization into 2 separate parts: object creation (constructor) and additional logic (Init).

public class SomeClass
{
    public SomeClass(params)
    {
         ...
    }
 
    public void Init()
    {
        ...
    }
}

Pros: The tests ignore Init method to avoid dependencies on the constructor logic.

Cons: Outside the tests, this approach works really badly. Firstly, because it is not obvious. Secondly, it complicates the settings of IoC container and violates the Single responsibility principle of SOLID.

I don’t recommend using initializers even though they solve the central issue of constructors.

2. Fabric method.

The instantiation logic is moved to a separate class.

public class SomeClass
{
    public SomeClass(calculatedParams)
    {
        ...
    }
}
 
    public interface ISomeClassCreator
    {
        SomeClass Create(params);
    }
 
public class SomeClassCreator: ISomeClassCreator
{
    public SomeClass Create(params)
    {
        ...
    }
}

Pros: Allows to get rid of heavy constructors and improves the code structure.

Cons: Requires the creation of two special types for tests. Besides, heavy constructors are the issue not only for unit-testing. That’s why such complication can be reasonable.

Usage of variables environments

If the method uses a certain global resource (for example, settings from app.config), the process of its testing becomes non-trivial at all.

public class SomeClass
{
    public void Do()
    {
            ...
        var mySetting = ConfigurationManager.AppSettings["MySetting"]
            ...
    }
}

Simulating correct operation for ConfigurationManager is a complex task. We can’t mock it. We can create different files for different tests, but this may be a rather complicated solution. In general, the case is identical to the static object. That’s why the solution is the same — create a class that contains environment variables and works through it:

public class SomeClass
{
    private Settings _settings;
 
    public SomeClass(Settings setting)
    {
            _ settings = setting;
    }

    public void Do()
    {
                ...
        var mySetting = settings.MySetting;
                ...
    }
}

Conclusion

Support of unit tests adds several factors to consider when designing a testing workflow, but the efforts will pay off.

There are lots of best practices for writing testable code. The main goal for us is to create a robust solution that will be maintainable for everyone. Even if you don’t plan to write unit tests, you need to use this guide because it will help you to improve the quality of the code and the architecture in general. It takes additional time, but it’s worth it.

In the experience of Dashdevs company, we had a lot of products where clients asked not to write unit tests. After all, most of the clients realized that unit tests are the warrants of the stable product.

Let’s discuss the nuances of the development of your reliable product with Dashdevs.

It’s not the end

Would you like to discuss with us your idea or project? Feel free to contact us, and we’ll reach out to you shortly!

Share article

Table of contents