ASP.NET 5 MVC6 と xUnit で ModelState の単体テストを書こうのコーナーです。
「ModelStateの単体テストコード、うまくできませぬ」と言われたのでコードを見せてもらったら、コントローラーをnewしてなんちゃらしていた事件があったので、書いておこうと思いました。
Environment
- Visual Studio 2015 Enterprise Update1
- ASP.NET and Web Tools 2015 (RC1 Update 1)
- xUnit(2.2.0-beta1..あたり)
Overview
ASP.NET5 MVC6 のプロジェクトを作って、Depenency Injection(DI)について、4種類がどう異なるのかを見たい今日この頃です。
いつものことですが、Visual Studioは英語版なので日本語の正確な項目名わかりません...涙。
それにしてもASP.NET5が 「ASP.NET Core 1.0」に名称変わるのでタイトルもそう書きたいところですが、名称変更となるRC2、今月(2016/2)リリース予定がTBDに戻ってたので、もう少しこの名称で書いておきましょう。。。
1. テスト対象のASP.NET5 MVC6のプロジェクトを作る
まず、プロジェクトをざっくり作ります(説明雑...)。
beachside.hatenablog.com
ご不明の場合は、「1. ASP.NET5 MVC6 のプロジェクト作成」あたりでさらりと作れます。
テストの対象となるサンプルのModelを、以下の感じで作ります。
public class DemoModel { [Required] public string Id { get; set; } [StringLength(8)] public string Name { get; set; } [Range(1,12)] public int Number { get; set; } }
各種Attributeをつけているので、
- Idプロパティは、は入力必須
- Nameプロパティは、文字数の最大が8文字
- Numberプロパティは、範囲が1~12の間のint
という制約です。
コントローラーで呼ばれるイメージは、以下です。
public IActionResult Post(DemoModel model) { if (!ModelState.IsValid) { // エラー時のレスポンスかえす処理を実装するはず } // 正常時の処理を実装....するはず // 以下略 }
2. 単体テストプロジェクトの追加
ソリューション配下の適当なところ(どこにするかはプロジェクト次第...)で右クリックして、[Add] > [New Project]をクリックしてプロジェクトを追加します。
余談ですが、
私は、ソリューションの直下(srcと同じ場所)にtestsというフォルダを作ってtestプロジェクトを追加してます。プロジェクト毎にフォルダ作ってその中にテストプロジェクトとプロダクトのプロジェクトを入れるパターンもgithubとかで見ますね。
本題に戻り、xUnit Testのテンプレートを選択してプロジェクト名を入力し、プロジェクトを追加しましょう。
次に、
xUnitのプロジェクトからプロダクトのプロジェクトがみえるようにします。
プロジェクトのすぐ下にある[Reference] > [Add Reference]をクリックして参照を追加します。追加する参照は、[Projects]の中にあるものです(先ほどModelを追加したプロジェクト)。
これで下準備は完了。
3. 単体テストメソッドの作成
単体テストの作成ですが、コントローラーをnewしてパラメータを渡すようなことはやめましょう(むしろ、それはあかん)。
Validateの処理を直接行って結果を検証するイメージです。
サンプル第一弾のテスト内容は、入力必須のはずのIdプロパティに入力値がない場合にします。
値をnullにしてます。
(stringなので、nullとstring.EmptyやWhiteSpaceの場合もあったりなかったりしますがまずはnullで...)
コード書いてしまいます。
public class DemoModelTest { [Fact] public void ModelState_invalid_IdProperty() { var model = new DemoModel() { //テスト対象となる値をプロパティに入れる...今回はIdプロパティがnullの想定 }; var context = new ValidationContext(model, null, null); var result = new List<ValidationResult>(); var validationResult = Validator.TryValidateObject(model, context, result, true); Assert.False(validationResult); Assert.False(result.SelectMany(r => r.MemberNames).Any(r => "Id".Equals(r))); }
コードをざっくり説明すると、
テスト用のModelと、Modelに応じたValidationContextを生成して、Validator.TryValidateObjectメソッドを実行。
Validationの結果は、boolの戻り値、そのエラー内容は、パラメーターとして渡した「List
そして、以下のAssertをしてます。
- Validator.TryValidateObjectメソッドの戻り値は、false
- エラーを出力したプロパティ名に「Id」が含まれている(リフレクションでプロパティ名とってこいよとかは...サンプルなので)
(何をテストするかと何をAssertするかは、プロジェクトによってちゃんと考えましょう...)
ついでにNameプロパティの8文字以内の制約に違反するテストしてみましょう。
テスト毎にValidationContextやValidationResultリストをnewするのが辛いので、Tupleの戻り値を持つジェネリックの「ValidateModelState」メソッドを作ってすっきりさせてみました。
[Fact] public void ModelState_invalid_NameProperty() { var model = new DemoModel() { Id = "b1", Name = "beachside" }; var result = ValidateModelState(model); Assert.False(result.Item1); Assert.True(result.Item2.SelectMany(r => r.MemberNames).Any(r => "Name".Equals(r))); } private Tuple<bool, List<ValidationResult>> ValidateModelState<TModel>(TModel model, bool validateAllProperties = true) { var result = new List<ValidationResult>(); var validationResult = Validator.TryValidateObject(model, new ValidationContext(model, null, null), result, validateAllProperties); return new Tuple<bool, List<ValidationResult>>(validationResult, result); }
ValidateModelStateメソッドは、戻り値のTupleの1つ目がValidator.TryValidateObjectメソッドの結果(bool)、2つ目がList
Assertの内容は、最初のサンプルとほぼ同じです。
Item1、Item2は辛いので...次期のC#7で話にでてる名前付きのタプルできるといいですね...
ということで、ModelのAttributeに頼って、ごりっごりにテストメソッド書いてプロダクトがハッピーになるといいです。
おしまい。