WPF MVVM Light unit testing ViewModels

Я не регулярно с шаблоном MVVM, и это в основном мой первый раз с ним.

То, что я делал ( "обычный" WPF), создавал мои представления с уровнем Business и, возможно, datalayer (который обычно содержит мои объекты, созданные службой или Entity Framework).

Теперь после некоторых игр я создал стандартный шаблон из MVVM Light и сделал это:

Поисковик

public class ViewModelLocator
{
    static ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        if (ViewModelBase.IsInDesignModeStatic)
        {
            SimpleIoc.Default.Register<IUserService, DesignUserService>();
        }
        else
        {
            SimpleIoc.Default.Register<IUserService, IUserService>();
        }

        SimpleIoc.Default.Register<LoginViewModel>();
    }

    public LoginViewModel Login
    {
        get
        {
            return ServiceLocator.Current.GetInstance<LoginViewModel>();
        }
    }
}

Вход в систему ViewModel:

public class LoginViewModel : ViewModelBase
{
    private readonly IUserService _userService;

    public RelayCommand<Object> LoginCommand
    {
        get
        {
            return new RelayCommand<Object>(Login);
        }
    }

    private string _userName;
    public String UserName
    {
        get { return _userName; }
        set
        {
            if (value == _userName)
                return;

            _userName = value;
            RaisePropertyChanged("UserName");
        }
    }

    /// <summary>
    /// Initializes a new instance of the LoginViewModel class.
    /// </summary>
    public LoginViewModel(IUserService userService)
    {
        _userService = userService;

        _closing = true;
    }

    private void Login(Object passwordBoxObject)
    {
        PasswordBox passwordBox = passwordBoxObject as PasswordBox;
        if (passwordBox == null)
            throw new Exception("PasswordBox is null");

        _userService.Login(UserName, passwordBox.SecurePassword, result =>
        {
            if (!result)
            {
                MessageBox.Show("Wrong username or password");
            }
        });
    }
}

Связывание и команды работают нормально, поэтому вопросов нет. Класс бизнес-макета для проектирования и тестирования:

public class DesignUserService : IUserService
{
    private readonly User _testUser;
    private readonly IList<User> _users;

    public void Login(String userName, SecureString password, Action<Boolean> callback)
    {
        var user = _users.FirstOrDefault(u => u.UserName.ToLower() == userName.ToLower());

        if (user == null)
        {
            callback(false);
            return;
        }

        String rawPassword = Security.ComputeHashString(password, user.Salt);
        if (rawPassword != user.Password)
        {
            callback(false);
            return;
        }

        callback(true);
    }

    public DesignUserService()
    {
        _testUser = new User
        {
            UserName = "testuser",
            Password = "123123",
            Salt = "123123"
        };

        _users = new List<User>
        {
            _testUser
        };
    }
}

UserData - это статический класс, который вызывает вызовы в базе данных (Entity Framework).

Теперь у меня есть мой тест:

[TestClass]
public class Login
{
    [TestMethod]
    public void IncorrectUsernameCorrectPassword()
    {
        IUserService userService = new DesignUserService();

        PasswordBox passwordBox = new PasswordBox
        {
            Password = "password"
        };
        userService.Login("nonexistingusername", passwordBox.SecurePassword, b => Assert.AreEqual(b, false));
    }
}

Теперь мой тест не находится в самой ViewModel, а непосредственно на уровне Business.

В основном у меня есть 2 вопроса:

  • Я нахожусь на правильном пути или есть фундаментальный недостаток в моей реализации шаблона?

  • Как я могу проверить свою ViewModel?

Ответ 1

В вашей модели представлений есть один подходящий код для проверки кода, который является Login. Учитывая, что он закрыт, его следует протестировать с помощью LoginCommand.

Теперь можно спросить, какова цель проверки команды, когда у вас уже есть тест для базовой бизнес-логики? Цель состоит в том, чтобы проверить, что бизнес-логика называется и с правильными параметрами.

Как можно пройти такой тест? Используя mock. Пример с FakeItEasy:

var userServiceFake = A.Fake<IUserService>();
var testedViewModel = new LoginViewModel(userServiceFake);

// prepare data for test
var passwordBox = new PasswordBox { Password = "password" };
testedViewModel.UserName = "TestUser";

// execute test
testedViewModel.LoginCommand.Execute(passwordBox);

// verify
A.CallTo(() => userServiceFake.Login(
    "TestUser",
    passwordBox.SecurePassword,
    A<Action<bool>>.Ignored)
).MustHaveHappened();

Таким образом вы убедитесь, что команда вызывает бизнес-уровень, как ожидалось. Обратите внимание, что Action<bool> игнорируется при сопоставлении параметров - трудно сопоставить Action<T> и Func<T> и обычно не стоит.

Несколько заметок:

  • Возможно, вам захочется пересмотреть код кода сообщения в модели вида (это должно принадлежать представлению, модель просмотра должна либо запрашивать, либо отображать представление для отображения всплывающего окна). При этом также будет проведено более тщательное тестирование модели просмотра (например, не нужно игнорировать аргумент Action)
  • Некоторые люди проверяют свойства INotifyPropertyChanged (UserName в вашем случае) - это событие возникает при изменении значения свойства. Поскольку это много шаблонов кода, настоятельно рекомендуется использовать tool/library для автоматизации этого процесса.
  • Вы хотите иметь два набора тестов, один для модели представления (как в примере выше) и один для базовой бизнес-логики (ваш первоначальный тест). В MVVM VM - это дополнительный слой, который может показаться малопригодным, но все дело в том, что у него нет бизнес-логики и скорее сосредоточена на перестройке данных/подготовке к просмотру.