Visual Studio 2017で単体テストもしくはTDDする

Visual Studio 2017でC#で開発してる開発者で、開発時にMSTestを使って単体テストを作成したり、コードを書くときにTDDしたりは、すっかり開発風景に定着していると思います。ただ、たまに作成方法を忘れるので、確認を兼ねてVisua Studio 2017 Community版で、単体テストしたり、TDDするための環境の構築の作り方をまとめたいと思います。

いちおう今回の作成環境を載せておきます。どのバージョンでもほぼ同じなのであんまり関係ないですが念のため。

  • 環境
    • VisualStudio2017 Community || Pro - 15.4.3
    • C# 6.0
    • .NET 4.6.2

まずはソリューションとプロジェクトの作成する

まず、ソリューションを作成します。初期配置のプロジェクトはリストからクラスライブラリ (.NET Framework)を選択し、ソリューションの名前を「UnitTest.Sample」、プロジェクト名を「UnitTest.Target」とします。

f:id:Takachan:20171110020313p:plain

単体テストプロジェクトの追加する

次に単体テストプロジェクトを追加します。ソリューションエクスプローラー上のソリューションの項目を右クリックして、コンテキストメニューから、追加 > 新しいプロジェクト を選択します。

f:id:Takachan:20171110020352p:plain

新しいプロジェクトの追加ダイアログで右側のツリーから、Visual C# > テスト の項目を選択して(もうなってるかもしれません)、「単体テスト プロジェクト」を選択し、名前を、「UnitTest」として[OK]を押します。

f:id:Takachan:20171110020359p:plain

新規プロジェクトがソリューションに追加されていると思います。初期状態ではUnitTest1.csが配置されています。ファイルを開くと1つだけ空のテストが配置されています。

f:id:Takachan:20171110020433p:plain

また、この状態でビルドして「テストエクスプローラー」(表示されていない場合、メニューの テスト > ウィンドウ > テスト エクスプローラ で表示できます)を確認すると記述したテストの一覧が確認できます。

f:id:Takachan:20171110020415p:plain

最後にプロジェクトの参照からテスト対象プロジェクトを追加しておきます。そうでないとテスト対象のプロジェクトが見えません。

f:id:Takachan:20171110020440p:plain

テスト対象がある、UnitTest.Targetにチェックを入れて[OK]を選択します。

f:id:Takachan:20171110021009p:plain

テストを記述する、TDDを始める

仕様ですが、「文字列2つを入力して、数字なら各々を足して返却値を文字列で応答する」プログラムを作成したいと思います。

TDDはおおむね、「実装より先にテストを作る」、「存在しない操作のテストを書く」ことが先にきて「メソッドの中身を実装する」はテストを作った後に行います。また、「頻繁にテストを実行する」の行為を含みます。

前述の通り、この時点ではクラスも操作も無いのです。が、こうできたらいいなと考えながらやりたいことを記述し、最後に結果があっているかどうかを確認するためアサーションで結果の確認を書いていきます。。

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        var calc = new StringCalclator(); // やりたいこと
        string ans = calc.Add("1", "2");

        Assert.AreEqual("3", ans); // 実行結果の確認
    }
}

で、VSのコードアシスト機能で"StringCalclator"の部分にカーソルを当てて電球マークをクリック(もしくはShift + Ctrl + F10を同時押し)し、メニューから「class 'StringCalclator' を生成する」を選択します。

f:id:Takachan:20171110021017p:plain

自動でファイルの末尾にクラスが追加されます。

f:id:Takachan:20171110021024p:plain

で、次はAddにエラーが出るので同じく、電球マークを選択し、メソッドを生成しますを選択してメソッドを自動で追加します。この時、アクセス修飾子はpublicに変えておきましょう。

f:id:Takachan:20171110021115p:plain

(引数の名前は、自動で適当な名前が付与されるので後で自分の好きなようにで変更してください)

次に、動作を確認するために、TestMethod1()のスコープの中で、右クリックからコンテキストメニューを呼び出して、「テストの実行」を選択します。

f:id:Takachan:20171110021141p:plain

当然ですが、以下のように、テストが失敗して原因が「テスト エクスプローラー」上に表示されます。

f:id:Takachan:20171110021150p:plain

次に、メソッドの中身を実装します。ついでに、当然予想される使用で、「もし引数が数字じゃない場合、例外を発生させる」の仕様とテストも追加します。

また、自動生成したクラスを、UnitText.Targetプロジェクトに移動します。ここは自動ではやってくれないので手作業で行います。

f:id:Takachan:20171110021158p:plain

移動すると、UnitTest1.csでエラーが出るのでエラーが出てる箇所にカーソルを当てて、電球ボタンをクリック(もしくはShift + Ctrl + F10同時押し)して、メニューの「using UnitTest.Target;」を選択してエラーを訂正します。

f:id:Takachan:20171110021212p:plain

また例外を確認するためにテストに以下を追加します。[ExpectedException(typeof(ArgumentException))]という属性を追加することで、メソッドから例外が発生する旨をテスト項目として追加できます。

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void IlligalString_Arg_1()
{
    var calc = new StringCalclator();
    string ans = calc.Add("a", "2"); // 第1引数に無効な文字列

    Assert.AreEqual("3", ans);
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void IlligalString_Arg_2()
{
    var calc = new StringCalclator();
    string ans = calc.Add("1", "a"); // 第2引数に無効な文字列

    Assert.AreEqual("3", ans);
}

ここまで追記したら、テストエクスプローラか「すべて実行」を選択します。全て処理が成功して、グリーンな状態になると思います。

f:id:Takachan:20171110021233p:plain

以上を繰り返しながら、「テストを先に作成」してから「中身は後から実装する」ことで、「後から動作を検証できる」し「使い手の立場に立った操作の提供」が「効率的」に実装できるようになると思います。

テストメソッドの仕様

基本的にテストメソッドのやり方はMSDNの単体テストの基本というページに記載されていますが、しょっちゅうアドレスが変わってリンク切れになるので中身を一部抜粋したいと思います。

実行時間を指定する(タイムアウトを指定する)

以下の通り指定すると、指定ミリ秒以上実行時間がかかるとテストが失敗します。

[TestMethod]
[Timeout(2000)]  // ミリ秒指定で時間を指定します。
public void My_Test()
{
    ...
}

テスト結果の確認方法

基本的にAssertクラスのメソッドに予想される結果と、実際のオブジェクトの値を設定して結果を確認していきます。ちょっと成否が紛らわしいので、分からなくなったらMSDNを参照したほうがいいかもしれません。

// 値が等しいとテストに成功する
Assert.AreEqual(予測値, 実行結果);
// オブジェクト参照が同じときにオブジェクトが成功する
Assert.Same(予測値, 実行結果);
// 値が等しくない時にテストが成功する
Assert.AreNotEqual(予測値, 実行結果);

// falseだとテストが成功する
Assert.IsFalse(実行結果);
// trueだとテストが成功する
Assert.IsTrue(実行結果);

// 実行結果が予測される型の場合テストが成功する
Assert.IsInstanceOfType(実行結果、予測される型);

// Nullじゃない時にテストが成功する
Assert.IsNotNull(実行結果);
// Nullの場合テストが成功する
Assert.IsNull(実行結果);

// 呼ぶとテストが必ず失敗する
Assert.Fail("メッセージ");

テスト前準備、後処理

テストクラス内のテストを開始する前や後に実行したい処理がある場合、以下をテストクラス内に指定します。

[TestClass]
public class UnitTest1
{
    // クラス内のテストの開始前に呼ばれる
    [ClassInitialize]
    public static void ClassInitialize(TestContext context)
    {
        // クラスの前処理
    }

    [TestMethod]
    public void Test()
    {
    }

    // クラス内のテストが終わった時に呼ばれる
    [ClassCleanup]
    public static void ClassCleanup()
    {
        // クラスの後処理
    }
}

アセンブリ単位(つまりテストプロジェクト内のテスト)が開始される前や後に実行したい処理がある場合以下をテストクラス内に指定します。どこに指定してもクラス内をテスト開始ませに検索して実行してくれるので記述場所は問いません。

[TestClass]
public class UnitTest1
{
    // プロジェクトのテスト開始前(≒テスト全体)に呼ばれる
    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext context)
    {
        // テストプロジェクト全体の前処理
    }

    [TestMethod]
    public void Test()
    {
    }

    // プロジェクト内の内のテストが全部終わった時に呼ばれる
    [AssemblyCleanup]
    public static void AssemblyCleanup()
    {
        // テストプロジェクト全体の後処理
    }
}

簡単でしたが、単体テストとTDDの説明は以上となります。