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

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

Sometimes it happens that passing all dependencies to a class turns out to be difficult due to the number of paths existing in the code, each of which uses different dependencies. There are situations when the class handles several specific cases and requires a separate helper class for each. One can imagine a situation when 10 different classes are passed to the constructor, each of them to handle 1 of 10 extreme edge cases. Such a solution cannot be called clear or elegant. In such cases developer should consider whether his problem cannot be solved by using the appropriate factory.

Due to the fact that the problem is abstract, here is an example situation presented in the context of the SalaryCalculator class used in the previous posts in the series. The SalaryCalculator class requires an extension to include functionality that will calculate wage deductions. At this stage, deduction calculation modules already exist, but are divided into individual classes according to the position held by the employee.

Here is a trivial solution to the problem, which of course is a non-optimal solution, certainly not simplifying the process of creating unit tests. The following example shows the code where the CalculateSalary method has been extended with a conditional statement that creates the appropriate deduction calculator depending on the position occupied by the employee.

public decimal CalculateSalary(Employee employee)
{
     if(_dateTime.Today().Month == 2)
     {
        return BaseSalary;
     }

     IPenaltyCalculator penaltyCalculator = null;
     switch(employee.JobPosition)
     {
        case JobPosition.Accountant:
           penaltyCalculator = new AccountentPenaltyCalculator();
          break;
        case JobPosition.Assitant:
           penaltyCalculator = new AssistantPenaltyCalculator();
          break;
        case JobPosition.Cashier:
           penaltyCalculator = new CashierPenaltyCalculator();
          break;
        case JobPosition.Manger:
           penaltyCalculator = new ManagerPenaltyCalculator();
          break;
        case JobPosition.Trainee:
          penaltyCalculator = new TraineePenaltyCalculator();
          break;
     }
     var workHoursInCurrentMonth = _workTimeStorage.GetWorktime(employee, _dateTime.Today().Year, _dateTime.Today().Month);
     return employee.Stake * workHoursInCurrentMonth + _bonusCalculator.CalculateBonus(employee) – penaltyCalculator.GetPenalty(employee);
  }

The function written in such a way causes the test written for it to break the principle of class independence from other components. For the CaluclateSalary method implemented in the presented form, the salary calculation correctness test will depend on how the deduction calculator works, which is unacceptable.

The solution to this problem may be the use of a factory that will take one argument in the form of an employee position and return the IPenaltyCalculator interface. The factory prepared in this way should be passed – like the other dependencies – to the SalaryCalculator class constructor. Thanks to this approach, the test code has the possibility of mocking the factory, and thus developer can ensure the correctness of the test method in abstraction from the deduction calculator it uses. Then, as part of the unit tests of the CaluclateSalary function, developer can rightly assume that the result of the GetPenalty method is always correct.

Here is the correct solution to the problem.

public class SalaryCalculator : ISalaryCalculator
{
       private const decimal BaseSalary = 2500;
       private readonly IWorkTimeStorage _workTimeStorage;
       private readonly IBonusCalculator _bonusCalculator;
       private readonly IDateTime _dateTime;
       private readonly IPenaltyCalculatorFactory _penaltyCalculatorFactory;

       public SalaryCalculator(
              IWorkTimeStorage workTimeStorage,
              IBonusCalculator bonusCalculator,
              IDateTime dateTime,
              IPenaltyCalculatorFactory penaltyCalculatorFactory)
       {
              _workTimeStorage = workTimeStorage;
              _bonusCalculator = bonusCalculator;
              _dateTime = dateTime;
              _penaltyCalculatorFactory = penaltyCalculatorFactory;
       }

       public decimal CalculateSalary(Employee employee)
       {
        if(_dateTime.Today().Month == 2)
        {
            return BaseSalary;
        }

        var penaltyCalculator = _penaltyCalculatorFactory.Create(employee.JobPosition);
        var workHoursInCurrentMonth = _workTimeStorage.GetWorktime(employee, _dateTime.Today().Year, _dateTime.Today().Month);
        return employee.Stake * workHoursInCurrentMonth + _bonusCalculator.CalculateBonus(employee) – penaltyCalculator.GetPenalty(employee);
       }
}

public class PenaltyCalculatorFactory : IPenaltyCalculatorFactory
{
    public IPenaltyCalculator Create(JobPosition jobPosition)
    {
        switch (jobPosition)
        {
            case JobPosition.Accountant:
             return new AccountentPenaltyCalculator();
            case JobPosition.Assitant:
             return new AssistantPenaltyCalculator();
            case JobPosition.Cashier:
             return new CashierPenaltyCalculator();
            case JobPosition.Manger:
             return new ManagerPenaltyCalculator();
            case JobPosition.Trainee:
              return new TraineePenaltyCalculator();
            default:
             return new NullPenaltyCalculator();
        }
    }
}

It should be noted that the conditional instruction responsible for selecting the appropriate deduction calculator has been moved to the Create method in the prepared factory. Meantime, the SalaryCalculator class has been extended with an additional dependency in the form of the discussed factory. The results of the tests written for a class prepared this way are completely independent of the dependencies used.

Stay tuned! Next part of the article in a week!
Let know us what do you think about this article? Share your knowledge and experience with us: