How to Write Testable Code in .NET

Introduction

In this article, I give a brief introduction to writing testable code. Although I have described and used samples in the context of .NET, the high-level principles of writing testable code applies to most of the programming language.

What is Testable Code?

Testable code refers to the loosely coupled code where code does not directly depend on internal/external dependencies, so we can easily replace the real dependencies (sometimes referred to as real services) with mock services in test cases. For example, if my code calls a method GetProductInfo() which is connecting to a real database, fetches the product information, and returning to the main method. To test my main method functionality without actually connecting to the real database, I can write a test that uses a mock service to get product data.

While it might seem a little confusing at this point, it is actually very simple when you see a working example of it.

Why is it important to write testable code?

Writing testable code is crucial, as it helps you to identify and resolve potential problems/ bugs in the early development stage instead of getting issues in UAT or production when working with real services. Also, testing the fake services is fast compared to testing real services. For example, connecting to a real database is more time-consuming than testing with fake data in a mock service.

How to write testable code

Writing testable code is all about dependency management. If we are writing code using the SOLID principles, then our code will already be loosely coupled and in compliance with testing standards. While writing testable code, our main objective is to identify the dependencies and move the instantiation of those dependencies outside of our code. When we create an object of the class using a new keyword inside the current class, then this class directly depends on the class whose object we are creating. For example, in the below code, ProcessProduct class creates the object of the DBService class. Hence DBService class is the dependency and the ProcessProduct class depends directly on DBService.

class Product  
{  
    public int Id { get; set; }  
    public string Name { get; set; }  
    public string Category { get; set; }  
    public float Price { get; set; }  

}  

class ProcessProduct  
{  
    public void DisplayProduct()  
    {  
        DBService dbService = new DBService();  
        var product = dbService.getProduct();  
        Console.WriteLine($"Product Name: {product.Name} Category: {product.Category} Price: {product.Price}");  
    }  
}  

class DBService  
{  
    public Product getProduct()  
    {  
        throw new NotImplementedException("Get product from database");  
    }  
}  

To make this code loosely coupled, we will use a very popular design pattern called dependency injection. There are several ways to implement dependency injection which is itself a very wide topic. So to keep this article simple, I will use one of the ways to implement dependency injection- Dependency injection using Constructor.

In this method, instead of creating the object of DBService inside ProcessProduct, we will inject the object through the constructor of the dependent class and save it in a private variable as shown in the below code.

class Product   
{   
    public int Id { get; set; }   
    public string Name { get; set; }   
    public string Category { get; set; }   
    public float Price { get; set; }   
}   
class ProcessProduct   
{   
    private IDBservice _dbService;   
   
    public ProcessProduct(IDBservice dbService)   
    {   
        _dbService = dbService;   
    }   
    public void DisplayProduct()   
    {   
        var product = _dbService.getProduct();   
        Console.WriteLine($" Product Name: { product.Name } Category: { product.Category } Price: { product.Price }");   
    }     
}   
interface IDBservice {   
    Product getProduct();   
}   

class DBService : IDBservice   
{   
    public Product getProduct() {   
        throw new NotImplementedException("Get product from database");   
    }   
}  

We have also created an interface IDBService in the above example and declared the object of DBService using this interface. By using this interface, we allow any class object that implements the IDBService interface to inject through the constructor.

Below is an example of passing a mock class object for testing.

class ProcessProduct  
{  
    private IDBservice _dbService;  
    public ProcessProduct(IDBservice dbService)  
    {  
        _dbService = dbService;  
    }  
    public void DisplayProduct()  
    {  
        var product = _dbService.getProduct();  
        Console.WriteLine($" Product Name: { product.Name } Category: { product.Category } Price:  { product.Price }");  
    }  
  
}  
  
interface IDBservice {  
    Product getProduct();  
}   
class MockDBService : IDBservice  
{  
    public Product getProduct()  
    {  
        return new Product()  
        {  
            Id = 2124,  
            Name = "Eggs",  
            Category = "Food",  
            Price = 2.23m  
        };  
    }  
}  
  
class TestProcessProduct  
{  
    void Test()  
    {  
        ProcessProduct processProduct = new ProcessProduct(new MockDBService());  
        processProduct.DisplayProduct();  
    }  
}  

In this example, we want to test the ProcessProduct class to display product information without actually connecting to the real database. To achieve this, instead of injecting the DBService class object, we are injecting the MockDBService class object. And because of dependency injection, we do not need to do any changes in the ProcessProduct class. Hence, this code is loosely coupled and testable.

Conclusion

In this article, I have explained the fundamentals of writing testable code with a simple example. The real-life projects might include many dependencies and it is difficult to manage them. So in those cases, we want to use some Dependency Injection Containers like “Unity Container” to create and resolve dependencies. Also writing mock cases can be cumbersome, so instead, we can use frameworks to write mock services.

Thanks for reading!