Поиск…


Тестирование моделей просмотра

Прежде чем мы начнем ...

С точки зрения уровней приложений, ваш ViewModel - это класс, содержащий всю бизнес-логику и правила, которые делают приложение таким, каким оно должно соответствовать требованиям. Также важно сделать его максимально независимым, уменьшая ссылки на пользовательский интерфейс, уровень данных, встроенные функции и вызовы API и т. Д. Все это делает вашу виртуальную машину проверкой.
Короче говоря, ваша ViewModel:

  • Не следует зависеть от классов пользовательского интерфейса (просмотров, страниц, стилей, событий);
  • Не следует использовать статические данные других классов (насколько это возможно);
  • Следует внедрить бизнес-логику и подготовить данные, которые должны быть использованы для пользовательского интерфейса;
  • Должны использоваться другие компоненты (база данных, HTTP, пользовательский интерфейс) через интерфейсы, разрешаемые с помощью Injection Dependency.

Ваша ViewModel может иметь свойства других типов виртуальных машин. Например, ContactsPageViewModel будет иметь тип сбора, такой как 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

Мы останемся только с этими двумя сценариями. Конечно, должно быть гораздо больше случаев, и вы должны определить их все до фактического кодирования, но для нас достаточно хорошо ознакомиться с модульным тестированием моделей просмотра.

Давайте рассмотрим классический подход 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);
    }
}

Мы определили два свойства string и команду для привязки к пользовательскому интерфейсу. Мы не будем описывать, как создать класс страницы, разметку XAML и связать ViewModel с этим в этом разделе, поскольку они не имеют ничего конкретного.

Как создать экземпляр LoginPageViewModel?

Я думаю, вы, вероятно, создали виртуальные машины только с конструктором. Теперь, поскольку вы можете видеть, что наша виртуальная машина зависит от 2-х услуг, которые вводятся в качестве параметров конструктора, так что не просто сделать var viewModel = new LoginPageViewModel() . Если вы не знакомы с Dependency Injection, это лучший момент, чтобы узнать об этом. Надлежащее модульное тестирование невозможно без знания и соблюдения этого принципа.

тесты

Теперь давайте напишем некоторые тесты в соответствии с перечисленными выше случаями использования. Прежде всего вам нужно создать новую сборку (только библиотеку классов или выбрать специальный проект тестирования, если вы хотите использовать инструменты тестирования модулей Microsoft). Назовите это что-то вроде ProjectName.Tests и добавьте ссылку на ваш оригинальный проект PCL.

В этом примере я собираюсь использовать NUnit и Moq, но вы можете использовать любые тестовые библиотеки по вашему выбору. Ничего особенного с ними не будет.

Хорошо, это тестовый класс:

[TestFixture]
public class LoginPageViewModelTest
{
}

Письменные тесты

Ниже приведены методы тестирования для первых двух сценариев. Попробуйте сохранить 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;
    }
}

И здесь мы идем:

введите описание изображения здесь

Теперь цель состоит в том, чтобы написать правильную реализацию для метода Login ViewModel и все.

Реализация бизнес-логики

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