你的浏览器还没开启 Javascript 功能!

【読書メモ】The Art Of Unit Testing 2nd Edition - Chap3~Chap5

前回までは単体テストとは何か、必要性、そしてテストフレームワークについて書きました。

今回はクラス間の依存に対して、どういうふうに単体テストを行うことについて説明していきたいと思います。

前提

このコンテンツで扱うこと

  • クラス間の依存
  • 依存性注入
  • stubの使い方
  • mockの使い方

クラス間の依存性

我々が知っている通り、システム作りにおいてクラス同士の依存は避けられません。

  • 例)クラスAからクラスBを呼び出して何かの処理を行う
public class Aclass{
    public srting DoSomething(){
        var b = new Bclass();

        if (b.Check()){
            return "OK";
        }else{
            return "NG";
        }
    }
}

public class Bclass{
    public bool Check(){
        bool isOk = false;
        // なにかの判定処理
        return isOk;
    }
}

では、このAクラスに対して単体テストするときに、Bクラスも一緒にテストする必要があるか?

答えはYesでもありNoでもあります。

矛盾のように聞こえ混乱するかもしれませんので、ちゃんと説明すると、

  • 我々はクラスB実行結果をテストする必要ありません
  • しかし、我々はこのクラスA実行結果絶対に正しいことを担保しなければいけません

そして、このコードはクラス間が密結合であるため、そもそもクラスAに対して単体テストできません
このコードを単体テストを行うためには最初にリファクタリングが必要です。

依存性注入

依存性注入を説明する前に、密結合と疎結合を説明しなければなりません。

密結合

密結合とは、システムの構成要素間の結びつきや互いの依存関係、関連性などが強く、各々の独立性が低い状態のこと。

さっきの例でいうとクラスAの中で直接クラスBのインスタンスを生成したことが密結合に繋がります。

疎結合

逆に、疎結合とは要素間の結びつきが弱く独立性が高い状態のことを指します。

クラスAはクラスBを呼び出して処理するときは直接インスタンス生成するのではなく、インタフェースを介します。

では、コードを見ながらリファクタリングしていきましょう。

※ちなみにコードはサラで書いていますので、細かいミスがあれば大目に見てくださいww

まずはクラスBのインタフェースを定義します。そのインタフェースをクラスBに実装します。

public interface IBclass{
    bool Check();
}

public class Bclass : IBclass{
    public bool Check(){
        bool isOk = false;
        // なにかの判定処理
        return isOk;
    }
}

ここで依存性注入の登場です。

クラスAではコンストラクタからの依存性注入することによって、インタフェースを介してBclassを呼び出します。

public static void Main(string[] args){
    var b = new Bclass();
    // クラスBのインスタンスは外から注入
    // 本来であればなにかのDIフレームワークを使う
    var a = new Aclass(b);
    a.DoSamething();
}

public class Aclass{

    // クラスBのインタフェース
    private IBclass _bclass;

    // コンストラクタでクラスBのインスタンスは外から生成される
    public Aclass(IBclass bclass){
        this._bclass = bclass;
    }

    public string DoSomething(){
        if (_bclass.Check()){
            return "OK";
        }else{
            return "NG";
        }
    }
}

ASP.NET Core ではデフォルト依存性注入をサポートしています。

この一連のリファクタリングを通してクラスAとクラスBが疎結合になったところで、
クラスAに対して単体テストを行います。

Fakeオブジェクト

クラスAの単体テストを行うため、FakeオブジェクトのクラスBを用意する必要あります。
Fakeオブジェクトにはstubmockの2つの定義があります。

stub

stubとはSUTの要求に対して、あらかじめ用意した答えを返すオブジェクトのことを言います。

では、またまたコード書きながらstubについて理解していきましょう。

// クラスBのstub
public class BclassStub : IBclass{
    public bool WillBeValid { get; set; }

    public bool Check(){
        return WillBeValid;
    }
}

// クラスAの単体テスト
 [TestFixture]
public class AclassTest{
    [Test]
    public void DoSomething_BclassIsTrue_ReturnOK(){
        // Arrage
        var stub = new BclassStub();
        stub.WillBeValid = true;
        string expected = "OK";

        // Act
        var sut = new Aclass(stub);
        string result = sut.DoSomething();

        // Assert
        Assert.AreEqual(expected, result);
    }

    [Test]
    public void DoSomething_BclassIsFalse_ReturnNG(){
        // Arrage
        var stub = new BclassStub();
        stub.WillBeValid = false;
        string expected = "NG";

        // Act
        var sut = new Aclass(stub);
        string result = sut.DoSomething();

        // Assert
        Assert.AreEqual(expected, result);
    }
}

このようにstubを利用して、クラスBを意識しなくてもクラスAの実行結果が正しいことを担保できます。

stubの特徴をまとめると

  • 手動でコーディングしなければならない
  • 処理の細かい動作を把握しないと、適切なstubオブジェクトが書けません
  • 手動でコーディングしているので、テストケースの失敗する可能性が低い

mock

mockもstubと同じく偽物のオブジェクトですが、手動でコーディングするのではなく、
mockフレームワークによって自動生成されます。

.NETで有名なmockフレームワークはMoq,NSubstituteなどいろいろありますが、今回はNSubstituteを用いて実装していきたいと思います。

// クラスAの単体テスト
 [TestFixture]
public class AclassTest{
    [Test]
    public void DoSomething_BclassIsTrue_ReturnOK()
    {
        // Arrage
        // インタフェースからクラスBのmockを生成
        var mock = Substitute.For<IBclass>();
        // mockの戻り値をセット
        mock.Check().Returns(true));
        string expected = "OK";

        // Act
        var sut = new Aclass(mock);
        string result = sut.DoSomething();

        // Assert
        Assert.AreEqual(expected, result);
    }


    [Test]
    public void DoSomething_BclassIsFalse_ReturnNG()
    {
        // Arrage
        // インタフェースからクラスBのmockを生成
        var mock = Substitute.For<IBclass>();
        // mockの戻り値をセット
        mock.Check().Returns(false);
        string expected = "NG";

        // Act
        var sut = new Aclass(mock);
        string result = sut.DoSomething();

        // Assert
        Assert.AreEqual(expected, result);
    }
}

stubと比べmockは自分でいちからコーディングする必要ありませんので、手軽にFakeオブジェクトを作れます。

mockの特徴

  • mockは簡単、手軽、効率的
  • 自動生成のため、手動のstubと比べ自分で制御しにくい
  • 結果を自分で完全に制御できないから、テストケースの失敗する可能性が高い

まとめ

今回はクラス間の依存性に対してどうやって単体テストするのかについて説明しました。

stubとmockをどっちを使ったほうがいいと聞かれても、正直ケース・バイ・ケースとしか答えようがありません。

私が尊敬する先輩にその質問する、

雰囲気で使えばいいんじゃない?」といういい感じの答えが帰ってくるくらいですww

まぁ、その話はさておき・・・

単体テストにおいては正しい一見識を持つことが非常に大事です。

間違った考えで単体テストを書いてしまうと、書けば書くほど単体テストのメリットを享受できず、どんどん落胆します。

最終的に単体テストを書くことを諦めてしまうでしょう。私も一回それで挫折したことがありました。

私の考えは絶対に正しいであることは担保できませんが、少なくとも私が単体テストに対して悩みに悩んで熟慮した結果、このコンテンツが生まれました。

もし、何か意見があれば遠慮なくご連絡ください。

出典

The Art of Unit Testing: with examples in C#

コード

Github Repository