Help us understand the problem. What is going on with this article?

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

More than 5 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を使った方がよいことがある。
テスト可能な状態のまま、このような機能拡張を行ってみる。

progrommer
SEだけどプログラマー。実装とテストの自動化に興味あり。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした