C#でドメイン駆動開発。今回でデータを保存するときのMoqを使った
テストコードの書き方を中心に解説していきます。
データ保存処理はどんなシステムにもよく出現するしょりなので
じっくり解説していきます。
保存(Save)
これまではデータ表示のシナリオを中心に見てきましたが,
もう一つのよくあるシナリオとして,データ保存に関して見ていきましょう。
計測日時と計測値を手入力して保存する画面を作成します。
それではTestsプロジェクトにMeasureSaveViewModelTestという
テストクラスを作成し,ViewModelsにMeasureSaveViewModelクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 |
[TestClass] public class MeasureSaveViewModelTest { [TestMethod] public void 計測登録_シナリオ() { var viewModel = new MeasureSaveViewModel(); } } public class MeasureSaveViewModel { } |
MeasureSaveViewModelTestクラスに登録画面の初期値をテストしていきます。
1 2 3 4 5 6 7 8 |
[TestMethod] public void 計測登録_シナリオ() { var viewModel = new MeasureSaveViewModel(); viewModel.MeasureDate.Is("2017/01/03 13:00:00".ToDate()); viewModel.MeasureValue.Is(""); viewModel.UnitLabel.Is("m/s"); } |
MeasureDateの初期値はシステム日付を想定しています。
といっても値は固定しないとテストにならないので適当な日時を入れています。
MeasureValueは空文字。
UnitLabelは単位を表示しています。
今回は計測値にテキストボックスを想定しているので,
単位を同時に出すと制御が面倒になります。
数値部分と単位のエリアを分けるために単位ラベルのプロパティを作成しました。
コンパイルエラーがでているので,
MeasureSaveViewModelを次のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class MeasureSaveViewModel : ViewModelBase { private DateTime _measureDate = DateTime.Now; private string _measureValue = ""; public DateTime MeasureDate { get { return _measureDate; } set { SetProperty(ref _measureDate, value); } } public string MeasureValue { get { return _measureValue; } set { SetProperty(ref _measureValue, value); } } public string UnitLabel => "m/s"; } |
3つのプロパティを追加し,
データバインドできる形にしています。
UnitLabelは固定値なので文字列を入れています。
ただこうするとドメイン要素がViewModelにこぼれだしているので
MeasureValueクラスで単位を一括管理できるように変更します。
【MeasureValue】
1 2 3 4 5 6 7 8 |
public static readonly string UnitName = "m/s"; public MeasureValue(float value) { Value = value; } public float Value { get; } public string DisplayValue => Math.Round(Value, 2) + <strong>UnitName</strong>; |
UnitNameを読み取り専用で静的変数で宣言し単位を代入します。
【MeasureSaveViewModel】
1 |
public string UnitLabel => <strong>Domain.ValueObjects.</strong><strong>MeasureValue</strong><strong>.UnitName</strong>; |
ViewModelではそのUnitNameをバインドしておくことで
ドメイン要素の流出が防げます。
それではテストをしてみましょう。
NGになります。結果を見るとMeasureDateの値がNGなことがわかります。
システム日付をテストする場合はどうすればよいでしょうか。
Moqを使用すると次のように書くことができます。
1 2 3 4 5 6 7 8 9 10 |
[TestMethod] public void 計測登録_シナリオ() { <strong>var viewModelMock = new Mock<MeasureSaveViewModel>();</strong> <strong> viewModelMock.Setup(x => x.GetDateTime()).Returns("2017/01/03 13:00:00".ToDate());</strong> <strong> var viewModel = viewModelMock.Object;</strong> viewModel.MeasureDate.Is("2017/01/03 13:00:00".ToDate()); viewModel.MeasureValue.Is(""); viewModel.UnitLabel.Is("m/s"); } |
viewModelMockはMeasureSaveViewModelという
ViewModelをMockの型に指定しています。
そしてviewModelはviewModelMockのObjectを代入しています。
こうしておくとMeasureSaveViewModelの一部の関数を
テストロジックに変更することができます。
今回はMeasureSaveViewModelに
GetDataTimeというシステム一時を返却する関数を作り,
それがテストロジックと切り替わるようにしました。
ただしテストロジックと切り替える場合は
関数を「public」+「virtual」にしておく必要があります。
これはMoqがメソッドを上書きできるようにするためです。
1 2 3 4 |
public virtual DateTime GetDateTime() { return DateTime.Now; } |
この関数をMoqでSetupしておけば,
テスト時はRetutnsで指定した値が返却されるためテストが成功します。
これで実行時はシステム日時,テスト時は指定した日時でテストすることができます。
それではテストを実行しましょう。
まだNGです。
MeasureSaveViewModelのMeasureDateがDateTime.Nowを設定しているため
コンストラクタでGetDataTimeの値を設定するように変更します。
1 2 3 4 5 |
private DateTime _measureDate; public MeasureSaveViewModel() { _measureDate = GetDateTime(); } |
これでテストが通りました。
これでSave画面が起動したときのテストは完了しました。
次にSave処理のテストをします。
Saveボタンが押されたときにデータベースに保存するシナリオにします。
テストコードを追加します。
1 2 3 |
viewModel.MeasureDate = "2017/01/03 12:50:00".ToDate(); viewModel.MeasureValue = "1.23456"; viewModel.Save(); |
最初の2行はユーザーがテキストボックスに
値を入力したことをシミュレートしています。
3行目はSaveボタンが押されたときの処理です。
この場合まだなにも値を検証するテストコードが書かれていません。
Saveされた値はどのようにテストすべきでしょうか?
Saveボタンが押された後は,
データベースに保存しに行きます。
データベースへのアクセス処理は
Infrastructureに記述するためテストコードの対象外です,
であればInfrastructureに渡される直前の値をチェックすれば実際に
データベースに保存される値に近いものがテストできるはずです。
ドメインとインフラの境目は何でしょうか?
リポジトリーですね。
この場合は計測値なのでMeasuresテーブルに保存します。
よってIMeasureRepositoryの値をチェックすればよいことになります。
IMeasureRepository を次のように定義を増やします。
1 2 3 4 5 6 |
public interface IMeasureRepository { MeasureEntity GetLatest(); IReadOnlyList<MeasureEntity> GetData(); <strong> </strong><strong>void</strong><strong> Save(</strong><strong>MeasureEntity</strong><strong> entity);</strong> } |
Save関数の定義を追加しました。
引数はMeasureEntityです。
Save時はこのMeasureEntityを検証します。
MeasureSaveViewModelにSaveメソッドを生成し,
Measuresテーブルに接触するためにIMeasureRepositoryの変数を保持します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<strong> private</strong> <strong>IMeasureRepository</strong><strong> _measureRepository;</strong> <strong> </strong>private DateTime _measureDate; private string _measureValue = ""; <strong> </strong><strong>public</strong><strong> MeasureSaveViewModel(</strong><strong>IMeasureRepository</strong><strong> measureRepository)</strong> <strong> {</strong> <strong> _measureRepository = measureRepository;</strong> <strong> _measureDate = GetDateTime();</strong> <strong> }</strong> <strong>public</strong> <strong>void</strong><strong> Save()</strong> <strong> {</strong> <strong> </strong><strong>var</strong><strong> measureValue = </strong><strong>Convert</strong><strong>.ToSingle(MeasureValue);</strong> <strong> </strong><strong>var</strong><strong> entity = </strong><strong>new</strong> <strong>MeasureEntity</strong><strong>(</strong> <strong> </strong><strong>Guid</strong><strong>.NewGuid().ToString(),</strong> <strong> MeasureDate, </strong> <strong> measureValue);</strong> <strong> _measureRepository.Save(entity);</strong> <strong> }</strong> |
まず,IMeasureRepositoryを宣言し,
コンストラクタで設定します。
Saveメソッドではユーザー手入力項目のプロパティをもとに
MeasureEntityを作成し,
リポジトリのSaveを呼び出しています。
現状MeasureValueは空文字の可能性がありますが一旦無視します。
MeasureIdはGuidのためNewGuidを生成して文字列を指定しています。
このGuidもMoqをしようしてテスト用の値の検証ができますが,
ここではこのままとてしておきます。
また,コンパイルをするとMeasureFakeでエラーがでるので
メソッドを自動生成してそのままとしておきます。
【MeasureFake】
1 2 3 4 |
public void Save(MeasureEntity entity) { throw new NotImplementedException(); } |
MeasureSaveViewModelTestがNGになったままなのでテストコードを追加します。
[TestMethod]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void 計測登録_シナリオ() { <strong> </strong><strong>var</strong><strong> measureMock = </strong><strong>new</strong> <strong>Mock</strong><strong><</strong><strong>IMeasureRepository</strong><strong>>();</strong> var viewModelMock = new Mock<MeasureSaveViewModel>(<strong>measureMock.Object</strong>); viewModelMock.Setup(x => x.GetDateTime()).Returns("2017/01/03 13:00:00".ToDate()); var viewModel = viewModelMock.Object; viewModel.MeasureDate.Is("2017/01/03 13:00:00".ToDate()); viewModel.MeasureValue.Is(""); viewModel.UnitLabel.Is("m/s"); viewModel.MeasureDate = "2017/01/03 12:50:00".ToDate(); viewModel.MeasureValue = "1.23456"; viewModel.Save(); } |
これでテストが成功します。
ただSaveメソッドは呼ばれていますが,
保存する値の検証はまだ何もできていません。
そこでviewModel.Save()の前に次のように書きます。
1 2 3 4 5 6 7 8 9 10 |
viewModel.MeasureDate = "2017/01/03 12:50:00".ToDate(); viewModel.MeasureValue = "1.23456"; <strong>measureMock.Setup(x => x.Save(</strong><strong>It</strong><strong>.IsAny<</strong><strong>MeasureEntity</strong><strong>>()))</strong> <strong> .Callback<</strong><strong>MeasureEntity</strong><strong>>(saveValue => </strong> <strong> {</strong> <strong> </strong><strong>//saveValue.MeasureId.Is("?");</strong> <strong> saveValue.MeasureDate.Value.Is(</strong><strong>"2017/01/03 12:50:00"</strong><strong>.ToDate());</strong> <strong> saveValue.MeasureValue.Value.Is(1.23456f);</strong> <strong> });</strong> viewModel.Save(); |
measureMock.Setup~の部分は
ViewModelのSaveメソッドで
IMeasureRepositoryのSaveメソッドが
呼ばれたときの値を検証しています。
これで保存する直前の値が確認することができます。
今回MeasureIdは生のGuidにしていてテストできないので,
コメントアウトしています。
GetDataTimeと同じ要領で任意の値をテストできます。
今回は新規登録のみなのでテストの必要性はあまりありませんが,
登録と更新があるケースではこのGuidの値を検証する必要性があるでしょう。
この状態でテストをするとテストが成功します。
本当にSaveのSetupが動作しているか検証してみましょう。
検証する値を999fなどにすればNGになるはずです。
1 |
<strong>saveValue.MeasureValue.Value.Is(999f);</strong><strong> //NGになるかどうかの検証</strong> |
これでNGになりました。
今度は少し変わった検証としてMeasureSaveViewModelのSaveメソッドの_measureRepository.Save(entity)の部分をコメントアウトしてみましょう。
1 2 3 4 5 6 7 8 9 |
public void Save() { var measureValue = Convert.ToSingle(MeasureValue); var entity = new MeasureEntity( Guid.NewGuid().ToString(), MeasureDate, measureValue); //_measureRepository.Save(entity); コメントアウト } |
これでテストするとテストが成功します。
_measureRepository.Save(entity)が呼ばれていないためテストコードの
saveValue.MeasureValue.Value.Is(999f)も実施されずに成功してしています。
こういったテスト未通過を検出する関数がMoqにあります。
それを呼んでみましょう
measureMock.VerifyAll()の部分です。
これがあるとmeasureMockでテスト未通過があるとNGになります。
1 2 3 4 5 6 7 8 9 |
measureMock.Setup(x => x.Save(It.IsAny<MeasureEntity>())) .Callback<MeasureEntity>(saveValue => { //saveValue.MeasureId.Is("?"); saveValue.MeasureDate.Value.Is("2017/01/03 12:50:00".ToDate()); saveValue.MeasureValue.Value.Is(999f); }); viewModel.Save(); <strong>measureMock.VerifyAll();</strong> |
確認したら検証用に変更したしたコードをもとに戻し,
テストが成功していることを確認しましょう。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
私が参考にしたドメイン駆動開発本はこちらで紹介しています
勉強家の方は是非ご覧ください↓
C#を正しい3層構造で造れてますか?
- C#でドメイン駆動開発とテスト駆動開発で保守性の高いプログラムを書く!1
- C#でドメイン駆動をする前に良いコードと悪いコードの定義を理解しよう!2
- C#でドメイン駆動開発をやるうえで採用するアーキテクチャーに関して3
- C#でドメイン駆動開発をするうえで意識べきロジックの2つの種類④
- C#でドメイン駆動開発 アーキテクチャーの実装とテスト駆動での書き方を解説5
- C#でドメイン駆動開発 外部との接触箇所にRepositoryを使う!⑥
- C#でドメイン駆動開発Moqを使ったテスト駆動開発のやり方を解説!⑦
- C#でドメイン駆動設計UIとインフラストラクチャーを実装する方法⑧
- C#でドメイン駆動開発 Entityの書き方と使い方とテスト駆動!⑨
- C#でドメイン駆動開発ValueObjectでプログラムの複雑さを取り除く⑩
- C#でドメイン駆動開発 ViewModelの中に明細項目があるときの実装⑪
- C#でドメイン駆動開発 データ保存時のロジックのMoqの書き方!⑫
- C#でドメイン駆動開発【DDD】エラー処理とExceptionの書き方!⑬
- C#でドメイン駆動開発をするうえで私が参考にした書籍ランキング!