Edited at

ユニットテストの作り方 Part.1 導入のためのデザインパターン

More than 3 years have passed since last update.

Part.2はこちら

ユニットテストを実行するためには、デバイスなどの環境依存要素を代替・排除する必要がある。

これらの取り扱いを注意深く設計しておかないと、ユニットテスト不能になってしまう。

本タイトルの一連の記事ではデザインパターンを用いながら、テストと実働のための実装を考える。

本記事では、オンメモリーで持つデータの扱いを考えるが、ネットワークなどのデバイスの場合も同じである。

突き詰めると、どのようにテスト用モジュールへの差し替えを行うかが焦点となる。

この考えは、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を使った方がよいことがある。

テスト可能な状態のまま、このような機能拡張を行ってみる。