15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

単体テストと副作用 - 第3話 DIとモック

Last updated at Posted at 2019-03-06

本シリーズ

単体テストと副作用 - 第1話 はじまり
単体テストと副作用 - 第2話 オブジェクト指向
単体テストと副作用 - 第3話 DIとモック
単体テストと副作用 - 第4話 副作用の回避
単体テストと副作用 - 第5話 オブジェクト指向への反乱

前回のあらすじ

  • オブジェクト指向は、責務を意識して機能を分離しよう。
  • 副作用を回避する方法があるらしい?
  • 女神さまの宿題は、テストフレームワーク、テストダブル、依存性の注入。

テストフレームワーク

テストフレームワークについては、前回も使ったお(・ω・)
C#においては、私は「xUnit.net」が好き。
基本的な使い方に関しては、以下の記事を参照されたし。

xUnit.net でユニットテストを始める

私がいつも便利に使っているのは、前回も紹介した MemberData 属性だお。
前回のテストコードを再掲する。

TestableSample2/TestableSample2.Test/Test_TypedValue.cs
using System.Collections.Generic;
using Xunit;
using TestableSample2.App;

namespace TestableSample2.Test
{
    public class Test_TypedValue
    {
        // Theoryは引数ありテスト
        // MemberDataのメソッドから引数を取得する
        [Theory]
        [MemberData(nameof(Constructor_Data))]
        public void Constructor(TypeKind expected, string value)
        {
            var typedValue = new TypedValue(value);

            Assert.Equal(value   , typedValue.Value);
            Assert.Equal(expected, typedValue.TypeKind);
        }

        // 上記テストメソッドに引数を渡す
        public static IEnumerable<object[]> Constructor_Data()
        {
            return new[] {
                new object[] { TypeKind.Null  , ""        },
                new object[] { TypeKind.Null  , "null"    },
                new object[] { TypeKind.Bool  , "true"    },
                new object[] { TypeKind.Bool  , "false"   },
                new object[] { TypeKind.Number, "0.0"     },
                new object[] { TypeKind.Number, "789"     },
                new object[] { TypeKind.Number, "-12.345" },
                new object[] { TypeKind.String, "a123"    },
                new object[] { TypeKind.String, "$#@?+"   }
            };
        }
    }
}

この属性に指定するメソッドが返す値は、対象のテストメソッドへ引数として順次渡される。
上記の例では、2つの引数でできた object 配列を9個渡すことにより、テストが9回走ることになる。

『…xUnit.netには他にも便利な特徴がありますが…まあいいでしょう…。これを駆使してテストコードを書くのです…』

テストダブル

テストダブルは、テスト対象以外のオブジェクトを代替し、単体テストを可能にするもの。
テストダブルには、色々な種類があるみたい。

  • テストスタブ
    • 実際のオブジェクトの代替として、テスト対象から呼び出され、値を返す。
  • テストスパイ
    • 実際のオブジェクトの代替として、テスト対象から呼び出され、引数を記録する。
  • モックオブジェクト
    • 実際のオブジェクトの代替として、テスト対象から呼び出され、引数を検証する。
  • フェイクオブジェクト
    • 実際のオブジェクトの代替として、より簡易な実装で動作する。
  • ダミーオブジェクト
    • テスト対象の引数として渡される、機能しない代替。

これらは独立していない場合もある(テストスタブ機能を持ったモックオブジェクトなど)。

実際にテストを書いてゆくとどれも使うことになるようだけど、特にモックオブジェクトは恩恵が大きい。
でも、自分で実装するのは面倒だね…。
他の言語ではテストフレームワークに組み込まれていたりするけど、C#の場合、「Moq」というライブラリを導入すれば、便利なモックオブジェクトが使えるようになる。
使い方については、この記事で大体把握できる。

Moq : Mocking Framework for .NET

使用例を示すお。

HogeTest.cs
using Moq;
using Xunit;

// モックにしたいインターフェイス
public interface IHoge
{
    int  Fuga(int x);
    void Piyo(int x);
}

public class HogeTest
{
    [Fact]
    public void TestFuga()
    {
        var mock = new Mock<IHoge>();

        // Fugaメソッドの呼び出しをセットアップする
        // 常に「引数 + 1」を返すように設定
        mock.Setup(hoge => hoge.Fuga(It.IsAny<int>()))
            .Returns<int>(x => x + 1);
        
        var hogeObj = mock.Object;

        Assert.Equal(  2, hogeObj.Fuga(1));
        Assert.Equal(  3, hogeObj.Fuga(2));
        Assert.Equal(101, hogeObj.Fuga(100));
    }

    [Fact]
    public void TestPiyo()
    {
        var mock = new Mock<IHoge>();

        var hogeObj = mock.Object;
        
        hogeObj.Piyo(1);

        hogeObj.Piyo(2);
        hogeObj.Piyo(2);

        hogeObj.Piyo(100);
        hogeObj.Piyo(100);
        hogeObj.Piyo(100);

        // Piyoメソッドの呼び出しを検証する
        mock.Verify(hoge => hoge.Piyo(  1), Times.Once);
        mock.Verify(hoge => hoge.Piyo(  2), Times.AtLeastOnce);
        mock.Verify(hoge => hoge.Piyo(100), Times.Exactly(3));
        mock.Verify(hoge => hoge.Piyo(  0), Times.Never);
        mock.Verify(hoge => hoge.Piyo(It.IsAny<int>()), Times.Exactly(6));
    }
}

「Moq」が提供するスタブとモックの機能により、テストで使用される代替オブジェクトを簡単に作成できるね(・ω・)

『…スタブとモックによって…オブジェクトを切り離すことが可能になります…。そのことが…副作用の回避に直接関係してきます…』

依存性の注入

依存性の注入(DI : Dependency Injection)とは、あるオブジェクトが依存しているオブジェクトを、クラス内で生成するのではなく、クラス外から注入することにより、型に関する依存性を消し去る手法だお。
注入の方法にはいくつかある。

  • コンストラクタ注入
    • コンストラクタの引数に渡して注入
  • セッター注入
    • セッターで注入
  • インターフェイス注入
    • メソッドの引数に渡して注入

ライブラリ無しでも簡単にできるけど、ライブラリを使うと色々便利みたい。
以下は、「Unity」というDIライブラリを使用した例。
「Unity」というとゲームエンジンが思い起こされるが、全くの別物だ(´・ω・`)

Program.cs
using System;
using Unity;

interface IHoge
{
    void Hello();
}

class Hoge : IHoge
{
    public void Hello()
    {
        Console.WriteLine("Hello, world!");
    }
}

class Fuga
{
    // IHogeインターフェイスに注入する
    // Hogeクラスへの依存が存在しない
    [Dependency]
    public IHoge Hoge { get; set; }

    public void Hello()
    {
        Hoge.Hello();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // DIコンテナの設定
        var container = new UnityContainer();
        container.RegisterType<IHoge, Hoge>();
        container.RegisterType<Fuga>();

        // Fugaインスタンスの生成
        var fuga = container.Resolve<Fuga>();

        fuga.Hello();
    }
}

Dependency 属性のついたプロパティに、あらかじめ設定したクラスのインスタンスが自動的に渡される。
Fuga クラスを見ると、Hoge クラスに全く依存していないことが分かる。
IHoge インターフェイスに外部から注入されるからだね。

『…Fuga クラスは Hoge クラスに全く依存していないので…Fuga クラスをテストする際…Hoge クラスをテストダブルで置き換えることが可能になります…』

考察

『…これらが"3種の神器"です…。ポイントは…DIコンテナを使用して…テスト時にモックオブジェクトを注入することができるという点です…』

そうか(・ω・)!
副作用を隔離し、インターフェイスの裏に隠してしまえば、DIによってモックオブジェクトにすり替えることが可能だお!
テスト時にはモックを、本番時には本物のコードを注入することで、テストの時だけ副作用を回避できる。

『…そうです…。あとはそれを形にするだけです…。論よりコード…。手を動かすのです…』

~ 次回へ続く ~

ここまでのまとめ

  • 副作用を持つクラスの代替となるモックオブジェクトを、DIで外部から注入してやれば、副作用を回避できる?

本シリーズ

単体テストと副作用 - 第1話 はじまり
単体テストと副作用 - 第2話 オブジェクト指向
単体テストと副作用 - 第3話 DIとモック
単体テストと副作用 - 第4話 副作用の回避
単体テストと副作用 - 第5話 オブジェクト指向への反乱

15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?