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

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

This rule is important for at least two reasons. The first results from the fact that the construction of popular libraries used for mocking in C # (e.g., NSubstitute) requires from the class which functions will be mocked to be an interface. Some libraries, such as Moq, have slightly less restrictive criteria and allow you to mock virtual methods.

The second argument is that starting writing a class by providing an interface for it forces developer to think about what functionalities a given class should really implement. The interface defines the way the class will communicate with other classes. Thanks to this, the functionalities and tasks realized by class implementing given interface will be defined in much preciser way. This approach reduces the likelihood of creating classes whose functional range is not consistent.

When defining an interface, keep in mind that it should refer in a generalized way to the tasks that will be carried out by its implementation. It should also be noted that the interface should allow a multitude of implementations. However, as with other rules, common sense should be used here. The defined interfaces should maintain a certain balance between the generality and the concretization of its applications, i.e. the interface should describe what should be the result and the argument of the method, as well as describe in a precise but concise manner the operation that it will perform. The interface name should not reveal implementation details or suggest a specific implementation.

For example, the interface shown below is definitely too specific.

public interface IHPPrinterLongFilePrinterWithISOCharacters
{
       public void Print(IFile file);
}

The interface name is so unambiguous that it directly implies the implementation details. However, it should be remembered that this is a very extreme case. In fact, the very name of the interface, apart from the application context, cannot be the only reason for assessing its correctness or usability.

The next interface is far too general.

public interface IPerformer<T, Tin>
{
       T Do(Tin source);
}

The interface written in this way is very vague, its name does not reveal what task it can perform or what features its argument should have. It should be remembered that the use of interfaces should facilitate, not hamper, the work of developers.

The interface should specify precisely what to expect after calling a given method and what arguments a given method requires. It also should leave implementation details in abstraction. In other words, interfaces should be treated as black boxes – we know what is the result of the operation, we know what it takes to perform the operation, but we do not know how the operation is performed. Below is an example of a properly written interface.

public interface IFilePrinter
{
       bool Print(IFile file, IPrintConfiguration configuration);
}

A positive side effect of using interfaces in a proper way is increased code readability. Thanks to well-written interfaces you can quickly find out what operations are performed and understand how data flows in the program. And it can be done without going too much into detail. It supports well up-down code analyzing behavior according to the principle from general to detail.

Rule 3: Calls to static functions should be wrapped.

The next principle combines the mechanisms described in the previous two rules. It often happens that static code is used in the code, and this applies to both system classes such as DateTime, or classes from external libraries or being part of the source code of the software being created. As mentioned before, test conditions should be strictly determined and deterministic. Calling functionality as part of the test code that uses the DateTime.Today function, as it does in the example below, carries the risk that such a test may become non-deterministic. When the call to the static function DateTime.Today is directly used in the code of the tested functionality, the result of the test of this functionality will depend on the day on which it is called. This situation is unacceptable.

To solve this problem, wrap static calls in wrapper classes that implement interfaces that describe the required static functionality. The following is an example of a class that wraps all invoked methods of the DateTime class, along with an appropriate interface.

public interface IDateTime
{
       DateTime Today();
       DateTime Now();
}

public class DateTimeWrapper : IDateTime
{
    public DateTime Now()
    {
        return DateTime.Now;
    }

    public DateTime Today()
    {
        return DateTime.Today;
    }

}

By using the IDateTime interface you can now modify the SalaryCalculator class so that the amount of salary in February always provides a concrete defined basis, and the code written in this way still remains fully testable.

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

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

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

        var workHoursInCurrentMonth = _workTimeStorage.GetWorktime(employee, _dateTime.Today().Year, _dateTime.Today().Month);
        return employee.Stake * workHoursInCurrentMonth +  bonusCalculator.CalculateBonus(employee);
       }
}

The code written in this way allows for full control over the initial conditions of the CalculateSalary functionality tests. For the code presented above, you can create tests that will check the behavior of the method depending on the selected month in the year. Without wrapping the DateTime class in a special wrapper class it would not be possible.

Stay tunned! Next part of the article in a week!
We know more – if you need help immediately or you want to grow with us, contact us: