これは、C#において、メソッド等のデフォルトがvirtualではないことのデメリットに書いた問題の解決編です。変更することができないクラスを参照するクラスの単体テストが書けないケースで、mockオブジェクトではない別の解決方法について書きます。
問題点のおさらい §
今回は、ソースで問題点を明示してみましょう。
こんなC#のソースがあるとします。
public class ClassA
{
private string val = string.Empty;
public void Add( string val )
{
this.val = val;
}
}
public class ClassB
{
public void Sample( ClassA instanceA )
{
instanceA.Add("test value");
}
}
ここで、ClassAはクラスライブラリなどに含まれていて変更できないクラスだとします。このとき、ClassBのSampleメソッドのテストを書きたいと思ったとします。テストは、正しい値がinstanceAに書き込まれたかどうかを確認したいわけです。しかし、ClassAは追加した値を取得する機能を提供しておらず、確かに追加されたか否かを確認できません。
Javaの場合は、メソッドはデフォルトでvirtualであるために、ClassAを継承したmockオブジェクトをテスト側で作成し、それをSampleメソッドに渡すことで、テスト側で用意したコードを実行させることができます。その結果、追加された値の正しさをテスト側で検証できます。しかし、デフォルトがvirtualではないC#ではできません。
朝起きてみると思い付いた §
これは簡単な方法では解決できないかな、と思いつつ作業が一時止まってしまったのですが。今朝、朝食を食べている途中で、ふっと良いアイデアが浮かびまいた。一瞬で答が見えました。
「そうか、必要なメソッドが呼べれば良いのであって、絶対にinstanceAを渡す必要があるわけではないんだ!」
C#の場合、メソッド呼び出しを間接的に行うメカニズムは、virtualな仮想メソッドだけではありません。delegateという強力なツールもあるわけです。
上の例の、ClassBを以下のように書き換えます。もちろん、ClassAは書き換えられないのが前提なので、書き換えずにそのまま使います。
public delegate void AddValue( string name );
public class ClassB
{
public void Sample( AddValue addValue )
{
addValue("test value");
}
}
こうすると、通常のプログラムのソースコードは、ClassAのAddメソッドのdelegateをSampleメソッドに渡すことで同等の機能を発揮させることができます。単体テストのソースでは、テスト側で用意したメソッドのdelegateをSampleメソッドに渡すことができます。
これでClassAを変更することなく、ClassBをテスト可能にすることができました。
残された課題 §
delegateを使う方法は、mockオブジェクトとは等価ではないので、完全に置き換えられる訳ではありません。いくつか思い付くことを書いておきます。
- 呼び出すメソッドが増えると、引数のdelegate値が増えてしまう
- delegateのインスタンスを作成する分だけパフォーマンスが落ちる
- delegateのインスタンスを作成する分だけソースを書くのが面倒
- 関係ない値を引数に渡した場合にコンパイラがエラーにしてくれる可能性が落ちる
- メソッドでは使えるが、プロパティやインデクサでは使えない
一方、以下のようなメリットも考えられなくはありません。
- ClassAとClassBの関係が粗になり、個々のクラスの独立性が高くなる
余談 §
この事例から、「あって良かったdelegate」と思いました。これは、よくJava信者から聞かれる「なぜdelegateが必要か分からない」という問いかけの答になるかもしれません。
つまり、メソッドのデフォルトがvirtualではない場合には、delegateは実用上必須の機能かもしれません。もし、拡張に対して開いていること(デフォルトがvirtualであること)が過度に強力すぎるとして否定する態度を取った場合、delegateの存在は必須であるのかもしれません。