# WindowsFormsで遊戯王を交えながらMVVMっぽく作って単体テストもやってみる _**最終更新日:2022/03/06**_ なぜ遊戯王を混ぜてしまったのか、その答えはデュエルの中に見つけるしかない。 {{toc}} ## 必要スキルなど このページを読み進めるにあたって必要になるスキル。 ・C# WindowsFormsで簡単なフォームアプリが作れるレベル。 ・MVVM 軽く説明するので知らなくてもいい。 ・遊戯王 説明しないのでデュエルリンクスやマスターデュエル等で基礎を学んでください! 遊戯王 デュエルリンクス https://www.konami.com/yugioh/duel_links/ja/ 遊戯王 マスターデュエル https://www.konami.com/yugioh/masterduel/jp/ja/ ## MVVMとは 画面(View)と画面と処理の中継(ViewModel)とその他処理(Model)に分けるソフトウェアアーキテクチャのこと。 色々分けてるので保守性とか開発生産性とかいいらしい。 WPFやUWPなどのXAMLベースのプラットフォームで使われている。 Model View ViewModel - Wikipedia https://ja.wikipedia.org/wiki/Model_View_ViewModel 遊戯王で例えると・・・ ・普通に作る フィールドにカードがくっついてて、効果処理もフィールドに依存している。 そのため、効果を変えようとすると、フィールドに依存しているため、書き換えや他への使い回しが大変。 ・MVVMで作る フィールドにカードがくっついてるところまで同じだが、効果処理自体はプレイヤーが管理している。 効果を発動するのにプレイヤーを経由する手間があるが、プレイヤーを変えることで効果も書き換えられる。 プレイヤーを他のデッキに使いまわしたり、テスト用のプレイヤーを用意することもできる。 これで君もMVVMのファンになったかな? ~~余計わからんわ!~~ ## MVVMっぽくする必要について ・画面と処理の分割 画面は画面で、処理は処理で分割できるので、それぞれ分担して作業ができる。 一人プロジェクトでは恩恵が薄そうだが、処理の部分を他のアプリへ移植・・・しやすいかもしれない! ・単体テスト なんか単体テストってWindowsFormsを参照してるプロジェクトは対象外みたい。 なので処理部分だけ.NET Coreでプロジェクトを作り、単体テストを実行できるようにする。 (そこまで調べてないので、もしかしたらWindowsFormsでも対象にできるやり方があるかもしれない) ・画面を変えられる 画面は画面で分けてあるので、プラットフォームによって画面を変えられる。 まぁそれをちゃんとやってくれる賢いやつがすでにありますけどね! ## ここまで読んで MVVMのとこで説明したXAMLベース(WPFやUWP)で、Prism(XAML用のフレームワーク)とか使えばいいじゃんと思ったアナタ! **その通りです!** このページは閉じて、そのスキルを磨きましょう、リンクも貼っていおきます! ユニバーサル Windows プラットフォーム (UWP) アプリとは - UWP applications | Microsoft Docs https://docs.microsoft.com/ja-jp/windows/uwp/get-started/universal-application-platform-guide Prism Library https://prismlibrary.com/ ここではWindowsFormsでMVVMっぽく化に挑戦する変な解説と思ってください。 MVVMっぽくと言ってるのは予防線・・・ ~~そもそもそこまで理解してないし・・・~~ ## 解説用サンプルソース ここからダウンロードしてください。 attachment:RunRunGridLiner-develop.zip Visual Studio 2019、.NET 5、Null許容有効化のソリューションになります。 中身は読み込んだ画像にグリッドを描く簡単なフォームアプリです。 (´・ω・`)らんらん♪ また描画処理でよく使うSystem.DrawingがWindows依存で.NET Coreでは使用できないため、外部パッケージの「ImageSharp」使用している。 これは.NET Coreで動くイメージライブラリで、どのプラットフォームでも動作する、System.Drawingとは勝手が違うので注意。 ImageSharpの簡単な解説はこちら →[[ImageSharp|ライブラリ解説/ImageSharp]] Six Labors : ImageSharp https://sixlabors.com/products/imagesharp/ ## プロジェクト解説 ソリューションに含まれてる各プロジェクトとそのソースの解説。 ### YusakuClassLibrary イントゥ・ザ・ヴレインズしそうな名前のこのプロジェクトは、ViewとViewModelの橋渡し役をするためのライブラリ。 #### IPlayMakerインターフェース ViewModelのクラスに実装させるためインターフェース。 ViewModelが実装済みかそうでないかの判断ぐらいにしか使わないw 由来はまんま、彼ならなんでもできるしね。 #### IChainEffectインターフェース Viewの処理を実装するために使用するインターフェース。 ダイアログ表示などの処理をこのインターフェースでカプセル化し、ViewModelからは処理の実行と結果の取得のみ行えるようにする。 これによってViewModelはWindowsFormsに依存せず、ダイアログ表示などを行えるようになる。 また単体テストにおいて、WindowsFormsに絡む処理をテスト用に実装することもできる。(この例は単体テストの解説で説明) 由来は誘発効果チェーン、ViewModelのそれぞれの処理をエフェクト(効果)としているので、その中でチェーンして効果を発動させるイメージ。 ~~これ遊戯王知らないとマジで意味わらかんな~~ #### CardStatusクラス ViewModelで定義するプロパティ用クラス。 Viewでバインドし、値変更を検知するためINotifyPropertyChangedを実装している。 ちなみにこれはPrismに入ってた同じようなクラスの丸パクリで、使いやすいように調整したものがこれ。 由来はカードの攻守レベルなどのステータスから。 ### RunRunGridLinerCore .NET Coreのみで各プラットフォームに依存しないコアなプロジェクト。 単体テストの対象になる。 #### ViewModel/MainViewModelクラス メイン画面のViewModel。 今回は画面がメインの1つしかないが、複数ある場合はここに作るんじゃないかな? ViewModelでは画面に絡まない動作処理を実装する。 ちなみにちゃんとしたMVVMでは、ViewModelはあくまでViewとModelの橋渡し役で、動作処理はModelに書くらしい。 ~~だめじゃん~~ 動作処理はエフェクトと呼び、この処理内部でさらに画面に絡む処理がある場合は、先程のIChainEffectで実装された処理を呼び出す。 由来は効果発動、どれもコントロールから呼び出される起動効果のイメージ。 ### RunRunGridLiner WindowsFormsで作られた画面があり、起動されるスタートアッププロジェクト。 #### View/MainViewクラス 今回のアプリのメイン画面、View。 ViewModelのプロパティのバインドと、コントロールのイベントがメイン。 コントロールのイベントから、対応するViewModelの動作処理を呼び出す。 ViewModelがIPlayMakerインターフェースで定義されているが、これはViewModelが未実装でも画面自体をコーディングし実行できるようにするため。 そのため、バインドやイベントの時にViewModelを呼び出す際はdynamicを使用している。 すでにViewModelが実装済みだったり、とりあえずで空メソッドを作ってあるなら必要ない、こんなこともできるよ!というサンプル。 #### View/ChainEffect ここのフォルダにはIChainEffectで実装した処理が格納されている。 このアプリではファイル選択ダイアログ表示とファイル保存ダイアログ表示がIChainEffectで実装されている。 ### RunRunGridLinerCore_Test 単体テストプロジェクト。 ViewModelが格納されているRunRunGridLinerCoreが対象、詳細は後述。 ## 処理の流れ ここから画面からどのように処理が流れていくのか、画像を読み込んで表示する処理をソースを交えて解説。 ### View 最初はコントロールのイベントが発生し、ViewModelのエフェクトが呼び出される。 ファイル選択ダイアログはチェーンエフェクトで実装したものを引数で渡している。 ``` csharp /// /// 画像ファイル読み込みメニューアイテムクリック時イベント /// /// イベント送信元 /// イベントパラメータ private void MenuItemReadImageFile_Click(object sender, EventArgs e) { try { ((dynamic)m_mainVM).ReadImageFileEffect(new OpenFileChainEffect(this)); } catch (RuntimeBinderException ex) { Console.WriteLine(ex.ToString()); } catch (Exception ex) { MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); } } ``` 本当ならメッセージボックス処理もエフェクトとチェーンエフェクトで実装するべきだが、面倒なのでやってないよ! ### ViewModel 処理の最初でチェーンエフェクトが発動(Activate)される。 引数で渡されたのはファイル選択ダイアログを表示するものなので、ここでファイルが選択される。 選択されれば「ActivateResult.Success」が返るので処理を続行。 GetValueメソッドに選択したファイルのパスが入っているので、そこから画像ファイルを読み込む。 最後にメモリストリームを通してバイト配列にし。描画イメージプロパティへ設定する。 プロパティで使用しているカードステータスはプロパティ変更通知インターフェースが実装してあるため、画面でバインドしているピクチャーボックスに自動的に値が反映される。 異なる型でのプロパティバインド実装については、汎用テクニック集で解説。 →[[汎用テクニック集#異なる型でのプロパティバインド|汎用テクニック集/異なる型でのプロパティバインド]] ``` csharp /// /// ファイル選択ダイアログを表示し、選択した画像ファイルを読み込みます。 /// /// ファイル選択ダイアログ表示チェーンエフェクト public void ReadImageFileEffect(IChainEffect chainEffect) { /* ファイルが選択されていない場合、終了 */ if (chainEffect.Activate() != ActivateResult.Success) { return; } /* 画像ファイル読み込み */ m_readImage = Image.Load((string)chainEffect.GetValue()); /* バイト配列として取得 */ using MemoryStream stream = new(); m_readImage.Save(stream, BmpFormat.Instance); stream.Flush(); DrawImage.Value = stream.ToArray(); } ``` ### 処理の流れまとめ 基本的に他の処理もこの流れになる。 処理はViewModelのエフェクト、間の処理はチェーンエフェクト、画面への値反映はカードステータス経由、これだけ。 当初の目的のMVVMっぽくなってると思う・・・思いたい。 ## 単体テスト ここからは単体テストの作成と実施を解説。 単体テストできるのは色々あるけど、今回はxUnitを使用する。 **ここから先は遊戯王要素はありませんのでご安心ください!** ちなみにxUnitは公式サイトと簡単な導入はあるが、リファレンスはないらしい・・・ Home > xUnit.net https://xunit.net/ Where is xUnit API reference / in detail documentation? (it is question, not Issue) · Issue #1018 · xunit/xunit · GitHub https://github.com/xunit/xunit/issues/1018 ### 拡張機能の準備 Visual Studioの拡張機能でxUnitの拡張機能を2つ入れる。 xUnitのテストを自動生成するジェネレータと、xUnitテストプロジェクトのテンプレートの2つ。 (画像の上2つのやつ) {{thumbnail(xunit_01.png, size=800, title=拡張機能)}} ### 単体テストプロジェクト作成 手っ取り早い方法が、対象のソース上で右クリックし、「単体テストの作成」を選択、ここからテストプロジェクトごと作る方法。 これだとプロジェクトからテストクラスまで一括で作ってくれる。 すでに手動でテストプロジェクトを作成済みでも、そこに追加することもできる。 {{thumbnail(xunit_02.png, size=641, title=単体テストの作成)}} 手動の場合は新規プロジェクト追加から「xUnitテストプロジェクト」を追加する。 {{thumbnail(xunit_03.png, size=800, title=新しいプロジェクトを追加)}} ### 単体テスト作成 Assertを使って、正しい値になるかチェックするように作成する。 ・Assert.True、Assert.False 値が真偽かどうか。 ・Assert.Null、Assert.NotNull 値がNullかどうか。 ・Assert.Equal、Assert.NotEqual 値が同じかどうか。 他にも色々とある、下記を参照。 xUnitで使用できるAssertion - Symfoware https://symfoware.blog.fc2.com/blog-entry-1063.html テストメソッドには目的に応じて属性を付与する。 [Fact]は引数なしのメソッドに、[Theory]は引数ありのメソッドに付与。 ``` csharp /// /// 初期化チェック /// [Fact] public void MainViewModel_CheckInit() { using MainViewModel mainVM = new(); Assert.Null(mainVM.DrawImage.Value); Assert.Equal(100, mainVM.GridWidth.Value); Assert.Equal(100, mainVM.GridHeight.Value); Assert.Equal(0, mainVM.GridX.Value); Assert.Equal(0, mainVM.GridY.Value); Assert.Equal(10, mainVM.LineWidth.Value); Assert.Equal(Color.Black.ToHex(), mainVM.LineColor.Value); Assert.Equal(255, mainVM.LineAlpha.Value); } ``` [MemberData]は独自のクラスを引数に渡すようにできる属性で、これ1つで事前に定義したテストデータを使用できる。 テストパターンを増やすときに、テストメソッド本体を変更せずにテストデータだけ変更すれば良くなるので便利だと思う。 ``` csharp public static IEnumerable ReadImageFileEffect_Success_TestData() { yield return new object[] { new TestChainEffect(ActivateResult.Success, "TestData/Input/ReadImage001.png") }; yield return new object[] { new TestChainEffect(ActivateResult.Success, "TestData/Input/ReadImage002.png") }; yield return new object[] { new TestChainEffect(ActivateResult.Success, "TestData/Input/ReadImage003.png") }; } /// /// 画像ファイル読み込み/成功 /// [Theory] [MemberData(nameof(ReadImageFileEffect_Success_TestData))] public void ReadImageFileEffect_Success(IChainEffect readChainEffect) { using MainViewModel mainVM = new(); mainVM.ReadImageFileEffect(readChainEffect); IEnumerable imageArray = LoadImage((string)readChainEffect.GetValue()).ToArray(); Assert.True(mainVM.DrawImage.Value.SequenceEqual(imageArray)); } ``` テストメソッドの命名はMicrosoftのドキュメントが参考になる。 このプロジェクトだと「メソッド名+テスト内容」にしてる、あんま長い名前だと見づらいしね。 単体テストを記述するためのベスト プラクティス - .NET | Microsoft Docs https://docs.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-best-practices ### 単体テスト実施 テストクラス上で右クリックし、「テストの実行」を選択するとテストエクスプローラが表示され、テストが実行される。 問題なければすべてチェックされOKになる。 {{thumbnail(xunit_04.png, size=800, title=テストエクスプローラ)}} ### 単体テストまとめ このように1度テストを作ってしまえば、誰がやっても同じ内容で素早く実施できるため、使いこなせれば便利。 ただ単体テストが実施しやすいようにコードを書き、単体テスト自体も書く必要があるため、ちょっと手間に感じる人も居るかも? ちなみに予めテストを作成し、そのテストに合わせてプログラムを作っていくことを「テスト駆動開発」という。 テスト駆動開発 - Wikipedia https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA ## 最後のまとめ このページ書くのが10年ぐらい遅い! まぁそれでも画面と処理分割して単体テストまで持っていけたので良かったと思う(小並感) --- [[csharp:wiki|メインページに戻る]]