问题 WPF MVVM Light单元测试ViewModels


我不是MVVM模式的常规,这基本上是我第一次玩它。

我以前做的(“正常”WPF)是用业务层创建我的视图,也许是数据层(通常包含由服务或实体框架创建的实体)。

现在经过一些玩弄我从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层上。

基本上我有两个问题:

  • 我是在正确的道路上,还是我的模式实施存在根本缺陷?

  • 我该如何测试我的ViewModel?


2996
2017-09-08 15:43


起源



答案:


您的视图模型有一段值得测试的相关代码,即 Login 方法。鉴于它是私有的,应该通过它进行测试 LoginCommand

现在,有人可能会问,当您已经对底层业务逻辑进行测试时,测试命令的目的是什么?目的是验证业务逻辑 叫做 与 正确的参数

如何进行这样的测试?通过使用 嘲笑。示例 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 在您的情况下) - 当属性值更改时引发该事件。由于这是很多样板代码,使用工具/图书馆 强烈建议自动执行此过程。
  • 你确实想拥有 两套测试,一个用于视图模型(如上例所示),另一个用于底层业务逻辑(原始测试)。在MVVM中,VM是一个额外的层,它看起来似乎没什么用处 - 但这就是重点 - 在那里没有业务逻辑,而是专注于视图层的数据重新排列/准备。

15
2017-09-08 23:00



感谢您的回答!但我在寻找,你还在测试命令的结果还是只是命令执行正确? - YesMan85
@Rogier21:你对什么理解 “命令的结果”?直接结果将是对业务层的调用(上面的测试覆盖) - 间接结果将是业务层代码所做的任何事情。这应该通过业务层测试来测试(据我所知,你已经做过了 DesignUserService测试)。 - k.m
是的,我明白你的意思了。你的意思是你会得到2个测试:1来测试业务层中方法的正确结果(例如Login),还有1个测试来测试是否从ViewModel正确调用了方法? - YesMan85
@Rogier21:是的,基本上是的。我把它作为我答案中的最后一点,以便一切都清楚。 - k.m


答案:


您的视图模型有一段值得测试的相关代码,即 Login 方法。鉴于它是私有的,应该通过它进行测试 LoginCommand

现在,有人可能会问,当您已经对底层业务逻辑进行测试时,测试命令的目的是什么?目的是验证业务逻辑 叫做 与 正确的参数

如何进行这样的测试?通过使用 嘲笑。示例 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 在您的情况下) - 当属性值更改时引发该事件。由于这是很多样板代码,使用工具/图书馆 强烈建议自动执行此过程。
  • 你确实想拥有 两套测试,一个用于视图模型(如上例所示),另一个用于底层业务逻辑(原始测试)。在MVVM中,VM是一个额外的层,它看起来似乎没什么用处 - 但这就是重点 - 在那里没有业务逻辑,而是专注于视图层的数据重新排列/准备。

15
2017-09-08 23:00



感谢您的回答!但我在寻找,你还在测试命令的结果还是只是命令执行正确? - YesMan85
@Rogier21:你对什么理解 “命令的结果”?直接结果将是对业务层的调用(上面的测试覆盖) - 间接结果将是业务层代码所做的任何事情。这应该通过业务层测试来测试(据我所知,你已经做过了 DesignUserService测试)。 - k.m
是的,我明白你的意思了。你的意思是你会得到2个测试:1来测试业务层中方法的正确结果(例如Login),还有1个测试来测试是否从ViewModel正确调用了方法? - YesMan85
@Rogier21:是的,基本上是的。我把它作为我答案中的最后一点,以便一切都清楚。 - k.m