僕とリファクタリングとNUnitとNSubstitute

はぅ君さんが、NUnit(と、それを動かすためのJenkins)に手を出されたらしい。

NUnitでテストを書く際、セットで使うとちょっと幸せになれる(かもしれない)NSubstituteというライブラリがあります。

というわけで本日は、はぅ君さんがご存知なさそうだったNSubstituteの便利さについてご紹介します。

レガシーコード改善ガイド (Object Oriented SELECTION)』を題材に、リファクタリングNUnitを使用したテスト → NUnit + NSubstituteを使用したテストをステップバイステップで書いていきます。

なお、本記事の執筆にあたり使用した環境は、Visual C# 2013 + NUnit 2.6.4 + NSubstitute 1.8.0 です。

ナイスな例題

ある販売管理システムにSaleというクラスがあるとします。このクラスのScanというメソッドは、顧客が購入しようとしている品物のバーコードを受け取ります。Scanメソッドが呼び出されると、Scanオブジェクトは読み取った品物の名前と価格とをレジの画面に表示します。

f:id:tercel_s:20141220220320p:plain

このクラスをテストして、正しい文字列が表示されるかどうかを確認するにはどうすればよいでしょうか。

ちなみにこのScan()メソッド、データベースへのアクセスやレジ画面のAPI呼び出しが深く埋め込まれているモンスターメソッドであると仮定しましょう。

    public class Sale
    {
        /// <summary>
        /// バーコードを受け取り、
        /// 品物の名前と価格をレジの画面に表示します。
        /// </summary>
        /// <param name="barcode">バーコード</param>
        public void Scan(string barcode)
        {

            //   :
            //   :
            // とてつもなく長いメソッドにつき省略
            //   :
            //   :

        }
    }

このままではNUnitアサーションが使えません。

なにはともあれリファクタリング

もし、画面の更新が行われている箇所を特定して別クラス(クラス名は引用元に従いArtR56Display)に切り出すことができたら(下図参照)、テストしやすくなるかもしれません。

f:id:tercel_s:20141220220517p:plain

このときArtR56Displayクラスの役割は、渡された引数lineをそのままレジの画面に表示するだけです。外から見たシステム全体の動きは全く変わっていません。

さらに、このArtR56Displayクラスを、テストのときだけ擬装オブジェクトにすり替えできるよう、Interfaceを一皮かぶせてあげます。

f:id:tercel_s:20141220220710p:plain

    public interface IDisplay
    {
        void ShowLine(string line);
    }

Saleの中で、IDisplayは以下のように使われるでしょう。

    public class Sale
    {
        private IDisplay display;

        public Sale(IDisplay display)
        {
            this.display = display;
        }

        public void Scan(string barcode)
        {
            //   :
            // 前略
            //   :

            // 画面表示部
            string itemLine = item.Name + " " + item.Price;
            display.ShowLine(itemLine);

            //   :
            // 後略
            //   :
        }
    }

画面表示部を丸ごとIDisplayに委譲してあげるイメージですね。

NUnitでテストを書こう!

ここからテスト用のプロジェクトに書くコードです。

まず、ArtR56Displayの擬装クラスFakeDisplayの実装です。

    public class FakeDisplay : IDisplay
    {
        private string _lastLine = string.Empty;
        public string LastLine
        {
            get{ return _lastLine; }
            private set{ _lastLine = value; }
        }

        public void ShowLine(string line)
        {
            _lastLine = line;
        }
    }

こいつには、直前にShowLine()で出力した文字列を、LastLineプロパティに保持する隠し機能を持たせます。これで、LastLineにアクセスすると、出力文字列を取得することができ、期待値との突合ができるようになる寸法です。

NUnitを用いたテストコードを以下に示します。

    [TestFixture]
    public class SaleTest
    {
        [Test]
        public void TestDisplayAnItem()
        {
            FakeDisplay display = new FakeDisplay();
            Sale sale = new Sale(display);
            sale.Scan("1");

            Assert.AreEqual(@"ブルーレット置くだけ ¥1,000-", display.LastLine);
        }
    }

……ちなみにMicrosoftが密かに推奨しているMSTestを利用した場合のテストコードは以下のようになります。ほとんど一緒ですな。

    [TestClass]
    public class SaleTest
    {
        [TestMethod]
        public void TestDisplayAnItem()
        {
            FakeDisplay display = new FakeDisplay();
            Sale sale = new Sale(display);
            sale.Scan("1");

            Assert.AreEqual(@"ブルーレット置くだけ ¥1,000-", display.LastLine);
        }
    }

テスト自体は一応これでもよいのですが、擬装クラスを書いたせいでテストコードが増えてしまってたいへんです。

もう少しスマートにならないものでしょうか。

NSubstituteを使ってテストコードを減らす

ここからちょっとした小技で、NSubstituteというライブラリを使います。NuGetからインストール可能なので導入もベリーイージーです。

FakeDisplayを作らなくてもよくなるのでテストコードがすっきりします。

以下に、NSubstituteの使用例を示します。

Sale.Scan()経由でIDisplay#ShowLine()が呼び出されたときに、IDisplay#ShowLine()の受け取ったパラメータが期待値と一致するかどうかを確認するテストコードです。

    [TestFixture]
    public class SaleTest
    {
        [Test]
        public void TestDisplayAnItem()
        {
            var substitute = Substitute.For<IDisplay>();
            Sale sale = new Sale(substitute);
            sale.Scan("1");

            // IDisplay#ShowLineに渡された引数が期待値と一致するかを確認
            substitute.Received().ShowLine(@"ブルーレット置くだけ ¥1,000-");
        }
    }

これで、わざわざ《テストのためだけの余分なクラス》を作る手間が一つ減るので、ちょっと幸せになる気がします。

なお、期待値を「ルーレット置くだけ ¥1,000-」に変更してわざとテストを失敗させた結果がこちら。

f:id:tercel_s:20141220214811p:plain

めでたし。

いやめでたくない。

NSubstituteのそのほかの使い方は公式のドキュメントに書いてあるんじゃないでしょうかね(投げやり

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.