unittest-simple-slide



unittest-simple-slide

0 0


unittest-simple-slide

ソフトウェアテスト勉強会用発表資料「単純な単体テストだけでがんばる」

On Github masakura / unittest-simple-slide

単純な単体テストで

がんばる

かごべん ソフトウェアテスト勉強会

政倉 智

自己紹介

  • 名前 政倉 智 (まさくら とも)
  • 所属 codeArts 株式会社
  • 所属 html5j 鹿児島
  • 趣味 バイク

アジェンダ

  • 単体テスト (xUnit) おさらい
  • がんばってみる!
  • その他工夫
  • 機能テストとの対比

単体テスト (xUnit) おさらい

一番単純なパターン

// 加算メソッド
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));
    }
}

簡単でしょ!

でも、現実のコードはこんな単純じゃない!

単体テスト方法が分からない

xUnit が難しいケース

  • 外部とのデータのやり取り
    • UI からの入力値
    • データベースとのやり取り
  • 日時
  • たくさんのライブラリに依存している
  • etc...

xUnit が簡単なケース

依存関係がないメソッドの単体テストはとても簡単!

本日のお題

本日は、この難しいメソッドをどうやってテストするかをやります

がんばる!

本日のお題のアプリ

レンタルビデオの料金見積もり

  • 通常 一週間以内は 500円/日、それ以降は 300円/日
  • 新作 500円/日
  • 旧作 一週間以内は 500円、それ以降は 300円/日
  • キッズ 三日以内は 300円、それ以降は 100円/日

ソースコード

料金の計算をする部分はこんな感じ。

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();
                    }
                }
            }
        }
    }
}

どうやる?

  • WinForm ならまだインスタンス化できるけど、ASP.NET だと...
  • データベースのインスタンスを用意しないと...
    • テストデータを入れないと...
    • 他の人が勝手にテストデータ入れても困るし...
    • 他のテストがデータ書き込んじゃうし..

困った...

どうする?

  • RoR みたいに機能テストを簡単にできるシステムがあれば...
  • 今回のために頑張っても、他のプロジェクトだと使いづらい...

どうする?

  • メソッド全体をテストするのは諦める
  • 「おさらい」の「簡単にできる例」でできないか考える

どうする?

ほとんどのコードはこんな感じ。

UI から入力値を読み取る 1 を元に、データベースから値を取得する 1 と 2 を元に計算する 3 の結果を UI やデータベースに反映する

このうち、3 の計算部分はテストが簡単!

どうする?

テストが簡単な部分だけを抽出して単体テストする

テストをしやすいようソースコードを加工する 計算部分だけをメソッドの抽出する そこを単体テストをする

コードの加工

  • 今回のソースコードは 3 と 4 が混ざっているので、分離
  • ここはテストコードのご加護がないので慎重に
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 の単体テストはかなり適当で!

テストが複雑化したら小分けにする

テストコードが長くなってメンテナンスが大変になってきたら、テスト対象クラスを分割するのがおすすめ

個人的な目安は単体テストのセットアップコードが複雑化したら

単体テストを追加したい! そのためにセットアップコードを修正 なぜか他のテストが壊れる

他にいいアイデアがある人がいたら教えてください!

機能テストとの対比

機能テストのメリット

  • テストのカバー範囲が広い
  • 機能を保証できる

機能テストのデメリット

  • フレームワークの流儀に振り回される
  • 大掛かりになりやすく、気軽に始めにくい

機能テストはだめ?

  • 機能テストができるならやったほうが
    • RoR の機能テストの仕組みは素晴らしい!
    • 揮発性メモリ DB を使う方法もある
  • 機能テストをする = 仕様の策定なので、メリットはでかい

まずは単体テストから

  • まずは一人でやりやすいところから単体テストを始めましょう
  • 上司にお伺いを立てる必要はありません!
  • やっているうちに、少しずつ勘所が分かってきます
  • メリットを感じたらチームみんなで少しずつ

更にその先へ

今テストできないところもテストしたいと思ったら次へ!

まずは今回の方法で スタブやモックの利用 機能テストに踏み出す E2E テストまでやる

まとめ

  • 単純な単体テストだけでもそれなりにできるよ
  • 最初はここから少しずつやっていけばいい
    • 上司の承認が要らないレベルから!
  • 慣れてきたら他の方法を覚えると良いよ!

ご清聴ありがとうございました!

単純な単体テストで がんばる かごべん ソフトウェアテスト勉強会 政倉 智