DEV Community

Shuto Osawa
Shuto Osawa

Posted on

How I Developed a Habit of Writing Unittests

Introduction

Since I started my career in software engineering, there are a myriad of skills I had to learn aside from 'writing code'.
I am primarily using C# at my work, and object-oriented coding was a foreign language for me, being that I have only done numerical calculations using other programming languages. At the time, there was no need for me to really think about classes and their relationship to other classes.

Difficulty at work

After nailing down some basic coding, it occured to me that I did not want to maintain what I have been writing due to the fact that the software was too messy and loose coupling was not achieved.
My company does not enforce code review or writing unittests, therefore the quality of the code is up to me.
This motivated me to get serious about researching on software development and increasing its quality.
The code I have been writing was in need of some serious refactoring, and I decided to start with writing unittests.

Meeting unittesting

Because my company does not enforce writing automated tests, the importance of it was lost on me. However, once I developed the habit of writing test code I could see the power of writing them, particularly when it comes to refactoring.
In this post, I will talk about how was able to grow accustomed to writing unittests. I was struggling greatly with actually applying them because I did not know that my code was not exactly testable.
There is plenty of information about unittesting online and how to do it right. So, I will simply talk about my personal experience.

One of the most basic examples provided online

The easiest unittest might be a method performing summation.

public class Arithmetic
{
    public int Summation(int a,int b)
    {
        return a + b;
    }    
}
Enter fullscreen mode Exit fullscreen mode

We can write a unittest for this code in NUnit in the following way.

public class Tests
{
    private Arithmetic arithmetic;
    [SetUp]
    public void Setup()
    {
        arithmetic = new Arithmetic();
    }

    [Test]
    public void SummationTest()
    {
        //Arrange
        int a = 1;
        int b = 1;
        //Act
        int result = arithmetic.Summation(a,b);
        //Assert
        Assert.AreEqual(2,result);
    }
}
Enter fullscreen mode Exit fullscreen mode

The code above passed the test. We can obviously write tests if the production code is simple enough like the summation example above. We have checked that 1+1 returns 2.
Image description

Different example

Consider an example of getting relative distance between a house and a cat. First, prepare a cat class and he has a house. The house instance will be generated within the cat class, so I will call it a coupled cat class.

public class House
{   
    public int Location { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class CoupledCat
{
    private House house;
    public int Location { get; set; }

    public CoupledCat()
    {
        house = new House();
        house.Location = 1;
    }

    public int RelativeDistance()
    {
        return Math.Abs(house.Location - this.Location);
    }
}
Enter fullscreen mode Exit fullscreen mode

We can run the code.

static void Main(string[] args)
    {

        //Prepare a cat with his house information
        CoupledCat cat = new CoupledCat();
        cat.Location = 3;

        //get distance
        int rel = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+rel+" meter(s)");        
    }
Enter fullscreen mode Exit fullscreen mode

Test for coupled cat class

We can write a test for the CoupledCat class.

public class CoupledCatTest
{
    private CoupledCat cat;
    [SetUp]
    public void Setup()
    {
        cat = new CoupledCat();
    }

    [Test]
    public void DistanceTest()
    {
        //Arrange
        cat.Location = 3;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);
    }
}
Enter fullscreen mode Exit fullscreen mode

We can write a test for this class, however it is inconvenient since the house location is fixed in the coupled cat class and we would like to move the house location.

Dependency injection

I would like to have some freedom in terms of house location. In order to resolve this dependency, we can introduce dependency injection. There are three ways to perform it, 1.Property Injection, 2.Constructor Injection, and 3.Method Injection.

1.Property Injection

The cat class has a House property. This injection is probably not so great since the code would not run if we forget to inject the house instance to the cat class.

public class Cat
{
    public House House { get; set; }
    public int Location { get; set; }
    public int RelativeDistance()
    {
        return Math.Abs(House.Location - this.Location);
    }
}
Enter fullscreen mode Exit fullscreen mode
public class House
{   
    public int Location { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Let's actually run the code. Before we run the code, the house instance needs to be injected to the cat's house property.

class Program
{
    static void Main(string[] args)
    {
        //Prepare a house
        House house = new House();
        house.Location = 1;

        //Prepare a cat with his house information
        Cat cat = new Cat();
        cat.House = house;
        cat.Location = 3;

        //get distance
        int rel = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+rel+" meter(s)");
    }
}
Enter fullscreen mode Exit fullscreen mode

2.Constructor Injection

We can change the injection location from property to the constructor. The house instance needs to be prepared before creating the cat clas.

public class Cat
{
    private House house;
    public int Location { get; set; }

    public Cat(House house)
    {
        this.house = house;
    }
    public int RelativeDistance()
    {
        return Math.Abs(house.Location - this.Location);
    }
}
Enter fullscreen mode Exit fullscreen mode

3.Method Injection

Since we need the house class only in the RelativeDistance method, so we can inject the instance in the method.

public class Cat
{
    public int Location { get; set; }

    public int RelativeDistance(House house)
    {
        return Math.Abs(house.Location - this.Location);
    }
}
Enter fullscreen mode Exit fullscreen mode

Writing a test for this house-cat example

I will write the test for property injection based cat class.

public class CatHouseTest
{
    private House house;
    private Cat cat;
    [SetUp]
    public void Setup()
    {
        house = new House();
        cat = new Cat();
        cat.House = house;
    }

    [Test]
    public void DistanceTest()
    {
        //Arrange
        house.Location = 1;
        cat.Location = 3;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);        
    }  
}
Enter fullscreen mode Exit fullscreen mode

Image description

Adding flexibility to the cat class

Though we introduced dependency injection, they are still not so flexible because the cat only knows the distance between his current location and his house. They would love to wander around the city and eat some fish. Let him have some fish!

Introducing an interface

Instead of a house we can implement a building having location information. By introducing an interface, we can look at all the classes with Location property in the same way.

public interface IBuilding
{
    int Location { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The house class can implement IBuilding since the house has the location property.

public class House:IBuilding
{
    public int Location { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Let's go back to the cat class. We can replace the House property in the cat class to the building interface.

public class Cat
{
    public IBuilding Building { get; set; }
    public int Location { get; set; }
    public int RelativeDistance()
    {
        return Math.Abs(Building.Location - this.Location);
    }
}
Enter fullscreen mode Exit fullscreen mode

We can run the code and there is no big difference between the previous code and the new code other than replacing cat.House.

class Program
{
    static void Main(string[] args)
    {
        //Prepare a house
        House house = new House();
        house.Location = 1;

        //Prepare a cat with his house information
        Cat cat = new Cat();
        cat.Building = house;
        cat.Location = 3;

        //get distance
        int rel = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+rel+" meter(s)");
    }
}
Enter fullscreen mode Exit fullscreen mode

The great thing about interface is that we can introduce anything that implements the interface. Let him finally introduce a fish market.

public class FishMarket:IBuilding
{
    public int Location { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
class Program
{
    static void Main(string[] args)
    {
        //Prepare a house
        House house = new House();
        house.Location = 1;

        //Prepare a fishmarket
        FishMarket fishMarket = new FishMarket();
        fishMarket.Location = 5;

        //Prepare a cat with his house information
        Cat cat = new Cat();
        cat.Location = 2;
        cat.Building = house;

        //get distance
        int relToHouse = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+relToHouse+" meter(s)");

        //Set fishmarket
        cat.Building = fishMarket;

        //get distance
        int relToFishMarket = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+relToFishMarket+" meter(s)");
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

Writing several tests

At this point, we could add some more tests and we can use TestCase attribute to perform several cases in a single method.
Let's write tests for both Cat to House distance and Cat to Fishmarket distance.
In the cat to fishmarket example, three test cases are provided. It is important to consider edge cases if necessary.

public class CatHouseTest
{
    private House house;
    private FishMarket fishMarket;
    private Cat cat;
    [SetUp]
    public void Setup()
    {
        house = new House();
        fishMarket = new FishMarket();
        cat = new Cat();       
    }

    [Test]
    public void DistanceToHouseTest()
    {
        //Arrange
        house.Location = 1;
        cat.Building = house;
        cat.Location = 3;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);
    }

    [TestCase(0,1,1)]
    [TestCase(8,2,10)]
    [TestCase(4,-1,3)]
    public void DistanceToFishMarketTest(int expected,int catLocation,int fishMarketLocation)
    {
        //Arrange
        fishMarket.Location = fishMarketLocation;
        cat.Location = catLocation;
        cat.Building = fishMarket;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(expected,result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

The result is all green, so we are very happy about the result!

Using external resource

If we need to use some data outside of the current project, our test would fail and we need to modify the test.
We can prepare the text file in the C drive and we would like to read some data from the file. The file only has a single number in the text file in this example.

public class Cat
{
    public IBuilding Building { get; set; }
    public int Distance { get; set; }

    public int LoadLocationFromText()
    {
        string text = System.IO.File.ReadAllText(path+location.txt);
        return Convert.ToInt32(text);
    }
    public int RelativeDistance()
    {
        int location = LoadLocationFromText();
        return Math.Abs(Building.Location - location);
    }

}
Enter fullscreen mode Exit fullscreen mode

The test passes only when the text file exists in the computer. This means that we cannot run the test if we use someone else's computer.
Image description

Introduce a testclass

we are interested in testing the RelativeDistance method and LoadLocationFromText method depends on the text file in my computer. It means that if the test is performed on someone else's computer, the test would not work unless the text file and its path are provided. It is better if we can run the test without preparing external resource.
If we are allowed to make some modification to the class methods, we can rewrite the LoadLocationFromText method using virtual keyword. If a method is virtual then we can override the method in the test. This allows us to safely ignore the external dependency part.

Let's create another class that inherits the cat class. The LoadLocationFromText has override keyword, so we can return arbitrary number instead of reading the text file.

public class CatTest : Cat
{
    private int input;
    public CatTest(int input)
    {
        this.input = input;
    }
    public override int LoadLocationFromText()
    {
        return this.input;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class CatHouseTest
{
    private House house;
    private FishMarket fishMarket;
    private CatTest cat;
    [SetUp]
    public void Setup()
    {
        house = new House();
        fishMarket = new FishMarket();
    }

    [Test]
    public void DistanceToHouseTest()
    {
        //Arrange
        cat = new CatTest(3);
        house.Location = 1;
        cat.Building = house;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);
    }

    [TestCase(0,1,1)]
    [TestCase(8,2,10)]
    [TestCase(4,-1,3)]
    public void DistanceToFishMarketTest(int expected,int catLocation,int fishMarketLocation)
    {
        //Arrange
        cat = new CatTest(catLocation);
        fishMarket.Location = fishMarketLocation;
        cat.Building = fishMarket;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(expected,result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now it passes all the tests.
Image description

Conclusion

The main goal for me was developing the habit of unittesting.
However, the biggest hurdle was understanding how to write a test. I started writing tests and performed refactoring in the following manner.

  1. Find the smallest class in the project. It is important to find a class with least amount of dependencies.
  2. Write one easy test for the class.
  3. If there is a huge class, break it down in smaller pieces of classes.
  4. Write tests for those small classes.
  5. Find a class with external dependency such as loading files, database connection and so on.
  6. If they can be modified then make the method virtual and override the method by creating a class for test purpose.
  7. Since the project started to have some tests, it should be easier to modify code without worrying too much about producing bugs.
  8. Introduce interfaces here and there and also apply design patterns if there is a suitable one. At this point the code should look way better than before and I am not afraid of writing tests for my code. The next step for me would be properly preparing a mock in my test.

Top comments (0)