C#

【C#】画面のテスト方法「ViewとViewModelの考え方を身につけよう」

前回、テスト駆動開発(テストファースト)のプログラミングについて記事にしました。
テスト駆動開発はビジネスロジックのコーディングは向いていますが、画面やデータベースに触れる個所はテストコーディングがしにくいです。

なぜテストコードが書きにくいのかというと、参照やデータが変動するケースが多いからです。
例えば、画面のテキストボックスやボタン操作など、それぞれのメソッドやプロパティはprivateで閉じられているものが多いです。

これらをわざわざpublicにして単体テストのためだけに公開するのは「愚の骨頂」ですよね。
修正漏れが発生するかもしれません。

したがって、アクセス修飾子は変更しないことが大前提なのです。

じゃーテスト駆動開発で”画面”のコーディングはできないんだね?
いいや!諦めるのは早い!

では、どのようにすればいいのでしょうか。
答えは…

「View」と「ViewModel」に分けて考えます。
Viewを画面、ViewModelはView(画面)を写し取ったクラスを1対1の関係で作成します。

つまり、どういうことかというと…
イメージとしてはViewはテキストボックス、ラベル、ボタンなどの定義がありViewModelはそれらのプロパティが定義されていることです。

実際に文字だと分かりにくいので、今回も実装より解説していきます!

テスト駆動開発ができる人とできない人とでは”天と地の差”があるので、現場で実践するとちょっと一目おかれます!(マジで

実際に皆さんの周りでテスト駆動(テストファースト)で開発できる人っています??

アプリケーションを作成して解説します!

プロジェクトを作成しよう!

まず、Visual Studioを起動しましょう。
今回もエリ狐はVisual Studio 2019を使用しますが、2017でも2015でも可です。

図のように「Windowsフォームアプリケーション(.NETFrameWork)」を選択し「次へ」を押下します。

プロジェクト名称は適当で大丈夫です。
私は「FormTestSample」というプロジェクト名にして、作成しました。

すると、Form1.cs[デザイン]が表示されるので以下のプロパティ設定をしてください。

Form1のサイズ size: 400, 150
Buttonの追加 (Name):CalculationButton
Text: 計算
TextBoxの追加 ■左辺
(Name):InputTextBox1
■右辺
(Name):InputTextBox2
Labelの追加 (Name):AnsLabel
Text: —-

このようにラベルとボタンを配置してください。

次にボタンクリック時のイベント処理をします。
「Form1」に配置したボタンをダブルクリックするとボタンのクリックイベントが自動的に生成されます。

本来であれば、このままクリックイベントにビジネスロジックを書きたくなりますが、コードの汎用性が欠けてしまいます。
またメンテナンスも悪くなり、いわゆるイケてないコードになってしまうのです。

では、このようにprivateなメソッドや変数の検証はテストコードはどのように書けばいいのでしょうか。
このような場合にViewとViewModelにコード分けて実装します。

ViewとViewModel

そもそもViewってなに?

いきなりViewと聞いても分からないと思います。ここでいうViewとは「画面」を指します。
つまりForm1のテキストボックスなど、ボタンなどのクラスをViewといいます。

一方でViewModelとは、これらテキストボックスやラベルなどのViewに反映するための変数(プロパティ)を集約したクラスを指します。
では、実際にForm1のViewModelを作成していきましょう。

まずForm1の名前を「Form1View」に変更します。

次にForm1ViewModelを作成します。
アクセス修飾子はどこからでもアクセス可能としたいので「public」にします。

次にForm1ViewModelのプロパティを作成していきます。
まず、Form1View.Designer.csを開き「Windowsフォームデザイナで生成されたコード」を確認します。
※赤枠の個所がViewに定義されている要素

これらをコピーし、Form1ViewModelに貼り付けをします。
その後、コメントアウトしてください。(プロパティとして定義する変数を知るためにやります。)

最終的にこれらをデータバインド(画面と紐づけ)していきます。
次にプロパティを定義していきます。

namespace FormTestSample
{
    //private System.Windows.Forms.Label label1;
    //private System.Windows.Forms.Button CalculationButton;
    //private System.Windows.Forms.TextBox InputTextBox1;
    //private System.Windows.Forms.TextBox InputTextBox2;
    //private System.Windows.Forms.Label label2;
    //private System.Windows.Forms.Label AnsLabel;
    public class Form1ViewModel
    {
        public string InputTextBox1Text { get; set; }
        public string InputTextBox2Text { get; set; }
        public string AnsLabelText { get; set; }
    }
}

ViewModelへのテストコーディング!

ViewModelの作成ができたので、これに対してテストコードを書いていけば画面の試験ができるということになります。
さっそくテストコードを書いていきます。

ソリューションの上で右クリックをし、[追加]-[新しいプロジェクト]より単体テストプロジェクトを追加します。

参照の追加で「FormTestSample」のチェックを入れます。

「new Form1ViewModel」に赤い波線が引かれているので、[ctrl + .(ドット)]より「using」を追加してください。
この操作により、先ほど作成したViewModelを参照することができます。

ここにテストロジックを追加していきます。
まずは「初期値」をチェックしましょう。

初期値が空文字かテストしよう!

まず「空文字」であるのかテストしていきましょう。

using FormTestSample;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void シナリオ()
        {
            var viewModel = new Form1ViewModel();
            Assert.AreEqual("", viewModel.InputTextBox1Text);
            Assert.AreEqual("", viewModel.InputTextBox2Text);
            Assert.AreEqual("", viewModel.AnsLabelText);
        }
    }
}

実際にテストコードが書けたら、テストから実行します。
きちんと動作しているか「レッドバー」を確認します。

続いてなぜ「エラー」となっているのかを確認します。
このような場合に「デバッグ」をしていき、原因を特定します。

テストタブより、「直前のデバッグ」を選択します。
実際にエラーとなっているところが発見できます。

今回は空文字を想定したチェックをしていましたが、実際には1のテキストボックスには「null」が設定されていることが分かりました。
それでは、空文字となるよう、ViewModelを修正していきましょう。

テストを再度実行すると、「グリーンバー(成功)」になりました。

C#6.0以降は
public string InputTextBox1Text { get; set; } = string.Empty;
このようにプロパティ定義ができますが…
ひと昔前はこのようにかけないので

public Form1ViewModel()
{
    InputTextBox1Text = string.Empty;
}

このように「コンストラクタ」に書いていました。

余談ですが、知っておくと良いお話です!

動きのあるテストを書いてみよう!


左テキストボックスに4、右テキストボックスに7を代入、計算ボタンを押下したら結果ラベルに28が表示されるというテストケースを書いていきましょう。

画像のようにテストケースを書いてみると「viewModel.multiplication();」に赤い波線(コンパイルエラー)が表示されました。
これはForm1ViewModelにmultiplication()が実装されていないためです。

それではコンパイルエラーとなっているmultiplication()を実装しましょう。

[ctrl + .(ドット)]を押下すると、解消候補がでてくるのでメソッドの自動生成をします。
このように自動生成することで、正しい個所に実装されコーディング誤りが少なくなります。

Form1ViewModelを確認すると…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FormTestSample
{
    //private System.Windows.Forms.Label label1;
    //private System.Windows.Forms.Button CalculationButton;
    //private System.Windows.Forms.TextBox InputTextBox1;
    //private System.Windows.Forms.TextBox InputTextBox2;
    //private System.Windows.Forms.Label label2;
    //private System.Windows.Forms.Label AnsLabel;
    public class Form1ViewModel
    {
        public string InputTextBox1Text { get; set; } = string.Empty;
        public string InputTextBox2Text { get; set; } = string.Empty;
        public string AnsLabelText { get; set; } = string.Empty;

        public void multiplication()
        {
            throw new NotImplementedException();
        }
    }
}

このようにmultiplicationが実装されていることが分かりました。
それでは、ここにビジネスロジックを追加しましょう。

public void multiplication()
{
    int left = Convert.ToInt32(InputTextBox1Text);
    int right = Convert.ToInt32(InputTextBox2Text);
    AnsLabelText = (left * right).ToString();
}

右辺と左辺を掛け算して、結果をラベルプロパティに代入します。

multiplicationの実装が完了したら、再度テストを実行します。
無事、テストは成功しました。

まとめ

このようにViewとViewModelを分けることで画面の単体テストが容易にできることが分かりました。
参照やデータ変動が活発な個所はテストがし辛いですが、実装を分けることでテストファーストな開発ができるのです。

privateで閉じられているメソッドも、わざわざpublicにして単体テストする必要はありません。
コーディングを工夫することで、バグのないイケてるコードが実装できるのでお勧めです!

実は…
今回のこの画面ですが、実はデータの紐づけが未実装です;
ViewとViewModelに分けた場合紐づける作業が必須です。

この紐づけることを「データバインド」といいます。
今回は記事が長くなるので、次の記事に詳しい実装方法を記述します!合わせてチェックしてくださいね。