NUnitのAssert.ThatメソッドにIsとかHasとかを入れて柔軟なテストコードを書こう

# はじめに
　NUnitもバージョン3になって、気づかないうちにメソッドが色々追加されていました。特にテストコードのモデルが新旧で区別されていたらしく、旧モデルしか書いていなかった自分への戒めとしてこの記事を残します。
　旧モデルは互換性を保つためにそのまま残されているみたいですが、コードのメンテはほとんどされなくなるみたいです。今、旧モデルでテストを書いてる人がいたら新モデル[^1]への移行も検討したほうがいいと思います。

　今回検証した環境はVisual Studio2015、NUnit 3.4.1です。記事の対象はテストコードを書いたことがある人です。

# 旧モデルと新モデル

　旧モデルと新モデルの違いは実際に見てみるのが一番早いと思います。

　一例

```csharp:旧モデル
        [Test]
        public void OldModel()
        {
            // 同値            
            Assert.AreEqual(5, 5);

            // 比較
            Assert.Greater(5, 0);  
            
            // 例外発生
            Assert.Throws<ArgumentNullException>(someMethod); 
        }
```


```csharp:新モデル
        [Test]
        public void NewModel()
        {
            // 同値            
            Assert.That(5, Is.EqualTo(5));

            // 比較
            Assert.That(5, Is.GreaterThan(0));

            // 例外発生
            Assert.That(someMethod, Throws.TypeOf<ArgumentNullException>());
        }
```

　 旧モデルでもシンプルに書けますし、あらゆるテストを書くことができます。実際、私も旧モデルでテストを書いてきました。グーグルで「NUnit 入門」で調べると「Assert.AreEqualが使えればOK」といった内容がゴロゴロ出てくるので、多くの方にとって馴染みのある形だと思います。
　では、なぜ新モデルを使うのかというと以下の3つが理由として挙げられます。

1. 複雑な条件が来た場合、旧モデルより柔軟に対応できる
2. テストコードを結果と期待値を記述するという内容に画一化できる
3. 旧モデルはサポートされなくなってきているので

　1に関してはこれから使い方を見ていくうちにわかると思います。2に関しては次章のAssert.Thatの使い方を見てもらえればわかると思います。

>　余談ですが、Assert.ThrowsはNUnit2.5からの追加要素で、Assert.Thatより後に考案されたものなのですが、既に旧モデルに分類されているようです。

# Assert.Thatの使い方

```csharp:
Assert.That(actual, constraint);
```

* **Assert.that**：新モデルのアサーション[^2]は主にこのメソッドを使用します。（というかこれしか使いません。）
* **actual**：テスト結果やテストしたいメソッドを入れます。
* **constraint(制約)**：resultに対して、期待する状態を記述します。日本語に訳すと**制約**と言います。

　基本的にこの構成さえ覚えておけば多種多様なテストコードを書くことが可能になります。


# 制約について
　制約では以下の4つのクラスを用いて、期待する結果の記述を行います。いずれもNUnit.Framework名前空間[^3]のクラスです。

* **Is**：主にオブジェクト単体に対しての制約作成に使用します。
* **Has**：コレクションの各要素にアクセスする制約作成に使用します。
* **Does**：最近[^4]追加されたクラスです。比較的新しいメソッドが格納されています。
* **Throws**：例外に関する制約を作成します。

# 様々なケースでの制約の使用例
　実際の使用例を使って、どのような記述ができるのかを見ていきます。

## 数値

```csharp:
        [Test]
        public void Numeric()
        {
            // 同値            
            Assert.That(5, Is.EqualTo(5));

            // 比較
            Assert.That(5, Is.LessThanOrEqualTo(5));
            Assert.That(5, Is.InRange(0, 100));
            Assert.That(5, Is.Positive);
        }
```

* Isクラスを利用して制約を記述しています。

## 文字列

```csharp:
        [Test]
        public void Strings()
        {
            // 空白         
            Assert.That("", Is.Empty);

            // 部分一致
            Assert.That("Penguin", Does.StartWith("Pen"));
            Assert.That("Penguin", Does.Contain("ngu"));
            Assert.That("Penguin", Does.EndWith("guin"));

            // 正規表現
            Assert.That("Penguin", Does.Match(@"P..guin"));
        }
```

* IsクラスとDoesクラスを利用して制約を記述しています。
* 部分一致を調べるメソッドはIsクラスにもありますが、現在は使用禁止となっています。

## コレクション
```csharp:
        [Test]
        public void Collection()
        {
            var list = new List<int>() { 1, 2, 3, 4, 5, 5 };
            
            // すべての要素で条件を満たす
            Assert.That(list, Has.All.LessThan(100));

            // ちょうど2つの要素で条件を満たす
            Assert.That(list, Has.Exactly(2).EqualTo(5));

            // いずれかの要素で条件を満たす
            Assert.That(list, Has.Some.EqualTo(3));
            Assert.That(list, Has.Member(3)); // ↑と同義
            Assert.That(list, Does.Contain(3)); // ↑と同義

            // いずれの要素でも条件を満たさない
            Assert.That(list, Has.None.GreaterThanOrEqualTo(100));

            // 並び順
            Assert.That(list, Is.Ordered);

            // 包含関係
            var superList = new List<int>() { 1, 2, 3, 4, 5, 5, 6, 7, 8 };
            Assert.That(list, Is.SubsetOf(superList));
        }
```

* コレクションの各要素に対しての制約を記入する場合はHasクラスを利用します。
* オブジェクト単体とみなす場合はIsクラスを利用します。

# 例外
```csharp:
        [Test]
        public void EcxeptionTest()
        {
            // 例外発生
            Assert.That(someMethod1, Throws.ArgumentException); // ArgumentExceptionは専用のプロパティが用意されている
            Assert.That(someMethod1, Throws.TypeOf<ArgumentException>());

            // メソッド定義をインラインで行う場合
            Assert.That(() => { someMethod1(); }, Throws.ArgumentException);
            Assert.That(() => { someMethod2(1, 2); }, Throws.ArgumentNullException); // 引数付きのメソッドは直接渡せないので、ラムダ式を利用する

            // プロパティ確認
            Assert.That(() => { someMethod2(1, 2); }, Throws.ArgumentNullException.With.Message.EqualTo("値を Null にすることはできません。")); // Messageは専用のプロパティが用意されている
            Assert.That(() => { someMethod2(1, 2); }, Throws.ArgumentNullException.With.Property("Message").EqualTo("値を Null にすることはできません。"));
        }

        public void someMethod1()
        {
            throw new ArgumentException();
        }

        public void someMethod2(int a, int b)
        {
            throw new ArgumentNullException();
        }
```

* 例外の制約にはThrowsクラスを利用します。
* 他のテストと異なり、第一引数が変数ではなくメソッドとなっているので注意が必要です。
* 今まで説明していませんでしたが、プロパティの状態を示す制約を書く場合はWithプロパティを利用します。

# 制約の合成
 これまで挙げた例では単一の制約のみを扱っていましたが、複数の制約を合成することが可能です。

```csharp:
        [Test]
        public void Compound()
        {
            // 積結合
            Assert.That(5, Is.GreaterThan(-10).And.LessThan(10));

            // 和結合
            Assert.That(5, Is.EqualTo(3).Or.EqualTo(5));

            // 否定
            Assert.That(5, Is.Not.EqualTo(0));
        }
```

# まとめ
　NUnitには新モデル[^1]と旧モデルがあることがわかりました。
　従来、複数のメソッドを使い分けて行っていたアサーション[^2]をAssert.Thatメソッド一つで行うことができるようになりました。

* * *

[^1]: 正確には2016年9月時点のモデルです。このモデルもいつまで続くかわからないので。
[^2]: テスト対象の状態を確認すること、検証
[^3]: 流し読みすると.NetFrameWorkと読んでしまいそうですね。
[^4]: ver2.9.1のときはなかったので最近と書きました