In order to execute unit tests, it is necessary to substitute / eliminate environment-dependent elements such as devices. If you do not carefully design these handlings, you will not be able to unit test. In a series of articles in this title, we will consider implementations for testing and production, using design patterns. In this article, we will consider how to handle data held on-memory, but the same applies to devices such as networks.
Ultimately, the focus is on how to replace the test module. This idea is called Dependency Injection (DI). In DI, the degree of coupling between modules is reduced via Interface.
The sample program is written in C ++, but C # and Python will also be written.
2015-04-26 Added Python sample code
The data can be anything that determines the behavior of the application or conveys the result of the behavior. You can do it locally, but there are times when you really want to use it globally. How should it be retained?
The following example assumes a function that changes its behavior depending on the version of the external module.
//Before introduction (untestable or difficult)
void func() {
//Get version
int version = SomeModule::getMajorVersion();
if (version > 10) {
//Something to do with the specified version
}
}
Here, it is assumed that SomeModule :: getMajorVersion ()
can be used only in a specific environment, or it is difficult to switch the state. With such code, it is difficult to test the if statement that follows.
Here, create a dummy class that replaces SomeModule
, prepare an entity and a Proxy that accesses the dummy, and make it testable.
First, from the Proxy class Interface that serves as the window.
class ModuleProxy {
virtual int getMajorVersion_() const = 0;
protected:
ModuleProxy() {}
public:
virtual ~ModuleProxy() {};
int getMajorVersion() const {
return this->getMajorVersion_();
}
};
Next is the entity class and dummy class. Here, the original SomeModule is also hidden and the usage class is created.
//A class that actually calls an external module
class SomeModuleImpl : public ModuleProxy {
virtual int getMajorVersion_() const override {
return SomeModule::getMajorVersion();
}
};
//Dummy class for testing
class ModuleDummy : public ModuleProxy {
virtual int getMajorVersion_() const override {
return major_;
}
public:
ModuleDummy(int ver = 0)
: major_(ver)
{}
int major_;
};
The func function and the test code are as follows.
void func(ModuleProxy& module) {
//Get version
int version = module.getMajorVersion();
if (version > 10) {
}
}
void XXXTest::funcTest() {
ModuleDummy dummy(10);
{
func(dummy);
}
{
dummy.major_ = 11;
func(dummy);
}
}
Another way to implement it is to use the Monostate pattern. Here, the member function is also static, but it can be a normal function.
class Data {
static int majorVersion;
public:
static int getMajorVersion() const {
return majorVersion;
}
friend class XXXTest;
};
int Data::majorVersion = 1;
The application implementation that uses this is as follows.
void func() {
if (Data::getMajorVersion() > 10) {
}
}
The test side looks like this.
void XXXTest::funcTest() {
{
Data::majorVersion = 10;
func();
}
{
Data::majorVersion = 11;
func();
}
}
The Monostate pattern is simple, but you need to think about value initialization. When referenced by other static elements, care must be taken in the order of initialization.
The Python version of the Monostate pattern is shown below. Classmethod was specified for the method.
class Data:
_majorVersion = 1
@classmethod
def getMajorVersion(cls) -> int:
return cls._majorVersion
The test side looks like this:
import unittest
class Test_testSample(unittest.TestCase):
def test_version(self):
Monostate.Data._majorVersion = 10
self.assertFalse(Target.func())
Monostate.Data._majorVersion = 11
self.assertTrue(Target.func())
In this article, DI was realized using the Proxy pattern. We also showed how to use the Monostate pattern more simply.
It's important to be aware of contracts, even in languages such as C ++ / C # / Python that don't support contract functionality.
Since Monostate can be used polymorphically, it is possible to incorporate Proxy patterns to make it easier to use.
It's good to use friend in the unit test, but it will add friend to the header on the application side, which will cause recompilation. Think about how to deal with it.
When implementing resource access with shared_ptr, it may be better to use weak_ptr as the IF of Proxy. Try these enhancements while still testable.
Recommended Posts