ユニットテストを実行するためには、デバイスなどの環境依存要素を代替・排除する必要がある。
これらの取り扱いを注意深く設計しておかないと、ユニットテスト不能になってしまう。
本タイトルの一連の記事ではデザインパターンを用いながら、テストと実働のための実装を考える。
本記事では、オンメモリーで持つデータの扱いを考えるが、ネットワークなどのデバイスの場合も同じである。
突き詰めると、どのようにテスト用モジュールへの差し替えを行うかが焦点となる。
この考えは、Dependency Injection (DI) と呼ばれている。
DI では、Interface を介してモジュール間の結合度を下げる。
サンプルプログラムは C++ で記述しているが、C# や Python も書く予定。
変更履歴
2015-04-26 Pythonのサンプルコードを追加
環境依存なデータ
データは、アプリケーションの動作を決めるものだったり、動作の結果を伝えるものだったり様々である。
ローカルで済めば良いが、どうしてもグローバルに使用したい場合がある。どのように保持すべきだろうか。
次の例では、外部モジュールのバージョンによって動作を変える機能を想定している。
// 導入前(テスト不能、または困難)
void func() {
// バージョン取得
int version = SomeModule::getMajorVersion();
if (version > 10) {
// 指定バージョンの時に、何か処理
}
}
ここで、SomeModule::getMajorVersion()
は特定環境でしか使えなかったり、状態を切り替えるのが大変だったりするものとする。このようなコードでは、続く if 文のテストは難しい。
Proxy パターンの利用
ここでは SomeModule
を置き換えるダミーのクラスを作り、実体とダミーにアクセスする Proxy を用意してテスト可能にしてみる。
まずは、窓口となるProxyクラスのInterfaceから。
class ModuleProxy {
virtual int getMajorVersion_() const = 0;
protected:
ModuleProxy() {}
public:
virtual ~ModuleProxy() {};
int getMajorVersion() const {
return this->getMajorVersion_();
}
};
次は実体クラスとダミークラス。
ここでは、元々のSomeModuleも隠蔽して、利用クラスを作っている。
// 実際に外部モジュールを呼び出すクラス
class SomeModuleImpl : public ModuleProxy {
virtual int getMajorVersion_() const override {
return SomeModule::getMajorVersion();
}
};
// テスト用ダミークラス
class ModuleDummy : public ModuleProxy {
virtual int getMajorVersion_() const override {
return major_;
}
public:
ModuleDummy(int ver = 0)
: major_(ver)
{}
int major_;
};
func関数と、テストコードは次のようになる。
void func(ModuleProxy& module) {
// バージョン取得
int version = module.getMajorVersion();
if (version > 10) {
}
}
void XXXTest::funcTest() {
ModuleDummy dummy(10);
{
func(dummy);
}
{
dummy.major_ = 11;
func(dummy);
}
}
Monostate パターンの利用
別の形として Monostate パターンを使用して実装を行ってみる。
ここではメンバー関数も static にしているが、通常の関数でもかまわない。
class Data {
static int majorVersion;
public:
static int getMajorVersion() const {
return majorVersion;
}
friend class XXXTest;
};
int Data::majorVersion = 1;
これを使うアプリケーション実装は次のようになる。
void func() {
if (Data::getMajorVersion() > 10) {
}
}
テスト側はこのようになる。
void XXXTest::funcTest() {
{
Data::majorVersion = 10;
func();
}
{
Data::majorVersion = 11;
func();
}
}
Monostate パターンはシンプルだが、値の初期化を考える必要がある。
他の static な要素から参照される場合は、初期化の順番に注意しなくてはならない。
サンプルコード(Python)
Monostate パターンの Python 版を次に示す。
メソッドには classmethod を指定した。
class Data:
_majorVersion = 1
@classmethod
def getMajorVersion(cls) -> int:
return cls._majorVersion
テスト側は、次のようになる。
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())
まとめ
本記事では Proxy パターンを用いて DI を実現した。
また、よりシンプルにMonostate パターンを使用する方法も示した。
次回以降のネタ
- 契約プログラミング
契約の機能をサポートしない、C++/C#/Python などの言語であっても、契約を意識しておくことは重要である。
- Proxy パターンと Monostate パターンの融合
Monostate はポリモルフィズム的に使えるので、Proxy パターンを取り込んでより使いやすくすることも可能である。
- ユニットテストで friend を使う (C++)
ユニットテストで friend を使うのは良いが、アプリケーション側の header に friend を追加することになるため、コンパイルのやり直しが発生してしまう。その対処方法を考える。
- shared_ptrとweak_ptrを使う (C++)
リソースアクセスをshared_ptrで実現する場合に、ProxyのIFとしてはweak_ptrを使った方がよいことがある。
テスト可能な状態のまま、このような機能拡張を行ってみる。