How to make unit tests Part.2 Class design for tests

Introduction

In a series of articles in this title, we will consider implementations for testing and production, using design patterns. In the previous article (http://qiita.com/progrommer/items/a04a74d8b9f43eaef4b9), I introduced the Proxy pattern and Monostate pattern, which have basic concepts and easy-to-use shapes. This time, I will describe how to design a class to be unit tested. Continuing from the last time, the theme is Dependency Injection (DI).

The sample program is a mixture of Python, C # and C ++, but we plan to supplement it in due course. Please forgive me.

Separation of generation and use

Same place for generation and use

Some implementations of classes (or functions) that are difficult to test are created and used in the same place.

// Python
class Some:
    def get(self):
        return 42

def func():
    some = Some()
    value = some.get()
    if value < 10:
        pass
// C++
struct Some {
    int get() const {return 42;}
};

void func() {
    if (Some().get() < 10) return;
}

In this example, an instance of class Some is created on the fly and the function is called. This makes it impossible to intervene in Some, even if you try to test the function func.

Another example

Be careful when creating a member in the constructor and then using it in one of the functions. In the following example, the Target class test code can replace m_some after Some is generated, leaving room for intervention. At first glance, this looks good. However, if you call Some from another class or function, you cannot test that class or function.

// C++
Target::Target()
  : m_some(new Some("abc"))
{
}

Target::exec() {
    m_some->get();
}

//Another function to be tested
void func() {
    Target target;
    target.exec();  //Original Some is generated and used in Target
}

Use patterns for generation

Unit testing becomes difficult if the above-mentioned fixed generation and use are performed. Once even one such class is created, it is difficult to unravel it. Therefore, it is desirable to design the class by separating the generation and use from the earliest stage. There are several ways to do this, but here are some examples that are actually easy to use.

Abstract factory example

There are several "creation" patterns in the design pattern, but let's use one of them, the Abstract factory pattern.

AbstractFactory.png

In the following example, the user Client class uses a generator called Creator. You are instantiating the Logger and File you need from the Creator instance you received. The substance of Creator is TestCreator created in main (), and it has been replaced with a test class that outputs to the console and generates a dummy file.

Class name in the figure Sample class name
ICreator Creator
TargetCreator None
Target1 Logger
Target2 File
Target1Object None
Target2Object None
ClientTest main function
DummyCreator TestCreator
Dummy1Object ConsoleLogger
Dummy2Object DummyFile
# Python
class Logger:
    def write(self, msg: str) -> None:
        pass

class ConsoleLogger(Logger):
    def write(self, msg: str) -> None:
        print(msg)

class File:
    pass

class DummyFile(File):
    pass

class Creator:
    def create_logger(self) -> Logger:
        return None
    def create_file(self) -> File:
        return None

class TestCreator(Creator):
    def create_logger(self) -> Logger:
        return ConsoleLogger()
    def create_file(self) -> File:
        return DummyFile()

class Client:
    def __init__(self, creator: Creator):
        self._logger = creator.create_logger()
        self._file = creator.create_file()
    def exec(self) -> None:
        #Something processing
        self._logger.write('Successful completion')

def main():
    creator = TestCreator()
    client = Client(creator)

    client.exec()

Singleton example

Another example of a "generate" pattern is the Singleton. Singletons can't intervene because the generation is closed inside the class. The purpose is to be unable to intervene, but it is difficult to use for testing.

Here, we will loosen the structure of Singleton a little and prepare a mouth for intervention. Instances are protected and all methods are either using NVI or just virtual.

Singleton1.png

The following is an example of replacing the Singleton class and the test class in C #. The second call to Print () that switches to Dummy prints "Dummy" even with the same method. At the time of unit test, it should be set in the preparation process so that Dummy is called from the beginning.

// C#
public class Some {
    protected static Some instance;
    static Some() {
        instance = new Some();
    }
    static public Some getInstance() {
        return instance;
    }
    virtual public void Print() {
        Console.WriteLine("Some");
    }
}

internal class SomeDummy : Some {
    internal static void UseDummy() {
        instance = new SomeDummy();
    }
    public override void Print() {
        Console.WriteLine("Dummy");
    }
}

public class Client {
    static void Main(string[] args) {
        {
            var target = Some.getInstance();
            target.Print();    // "Some"And output
        }

        SomeDummy.UseDummy();  //Replace singleton
        {
            var target = Some.getInstance();
            target.Print();    // "Dummy"And output
        }
    }
}

application

The Abstract factory can be annoying because it needs to pass the factory when creating the class. Applications such as providing a factory as a Singleton or Monostate can be considered. Of course, at the time of unit test, return the factory that generates the dummy class for testing. Using this to replace the first example:

// Python
class FactoryServer:
    some_creator = SomeCreator()

def func():
    some = FactoryServer.some_creator()
    value = some.get()
    if value < 10:
        pass

Summary

An example for separating generation and use is shown.

In the abstract factory example, we showed a structure in which the actual class and the class for unit testing can be freely replaced by abstracting the object for both creation and use. In the Singleton example, we intervened in the product and used a test instance. As an application, by combining Singleton / Monostate and Abstract factory, we gave a configuration that allows test intervention to be established at the same time while maintaining the simplicity of implementation.

Other

Recommended Posts

How to make unit tests Part.2 Class design for tests
How to make a unit test Part.1 Design pattern for introduction
[Python] How to make a class iterable
[Cocos2d-x] How to make Script Binding (Part 2)
[Cocos2d-x] How to make Script Binding (Part 1)
How to make Spigot plugin (for Java beginners)
How to make Python faster for beginners [numpy]
How to make a shooting game with toio (Part 1)
How to make a hacking lab-Kali Linux (2020.1) VirtualBox 64-bit Part 2-
How to make a Python package (written for an intern)
How to write a Python class
[For non-programmers] How to walk Kaggle
How to make a slack bot
How to make a crawler --Advanced
How to make a recursive function
Spigot (Paper) Introduction to how to make a plug-in for 2020 # 01 (Environment construction)
How to make a deadman's switch
[Blender] How to make a Blender plugin
[Blender] How to make Blender scripts multilingual
How to make a crawler --Basic
How to authenticate with Django Part 2
How to authenticate with Django Part 3
How to make a model for object detection using YOLO in 3 hours
How to call when using double underscore (Private attribute) for class function
How to access data with object ['key'] for your own Python class
How to make Word Cloud characters monochromatic
How to make Selenium as light as possible
[Introduction to Python] How to use class in Python?
How to use cybozu.com developer network (Part 2)
Decorator for unit tests using random numbers
How to create * .spec files for pyinstaller.
How to use Tweepy ~ Part 1 ~ [Getting Tweet]
[Python] Organizing how to use for statements
I wrote unit tests for various languages
How to install Windows Subsystem For Linux
How to use __slots__ in Python class
How to use Pylint for PyQt5 apps
How to make multi-boot USB (Windows 10 compatible)
How to use "deque" for Python data
How to make a Backtrader custom indicator
How to make a Pelican site map
How to use fingerprint authentication for KDE
[For recording] Keras image system Part 1: How to create your own data set?
How to make a dialogue system dedicated to beginners
How to make an embedded Linux device driver (11)
How to specify the launch browser for JupyterLab 3.0.0
How to use MkDocs for the first time
How to make WTForms TextArea correspond to file drop
How to make an embedded Linux device driver (8)
How to make an embedded Linux device driver (1)
How to make an embedded Linux device driver (4)
How to make multiple kernels selectable on Jupyter
How to use Template Engine for Network Engineer
How to make a dictionary with a hierarchical structure.
How to make an embedded Linux device driver (7)
How to make an embedded Linux device driver (2)
How to make scrapy JSON output into Japanese
How to make an embedded Linux device driver (3)
How to install Python for pharmaceutical company researchers
How to use data analysis tools for beginners
How to use Tweepy ~ Part 2 ~ [Follow, like, etc.]