What is unit tests

Unit Tests. Why Testable Code Matters? (Part 2)

Testing code is not enough to be smart. Code has to be created testable in advance. This ensures higher ROI with fewer investments. Plus there are bonuses: significant levels of scalability, stability and maintainability. Thus we continue the cycle of posts entirely dedicated to creation of testable-first software code.

Oh, and here’s the fastest way to get to the Part 1 of this story, in case you’ve missed something.

Impurity and Testability

Non-deterministic factors and side effects have the same detrimental influence on the codebase. If they are not controlled, they make the code less maintainable, less understandable, not reusable, more tightly coupled and untestable while deterministic and side-effect – free methods are more testable and reusable to create larger programs. Such methods are called pure functions.

Unit testing a pure function generally doesn’t cause any problems and it is easy. All it takes is to pass a few arguments and to verify that they are correct. Impure, hard coded, embedded factors which cannot be replaced make the code untestable. Taking into account applications are complex, you will get saddled with a codebase which is not maintainable and full of different bad practices.

Impurity and Testability

Impurity tends to spread. If the method Foo() rides on non-deterministic or side-effecting method Bar(), Foo becomes non-deterministic or with side effects. And as a result, the whole codebase can get contaminated.

Impurity can’t be avoided.  Any real-life application will sooner or later have to read and manipulate state by communicating with the databases, environment, web services, configuration files or other external systems. And instead of attempting to get rid of all impurity, it is more sensible to attempt to reduce those impurity factors. Prevent it from contaminating your codebase and decouple hard-coded elements to process and unit test those elements separately.

Warning signs of poorly testable code

If you have difficulty writing unit tests, the problem actually is not in a test case but in the very code. To stay prewarned let’s look through the most common warnings indicating that your code might be poorly testable.

Static Properties and Fields

Static properties and fields (global state) can deteriorate code readability and testability, by holding back the data which a method needs to perform its job, by using non-deterministic factors, or overpromoting side effects.

Functions that read or modify mutable global state are impure by default

For example, it is hard to reason about the following code, which relies on a globally accessible property:

If (!SmartHomeSettings.CostSavingEnabled) { _swimmingPoolController.HeatWater(); }

What if the HeatWater() method doesn’t get called when we are sure it should have been? Since any part of the application might have changed the CostSavingEnabled value, we must find and analyze all the places modifying that value in order to get to the reason for the problem. It is impossible to set some static properties for testing purposes (e.g., DateTime.Now, or Environment.MachineName; they are read-only, but still non-deterministic).

Immutable and deterministic global state is called a constant. Constant values don’t introduce any non-deterministic factors and side-effects.

double Circumference(double radius) { return 2 * Math.PI * radius; } // Still a pure function!

Singletons

The Singleton pattern is a form of the global state. It makes the API vaguer, hamper obtaining the truthful information about dependencies and promote tight coupling of the components. Singletons breach the Single Responsibility Principle since they control their own initialization and lifecycle along with their main jobs.

Singletons increase order-dependency of unit-tests because they carry state around for the lifetime of the whole app or unit test suite. Here is an example:

User GetUser(int userId)
 {
    User user;
    if (UserCache.Instance.ContainsKey(userId))
    {
        user = UserCache.Instance[userId];
    }
    else
    {
        user = _userService.LoadUser(userId);
        UserCache.Instance[userId] = user;
    }
    return user;
 }

If we run first a test for the cache-hit, a new user will be added to the cache. So if we run the next test to check the cache-miss scenario, it may fail since it presumes the cache is blank. To solve this problem, writing additional teardown code is needed to empty the UserCache after each unit test.

Employing Singletons is a practice which should be avoided. But it is essential to differentiate between Singleton as a single instance of an object and a design pattern. If it is a single instance of an object, it is the application’s task to create and maintain a single instance. This is handed with a factory or Dependency Injection container, which creates a single instance somewhere near the “top” of the application (i.e., closer to an application entry point) and then passes it to every object that requires it. This approach is absolutely valid, from both testability and API points of view.

The New Operator

Renewing an instance of an object to perform some tasks produces the same issue as the Singleton anti-pattern: vague APIs with tight coupling, poor testability and obscure dependencies.

For instance, to test if the following loop stops when a 404 status code is returned, the developer should set up a test web server:

using (var client = new HttpClient())
 {
    HttpResponseMessage response;
    do
    {
        response = await client.GetAsync(uri);
        // Process the response and update the uri...
    } while (response.StatusCode != HttpStatusCode.NotFound);
 }

Sometimes something new won’t do any damage: for example, creating simple entity objects will produce a good result:

var person = new Person("John", "Doe", new DateTime(1970, 12, 31));

It is good as well to write a small, temporary object with no side effects, except to introduce some changes to their own state, and then return the result based on that state. In the following example, we won’t pay any attention to Stack methods and whether they were called or not. Our task is to verify the end result:

string ReverseString(string input)
 {
    // No need to do interaction-based testing and check that Stack methods were called or not;
    // The unit test just needs to ensure that the return value is correct (state-based testing).
    var stack = new Stack<char>();
    foreach(var s in input)
    {
        stack.Push(s);
    }
    string result = string.Empty;
    while(stack.Count != 0)
    {
        result += stack.Pop();
    }
    return result;
 }

Static Methods

Static Methods can cause non-deterministic factors and side effects too. And they are the source of tight coupling and can increase untestability. For instance, to check if the following method works right, unit tests must act upon environment variables and read the console output stream to ensure that the appropriate data was typed.

void CheckPathEnvironmentVariable()
 {
 
    if (Environment.GetEnvironmentVariable("PATH") != null)
    {
        Console.WriteLine("PATH environment variable exists.");
    }
 
    else
    {
       Console.WriteLine("PATH environment variable is not defined.");
    }
 
 }

However, pure static functions are OK: any combination of them will still be a pure function. For example:

double Hypotenuse(double side1, double side2) { return Math.Sqrt(Math.Pow(side1, 2) + Math.Pow(side2, 2)); }

Conclusion

Writing code which can be easily tested is a challenging task. It calls for focusing, discipline, following certain rules and a lot of effort. However, writing code generally is a complicated task, and we’d rather be attentive and think twice before writing new code.

As a result, our code will have maintainable, not so tightly coupled, cleaner and reusable APIs which won’t be a conundrum to a developer trying to decipher it.

The key benefit of a good, testable code is not the testability only but the code which is easily understandable, maintainable and extendable.

VN:F [1.9.22_1171]
Rating: 3.3/5 (3 votes cast)
Unit Tests. Why Testable Code Matters? (Part 2), 3.3 out of 5 based on 3 ratings

    Facebook Comments

    comments

    'Unit Tests. Why Testable Code Matters? (Part 2)' have no comments

    Be the first to comment this post!

    Would you like to share your thoughts?

    Your email address will not be published.

    Software development and outsourcing blog by QArea © 2016

    Яндекс.Метрика