On Github masakura / unittest-simple-slide
かごべん ソフトウェアテスト勉強会
政倉 智
// 加算メソッド
public static class Calc
{
    public static int Add(int x, int y)
    {
        return x + y;
    }
}
[TestFixture]
public class CalcTest
{
    [Test]
    public void TestAdd()
    {
        // 1 + 2 を計算して 3 なのを確認!
        Assert.That(Calc.Add(1, 2), Is.EqualTo(3));
    }
}
依存関係がないメソッドの単体テストはとても簡単!
本日は、この難しいメソッドをどうやってテストするかをやります
レンタルビデオの料金見積もり
料金の計算をする部分はこんな感じ。
protected void Calculate_Click(object sender, EventArgs e)
{
    using (var connection = CreateConnection())
    {
        connection.Open();
        using (var command = connection.CreateCommand())
        {
            var videoId = int.Parse(VideoList.SelectedValue);
            command.CommandText = "select Type from Videos where Id = @Id";
            var idParamter = command.CreateParameter();
            idParamter.ParameterName = "Id";
            idParamter.DbType = DbType.UInt32;
            idParamter.Value = videoId;
            command.Parameters.Add(idParamter);
            using (var reader = command.ExecuteReader())
            {
                if (reader.Read())
                {
                    var type = (VideoType) reader.GetInt32(0);
                    var number = int.Parse(Number.Text);
                    switch (type)
                    {
                        case VideoType.Normal:
                            if (number <= 7)
                            {
                                Price.Text = (7*500 + (number - 7)*300).ToString();
                            }
                            else
                            {
                                Price.Text = (number*500).ToString();
                            }
                            break;
                        case VideoType.New:
                            Price.Text = (number*500).ToString();
                            break;
                        case VideoType.Old:
                            if (number <= 7)
                            {
                                Price.Text = 500.ToString();
                            }
                            else
                            {
                                Price.Text = (500 + (number - 7)*300).ToString();
                            }
                            break;
                        case VideoType.Kids:
                            if (number <= 3)
                            {
                                Price.Text = 300.ToString();
                            }
                            else
                            {
                                Price.Text = (300 + (number - 3)*100).ToString();
                            }
                            break;
                        default:
                            throw new InvalidOperationException();
                    }
                }
            }
        }
    }
}
ほとんどのコードはこんな感じ。
UI から入力値を読み取る 1 を元に、データベースから値を取得する 1 と 2 を元に計算する 3 の結果を UI やデータベースに反映するこのうち、3 の計算部分はテストが簡単!
テストが簡単な部分だけを抽出して単体テストする
テストをしやすいようソースコードを加工する 計算部分だけをメソッドの抽出する そこを単体テストをするswitch (type)
{
    case VideoType.Normal:
        if (number <= 7)
        {
            Price.Text = (7*500 + (number - 7)*300).ToString();
        }
        else
        {
            Price.Text = (number*500).ToString();
        }
        break;
一時変数を利用してフォーム依存部分を排除
// レンタル料金を保持する一時変数
int result;
switch (type)
{
    case VideoType.Normal:
        if (number <= 7)
        {
            result = 7 * 500 + (number - 7) * 300;
        }
    // ... 省略
    default:
        throw new InvalidOperationException();
}
Price.Text = result.ToString();
public static int CalcFee(VideoType type, int number)
    switch (type)
    {
        case VideoType.Normal:
            if (number <= 7)
            {
                return 7 * 500 + (number - 7) * 300;
            }
            // ...
}
protected void Calculate_Click(object sender, EventArgs e)
{
    // ... 省略
    Price.Text = CalcFee(type, number);
    // ... 省略
}
[Test]
public void TestCalculate()
{
    var result = [Default].Calculate(VideoType.New, 8);
    Assert.That(result, Is.EqualTo(4000));
}
抽出したメソッドを新しいクラスに移動するのがおすすめ
public static class RentalCalc
{
    public static int CalcFee(VideoType type, int days)
    {
        // ...
    }
}
レンタル料金の計算メソッドを作るけど、実装を適当にする
public int static CalcFee(VideoType type, int days)
{
    // ToDo 計算は適当
    Log.Error("ToDo ちゃんと実装するんだぞ!");
    return days * 300;
}
こういうのはテストしにくい!
public bool IsInRange
{
    get {
        var now = DateTime.Now;
        return StartDateTime <= now && now < EndDateTime;
    }
}
こうすれば簡単に!
// こっちをテストする
public static bool GetIsInRange(DateTime start, DateTime end, DateTime now)
{
    return start <= now && now < end;
}
public bool IsInRange
{
    get {
        return GetIsInRange(start, end, now);
    }
}
クラス全体で使いたいときはこんな感じで!
// 普通のコンストラクタ
public Foo():this(() => DateTime.Now) {}
// テスト時のみに使うコンストラクタ
public Foo(Func<DateTime> getNow)
{
    _getNow = getNow;
}
public bool IsInRange
{
    get {
        var now = _getNow();
        return StartDateTime <= now && now < EndDateTime;
    }
}
同じようなのを作る場合のおすすめ
単体テストとセットで普通に作る Template Method パターンで基底クラスを抽出 2 を継承して新しい横展開を行う 3 の単体テストはかなり適当で!テストコードが長くなってメンテナンスが大変になってきたら、テスト対象クラスを分割するのがおすすめ
個人的な目安は単体テストのセットアップコードが複雑化したら
単体テストを追加したい! そのためにセットアップコードを修正 なぜか他のテストが壊れる今テストできないところもテストしたいと思ったら次へ!
まずは今回の方法で スタブやモックの利用 機能テストに踏み出す E2E テストまでやる