はじめに
本タイトルの一連の記事ではデザインパターンを用いながら、テストと実働のための実装を考える。
前回の記事では、基本概念と使いやすい形を持つ Proxy パターンと Monostate パターンを紹介した。
今回は、ユニットテストをする対象となるクラスを設計する方法について述べる。
前回に引き続き、Dependency Injection (DI) を主題とする。
サンプルプログラムは Python と C# と C++ が混在しているが、いずれ補う予定である。
ご容赦願いたい。
生成と使用の分離
生成と使用が同じ場所
テストがしづらいクラス(や関数)の実装では、生成と使用が同じ場所で行われているものがある。
// 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;
}
この例では、クラス Some のインスタンスを直にその場で生成し、機能を呼び出している。
これでは、関数 func のテストを行おうとしても、Some に介入するのは不可能である。
別の例
コンストラクタでメンバーを生成し、その後、いずれかの関数で使用するケースも要注意である。
次の例では、Target クラスのテストコードは Some 生成後に m_some を差し替えることができ、介入する余地がある。一見すると、これでも良いように見える。
しかし、別のクラスや関数から Some を呼び出すような場合、そのクラスや関数のテストを行うことはできない。
// C++
Target::Target()
: m_some(new Some("abc"))
{
}
Target::exec() {
m_some->get();
}
// テスト対象のある別の関数
void func() {
Target target;
target.exec(); // Target 内で、オリジナルの Some が生成・使用される
}
生成のためのパターンを使う
以上のような固定的に生成と使用を行っていては、ユニットテストを困難にしてしまう。
1つでもこのようなクラスができてしまうと、それを解きほぐすのは大変である。
このため最初期から生成と使用を分離して、クラスを設計することが望ましい。
やり方はいくつかあるが、実際に利用しやすい形で例を挙げる。
Abstract factory の例
デザインパターンの中で「生成」に関するパターンはいくつかあるが、そのうちの 1 つ Abstract factory パターンを使用してみる。
次の例では、利用者 Client クラスが Creator という生成者を利用する形となっている。
受け取った Creator のインスタンスから自身に必要な Logger と File のインスタンスを生成している。
Creator の実体は main() 内にて作られた TestCreator であり、コンソールへの出力とダミーファイルの生成を行うテスト用のクラスに差し替えられている。
図のクラス名 | サンプルのクラス名 |
---|---|
ICreator | Creator |
TargetCreator | なし |
Target1 | Logger |
Target2 | File |
Target1Object | なし |
Target2Object | なし |
ClientTest | main関数 |
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:
# 何か処理
self._logger.write('正常終了')
def main():
creator = TestCreator()
client = Client(creator)
client.exec()
Singleton の例
もう 1 つの「生成」パターンの例は Singleton である。
Singleton は生成がそのクラス内部に閉じてしまっているため、介入はできない。
介入できないことが目的なのだが、テストには使用しづらいものとなる。
ここでは、Singleton の構成を少し緩めて、介入の口を用意する。
instance は protected で持ち、メソッドはすべて NVI を使うかそのままで virtual にする。
次に C# での Singleton クラスとテスト用クラスへの置き換え例を示す。
Dummy へと切り替えた 2 回目の Print() の呼び出しは、同じメソッドでも "Dummy" と出力する。
ユニットテストの時は、始めから Dummy を呼ぶように、準備処理の中で設定すればよい。
// 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" と出力
}
SomeDummy.UseDummy(); // Singleton を置き換え
{
var target = Some.getInstance();
target.Print(); // "Dummy" と出力
}
}
}
応用
Abstract factory は factory をクラス生成時に受け渡す必要があるため、煩わしいことがある。
factory を Singleton や Monostate で提供するような応用が考えられる。
もちろん、ユニットテスト時には、テスト用ダミークラスを生成する factory を返すようにする。
これを使って、最初の例を置き換えると、次のようになる。
// Python
class FactoryServer:
some_creator = SomeCreator()
def func():
some = FactoryServer.some_creator()
value = some.get()
if value < 10:
pass
まとめ
生成と使用を分離するための例を示した。
Abstract factory の例では、生成と使用のどちらもオブジェクトも抽象化することにより、実際のクラスとユニットテスト用のクラスを自由に差し替えられる構造を示した。
Singleton の例では、生成物に介入し、テスト用インスタンスを使用させた。
応用として、Singleton/Monostate と Abstract factory を組み合わせることで、実装の簡便さを保ったままテストの介入が同時に成立するような構成を与えた。
- Factory method は、自身の内部で生成するので、DI 向けではない。ただし、「別の例」で示した Some の生成処理を Factory method 化しておくこことはできる(本稿に追記予定)。
- Builder パターンは、Abstract factory の代わりとして使用できる
- Singleton の属性 instance は、static なメンバー変数、すなわち Monostate パターンである
その他
- Python で Singleton は面倒そうなので、Monostate を使った