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.
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.
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
}
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.
There are several "creation" patterns in the design pattern, but let's use one of them, the Abstract factory pattern.
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()
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.
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
}
}
}
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
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.
Recommended Posts