サーチ…


ビューモデルのテスト

始める前に...

アプリケーション層に関しては、ViewModelはビジネスロジックとルールをすべて含んだクラスで、要件に応じてアプリケーションを動作させます。 UI、データレイヤー、ネイティブフィーチャー、APIコールなどの参照をできるだけ独立させることも重要です。これらにより、VMはテスト可能になります。
つまり、あなたのViewModel:

  • UIクラス(ビュー、ページ、スタイル、イベント)に依存しないでください。
  • 別のクラスの静的データを使用するべきではありません。
  • ビジネスロジックを実装し、必要なデータをUI上に準備する必要があります。
  • 依存性注入を使用して解決されるインタフェースを介して他のコンポーネント(データベース、HTTP、UI固有)を使用する必要があります。

ViewModelには、別のVMタイプのプロパティもあります。例えば、 ContactsPageViewModelは、 ObservableCollection<ContactListItemViewModel>ようObservableCollection<ContactListItemViewModel>コレクション型のObservableCollection<ContactListItemViewModel>

ビジネス要件

実装する次の機能があるとします。

As an unauthorized user
I want to log into the app
So that I will access the authorized features

ユーザーストーリーを明確にした後、以下のシナリオを定義しました。

Scenario: trying to log in with valid non-empty creds
  Given the user is on Login screen
   When the user enters 'user' as username
    And the user enters 'pass' as password
    And the user taps the Login button
   Then the app shows the loading indicator
    And the app makes an API call for authentication

Scenario: trying to log in empty username
  Given the user is on Login screen
   When the user enters '  ' as username
    And the user enters 'pass' as password
    And the user taps the Login button
   Then the app shows an error message saying 'Please, enter correct username and password'
    And the app doesn't make an API call for authentication

私たちはこれらの2つのシナリオだけにとどまります。もちろん、実際のコーディングの前にすべてを定義する必要がありますが、ビューモデルの単体テストに慣れていれば十分です。

古典的なTDDアプローチに従って、テストされる空のクラスを書くことから始めましょう。次に、テストを作成し、ビジネス機能を実装することで緑色にします。

共通のクラス

public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

サービス

あなたのビューモデルがUIとHTTPクラスを直接利用してはならないことを覚えていますか?代わりに抽象クラスとして定義し、 実装の詳細に依存しないようにする必要があります。

/// <summary>
/// Provides authentication functionality.
/// </summary>
public interface IAuthenticationService
{
    /// <summary>
    /// Tries to authenticate the user with the given credentials.
    /// </summary>
    /// <param name="userName">UserName</param>
    /// <param name="password">User's password</param>
    /// <returns>true if the user has been successfully authenticated</returns>
    Task<bool> Login(string userName, string password);
}

/// <summary>
/// UI-specific service providing abilities to show alert messages.
/// </summary>
public interface IAlertService
{
    /// <summary>
    /// Show an alert message to the user.
    /// </summary>
    /// <param name="title">Alert message title</param>
    /// <param name="message">Alert message text</param>
    Task ShowAlert(string title, string message);
}

ViewModelスタブの構築

さて、私たちはログイン画面のページクラスを持っていますが、まずViewModelから始めましょう:

public class LoginPageViewModel : BaseViewModel
{
    private readonly IAuthenticationService authenticationService;
    private readonly IAlertService alertService;

    private string userName;
    private string password;
    private bool isLoading;

    private ICommand loginCommand;

    public LoginPageViewModel(IAuthenticationService authenticationService, IAlertService alertService)
    {
        this.authenticationService = authenticationService;
        this.alertService = alertService;
    }

    public string UserName
    {
        get
        {
            return userName;
        }
        set
        {
            if (userName!= value)
            {
                userName= value;
                OnPropertyChanged();
            }
        }
    }
    
    public string Password
    {
        get
        {
            return password;
        }
        set
        {
            if (password != value)
            {
                password = value;
                OnPropertyChanged();
            }
        }
    }

    public bool IsLoading
    {
        get
        {
            return isLoading;
        }
        set
        {
            if (isLoading != value)
            {
                isLoading = value;
                OnPropertyChanged();
            }
        }
    }

    public ICommand LoginCommand => loginCommand ?? (loginCommand = new Command(Login));

    private void Login()
    {
        authenticationService.Login(UserName, Password);
    }
}

2つのstringプロパティとUIにバインドするコマンドを定義しました。このトピックでは、ページクラス、XAMLマークアップを作成し、それにViewModelをバインドする方法については何も具体的ではないため、ここでは説明しません。

LoginPageViewModelインスタンスを作成するには?

私はあなたがおそらくコンストラクタでVMを作成していたと思います。今ではわかるように、VMは2つのサービスがコンストラクタパラメータとして注入されているため、 var viewModel = new LoginPageViewModel()はできません。あなたがDependency Injectionに精通していないなら、それについて学ぶのが最良の瞬間です。この原則を知らずに従うことなく、適切なユニットテストは不可能です。

テスト

上記のユースケースにしたがっていくつかのテストを書いてみましょう。まず、新しいアセンブリ(クラスライブラリのみを作成するか、Microsoft単体テストツールを使用する場合は特別なテストプロジェクトを選択する必要があります)を作成する必要があります。 ProjectName.Testsような名前を付け、元のPCLプロジェクトへの参照を追加します。

私はこの例ではNUnitMoqを使用するつもりですが、あなたの選択肢のテストライブラリを使うことができます。彼らに特別なものは何もないでしょう。

さて、それはテストクラスです:

[TestFixture]
public class LoginPageViewModelTest
{
}

テストの作成

最初の2つのシナリオのテスト方法は次のとおりです。期待される結果1つにつき1つのテスト方法を保ち、1つのテストですべてをチェックしないようにしてください。これは、コード内で何が失敗したかについてより明確なレポートを受け取るのに役立ちます。

[TestFixture]
public class LoginPageViewModelTest
{
    private readonly Mock<IAuthenticationService> authenticationServiceMock =
        new Mock<IAuthenticationService>();
    private readonly Mock<IAlertService> alertServiceMock =
        new Mock<IAlertService>();
    
    [TestCase("user", "pass")]
    public void LogInWithValidCreds_LoadingIndicatorShown(string userName, string password)
    {
        LoginPageViewModel model = CreateViewModelAndLogin(userName, password);

        Assert.IsTrue(model.IsLoading);
    }

    [TestCase("user", "pass")]
    public void LogInWithValidCreds_AuthenticationRequested(string userName, string password)
    {
        CreateViewModelAndLogin(userName, password);

        authenticationServiceMock.Verify(x => x.Login(userName, password), Times.Once);
    }

    [TestCase("", "pass")]
    [TestCase("   ", "pass")]
    [TestCase(null, "pass")]
    public void LogInWithEmptyuserName_AuthenticationNotRequested(string userName, string password)
    {
        CreateViewModelAndLogin(userName, password);

        authenticationServiceMock.Verify(x => x.Login(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
    }

    [TestCase("", "pass", "Please, enter correct username and password")]
    [TestCase("   ", "pass", "Please, enter correct username and password")]
    [TestCase(null, "pass", "Please, enter correct username and password")]
    public void LogInWithEmptyUserName_AlertMessageShown(string userName, string password, string message)
    {
        CreateViewModelAndLogin(userName, password);

        alertServiceMock.Verify(x => x.ShowAlert(It.IsAny<string>(), message));
    }

    private LoginPageViewModel CreateViewModelAndLogin(string userName, string password)
    {
        var model = new LoginPageViewModel(
            authenticationServiceMock.Object,
            alertServiceMock.Object);

        model.UserName = userName;
        model.Password = password;

        model.LoginCommand.Execute(null);

        return model;
    }
}

さあ、いくぞ:

ここに画像の説明を入力

今、目標は、ViewModelのLoginメソッドの正しい実装を書くことです。それだけです。

ビジネスロジックの実装

private async void Login()
{
    if (String.IsNullOrWhiteSpace(UserName) || String.IsNullOrWhiteSpace(Password))
    {
        await alertService.ShowAlert("Warning", "Please, enter correct username and password");
    }
    else
    {
        IsLoading = true;
        bool isAuthenticated = await authenticationService.Login(UserName, Password);
    }
}

テストを再実行した後:

ここに画像の説明を入力

これで、コードを新しいテストでカバーし続けることができ、より安定した、回帰セーフなテストが可能になりました。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow