放浪軍師のアプリ開発局

Xamarin.Formsを使ってAndroid,iOS,UWP,WPFで動くアプリを開発したりしています。Unityも始めます。尚、このブログはわからないところを頑張って解決するブログであるため、正しい保証がありませんのでご注意ください。

UnitTest で学ぶ Dependency Injection (依存性の注入)

さてはじまりました放浪軍師のアプリ開発局。今回はC#の備忘録だよ!

UnitTest で学ぶ Dependency Injection

先日業務の方で UnitTest を書く機会があり、その際に学んだ事を忘れる前に残しておきます。Dependency Injection の概念とかそういう小難しい事はここでは説明せず、コードベースでどういうメリットがあるのかを説明します。正直 Dependency Injection を完全に理解するには多分これが一番早いと思います

GitHub

github.com

他クラスに依存しないメソッドのテスト

例えばこういうクラスがあったとします。

public class VTuber {
    public string FastName { get; }
    public string LastName { get; }
    public string Attribute { get; }
    public VTuber(string fastName, string lastName, string attribute) {
        FastName = fastName;
        LastName = lastName;
        Attribute = attribute;
    }
    public string FullName() => $"{LastName} {FastName}";
}

VTuber の名前と属性を登録するクラスです。FullName() は姓名を返してくれるメソッドですね。単体テストはどう書けばよいでしょう?きっとこんな感じです。

[TestMethod()]
public void FullNameTest() {
    var vTuber = new VTuber("林檎", "九条", "貴族");
    Assert.AreEqual("九条 林檎", vTuber.FullName());
}

このように他クラスに依存していない場合はいたって単純なテストになります。

他クラスに依存したメソッドに対するテスト

さて、ではこのようなクラスがあったとします。

public class VTuberGroup {
    public string Name { get; }
    public List<VTuber> VTubers { get; }
    public VTuberGroup(string name, List<VTuber> vTuber) {
        Name = name;
        VTubers = vTuber;
    }
    /// <summary>ランダムで一人選ぶ</summary>
    /// <returns>VTuber</returns>
    public VTuber GetRandomVTuber() {
        if (VTubers == null) return null;
        var rnd = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
        return VTubers[rnd.Next(0, VTubers.Count)];
    }
}

VTuberGroup を登録するクラスです。GetRandomName() は登録したVTuberの中から抽選で一人が選ばれるメソッドですね。さて、今回のテストはどうしますか?

[TestMethod()]
public void GetRandomNameTest1() {
    var vTuberGroup = new VTuberGroup("None", null);
    Assert.IsNull(vTuberGroup.GetRandomVTuber());
}

[TestMethod()]
public void GetRandomNameTest2() {
    var vTubers = new List<VTuber>(){
        new VTuber("林檎", "九条", "貴族"),
        new VTuber("棗", "九条", "貴族"),
        new VTuber("茘枝", "九条", "貴族"),
        new VTuber("杏子", "九条", "貴族"),
    };
    var vTuberGroup = new VTuberGroup("Avatar2.0 Project", vTubers);
    foreach (var _ in Enumerable.Range(0, 1000)) {
        Assert.IsTrue(vTubers.Contains(vTuberGroup.GetRandomVTuber())); //ちょっと強引かも…
    }
}

こちらもなんてことはないですね。他クラスに依存していたとしても問題なく書けました。ではどういうパターンだと問題が起こるのでしょうか?

返答が不確定なクラスに依存したメソッドに対するテスト

ここからが本題です。このようなクラスがあったとしましょう。

public class VTuberFestival {
    public string Name { get; }
    public VTuberGroup VTuberGroup { get; }
    public VTuberFestival(string name, VTuberGroup vTuberGroup) {
        Name = name;
        VTuberGroup = vTuberGroup;
    }
    /// <summary>
    /// ランダムで選ばれた人物の紹介文を返す。
    /// </summary>
    /// <returns>紹介文</returns>
    public string Introductory() {
        var vtuber = VTuberGroup.GetRandomVTuber();
        var honorifics = new[] { "貴族", "王族", "神" };
        if (honorifics.Contains(vtuber.Attribute)) {
            return $"{vtuber.FullName()} 様にきていただきました!";
        }
        return $"{vtuber.FullName()} ちゃんがきてくれたぞ!";
    }
}

VTuber祭典のクラスです。 Introductory() では VTuberGroup.GetRandomVTuber() で選ばれた人物に対する紹介文を返します。ただし、その VTuberやんごとなき属性を持っている場合は気を使った文章になります。さて、この Introductory() に対するテストはどう書けばいいでしょうか?きっと簡単には書けないと思うでしょう。何故なら VTuberGroup.GetRandomVTuber() の返り値が不定であるからです。テストを書くならこの VTuberGroup.GetRandomVTuber() が返す値をテスト側で決める必要がありますよね?このように、返り値が不定の場合に問題になってくるのです。通常だと API の返答値なんかが対象になるのではないでしょうか?

interface に対してテストを書く

VTuberGroup が持つメソッドをテスト側で扱いたい場合、まず interface を作り実装します。

public interface IVTuberGroup {
    public VTuber GetRandomVTuber();
}

public class VTuberGroup : IVTuberGroup {
    //--- <略> ---
}

そして使用する側のクラスではそのインターフェイスに依存するように書き換えます。

public class VTuberFestival {
    public string Name { get; }
    public IVTuberGroup VTuberGroup { get; }
    public VTuberFestival(string name, IVTuberGroup vTuberGroup) {
        Name = name;
        VTuberGroup = vTuberGroup;
    }
    //--- <略> ---
}

あとはテスト側で IVTuberGroup 型のクラスをでっちあげて引き渡してやれば、依存先のメソッドを自由に操作できるようになるって寸法です。

public class MockVTuberGroup : IVTuberGroup {
    public VTuber VTuber { get; }
    public MockVTuberGroup(VTuber vTuber) => VTuber = vTuber;
    public VTuber GetRandomVTuber() => VTuber; //GetRandomVTuber() の返答値をテスト側で操作してしまう
}

[TestMethod()]
public void IntroductoryTest1() {
    var mockVTuberGroup = new MockVTuberGroup(new VTuber("ユイ", "結目", "芸人"));
    var vTuberFestival = new VTuberFestival("Avatar2.0 Project 単独ライブイベント", mockVTuberGroup);
    Assert.AreEqual($"結目 ユイ ちゃんがきてくれたぞ!", vTuberFestival.Introductory());
}

[TestMethod()]
public void IntroductoryTest4() {
    var mockVTuberGroup = new MockVTuberGroup(new VTuber("うか", "縁", "神"));
    var vTuberFestival = new VTuberFestival("Avatar2.0 Project 単独ライブイベント", mockVTuberGroup);
    Assert.AreEqual($"縁 うか 様にきていただきました!", vTuberFestival.Introductory());
}

なお、このでっち上げたクラスの事をモックと呼びます。ちなみに C# では moq というモック作成用のフレームワークが存在し、これを使うとテスト側で別クラスを作らずに済みます。こいつぁ便利!

[TestMethod()]
public void IntroductoryTestMoq() {
    var mockVTuberGroup = new Mock<IVTuberGroup>();
    mockVTuberGroup.Setup(x => x.GetRandomVTuber()) //戻り値をでっちあげるメソッド
                    .Returns(new VTuber("らみょん", "都三代", "芸人")); //戻り値
    var vTuberFestival = new VTuberFestival("Avatar2.0 Project 単独ライブイベント", mockVTuberGroup.Object);
    Assert.AreEqual($"都三代 らみょん ちゃんがきてくれたぞ!", vTuberFestival.Introductory());
}

で、Dependency Injection って結局何なのさ?

Dependency Injection (依存性の注入) とは上記でいうところのIVTuberGroup 型のクラスをでっちあげて引き渡してやれ部分になります。例えば、VTuberFestival クラスのコンストラクタが以下のような場合引き渡してやろうにもその術がありません。

public class VTuberFestival {
    public string Name { get; }
    public VTuberGroup VTuberGroup { get; }
    public VTuberFestival() {
        var vTubers = new List<VTuber>(){
            new VTuber("ひなた", "白石", "芸人"),
            new VTuber("そにあ", "三田", "衛兵"),
            new VTuber("うか", "縁", "神"),
            new VTuber("らみょん", "都三代", "芸人"),
        };
        VTuberGroup = new VTuberGroup("ひそうら", vTubers); // 中で new しちゃうと外から渡せないよね。汎用性もないし。
    }
    //--- <略> ---
}

簡単に言えばクラス内で new せずに外から渡す事を Dependency Injection (依存性の注入)と言うのです。

まとめ

普段から Dependency Injection を意識してクラスを構築しよう!