fbpx
How to avoid common pitfalls that make testing difficult? Part two

This is the second part of the entire entry. If you haven’t read the first part, look here.

This is the first and most important rule for writing testable code. The classes being tested should have all dependencies provided from the higher abstraction level (from the parent). Dependency injection is done, usually, through constructors, sometimes through setting methods. The purpose of this procedure is to reverse the relationship between two classes in inheritance relationship. That is, the parent class should decide how its child classes should behave, not the other way around.

Such construction of the classes is crucial in the context of writing testable code because, as part of the unit test, we test only one functionality, and the code of the test being created cannot be dependent on other modules.

As an example we may have a class that performs some calculations. The class requires, in order to perform these calculations, the use of a function which is a component of another class.

public class SalaryCalculator : ISalaryCalculator
{
    private readonly IWorkTimeStorage _workTimeStorage;

    public SalaryCalculator(IWorkTimeStorage workTimeStorage)
    {
        _workTimeStorage = workTimeStorage;
    }

    public decimal CalculateSalary(Employee employee)
    {
        var workHoursInCurrentMonth = _workTimeStorage.GetWorktime(employee, DateTime.Today.Year, DateTime.Today.Month);
        var bonusesCaluclator = new BonusCalculator(_workTimeStorage);

        return employee.Stake * workHoursInCurrentMonth + bonusesCaluclator.CalculateBonus(employee);
    }
}

The presented class includes a method whose task is to calculate the employee’s salary, which consists of the base salary and the bonus calculated by the BonusCalculator class. The SalaryCalculator class also uses a class that implements the IWorkTimeStorage interface, which is used to retrieve the number of hours an employee worked in a given month.

It is possible to write a test for such code, but it will not be a unit test. As part of writing unit tests, all functionalities that are not currently tested should be replaced using the ‘mocking’ mechanism, which allows you to simulate the result of external calls. There may be a question why in such a trivial situation there is a need to use Mock. However, when writing a test for CalculateSalary functionality, the developer would have to know exactly the procedure of how the bonuses are calculated by the BonusCalculator class. As a result, the expected test result would have to take into account the implementation of the BonusCalculator class, which at this stage means that such a test would be neither individual nor easy to maintain. Such a test would be susceptible for changes in the BonusCalculator class, with the consequence that there is more than one reason for this test to fail. This is very important consequence as the CalculateSalary test may not pass, even though the salary calculation functionality itself works properly. For this to happen, it is sufficient that there will be a simple change in the method of calculating the bonus in the BonusCalculator class which was not included in the test acceptance conditions.

To fix the situation it is enough that the BonusCalculator class is passed through the constructor parameter. Thanks to this, as part of the SalaryCalculator class tests, all calls to the BonusCalculator class can be replaced by calls to the object mocking the actual functionality.

Below is what the code looks like after making corrections.

public class SalaryCalculator : ISalaryCalculator
{
    private readonly IWorkTimeStorage _workTimeStorage;
    private readonly IBonusCalculator _bonusCalculator;

    public SalaryCalculator(IWorkTimeStorage workTimeStorage, IBonusCalculator bonusCalculator)
    {
        _workTimeStorage = workTimeStorage;
        _bonusCalculator = bonusCalculator;
    }

    public decimal CalculateSalary(Employee employee)
    {
      var workHoursInCurrentMonth = _workTimeStorage.GetWorktime(employee, DateTime.Today.Year, DateTime.Today.Month);
      return employee.Stake * workHoursInCurrentMonth + _bonusCalculator.CalculateBonus(employee);
    }
}

Dependencies of the class being tested should be provided by the constructor. This is in accordance with the principle of reversal of dependence (IoC), which helps to understand the flow of control within the software and reduces coupling between individual components. Dependency injection also increases test reliability due to the fact that a single unit of code is tested.

Next part of the article in a week!
If you need help immediately or you want to grow with us, contact us: