WPF MVVM Light 单元测试 ViewModels

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/12332123/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 05:16:17  来源:igfitidea点击:

WPF MVVM Light unit testing ViewModels

c#wpfunit-testingmvvmmvvm-light

提问by Roger Far

I am not a regular with the MVVM pattern and this is basically my first time playing with it.

我不是 MVVM 模式的常客,这基本上是我第一次使用它。

What I used to do ("normal" WPF), was creating my Views with a Business layer and perhaps a datalayer (which usually contains my entities created by a service or the Entity Framework).

我过去所做的(“普通”WPF)是使用业务层和数据层(通常包含由服务或实体框架创建的实体)创建我的视图。

Now after some toying I created a standard template from MVVM Light and did this:

现在,经过一番玩弄之后,我从 MVVM Light 创建了一个标准模板并执行了以下操作:

Locator:

定位器:

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>();
        }
    }
}

Login 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");
            }
        });
    }
}

Binding and commands work fine so there is no questions. Business mockup class for design and test time:

绑定和命令工作正常,所以没有问题。设计和测试时间的业务模型类:

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 is a static class which makes calls to the database (Entity Framework).

UserData 是一个静态类,它调用数据库(实体框架)。

Now I have my test:

现在我有我的测试:

[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));
    }
}

Now my test is not on the ViewModel itself but directly to the Business layer.

现在我的测试不是在 ViewModel 本身上,而是直接在业务层上。

Basically I have 2 questions:

基本上我有两个问题:

  • Am I on the right path, or is there a fundamental flaw in my pattern implementation?

  • How can I test my ViewModel?

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

  • 如何测试我的 ViewModel?

回答by k.m

Your view model has one relevant piece of code worth testing, which is Loginmethod. Given that it's private, it should be tested it via LoginCommand.

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

Now, one might ask, what is the purpose of testing command when you already have test for underlying business logic? The purpose is to verify that business logic is calledand with correct parameters.

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

How do one goes with such test? By using mock. Example with FakeItEasy:

如何进行这样的测试?通过使用模拟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();

This way you verify that command calls business layer as expected. Note that Action<bool>is ignored when matching parameters - it's difficult to match Action<T>and Func<T>and usually not worth it.

通过这种方式,您可以验证命令按预期调用业务层。请注意,Action<bool>匹配参数,则会被忽略-这是难以企Action<T>Func<T>,通常不值得。

Few notes:

几点注意事项:

  • You might want to reconsider having message box code in view model (this should belong to view, view model should either requestor notifyview to display popup). Doing so, will also make more through testing of view model possible (eg. not needing to ignore that Actionargument)
  • Some people do test INotifyPropertyChangedproperties (UserNamein your case) - that event is raised when property value changes. Since this is lot of boilerplate code, using tool /libraryto automate this process is highly suggested.
  • You do want to have two sets of tests, one for view model (as in the example above) and one for underlying business logic (your original test). In MVVM, VM is that extra layer which might seem to be of little use - but that's the whole point - to have no business logic there and rather focus data rearrangement/preparation for views layer.
  • 您可能需要重新考虑在视图模型中使用消息框代码(这应该属于视图,视图模型应该请求通知视图显示弹出窗口)。这样做,也将使更多的视图模型测试成为可能(例如,不需要忽略该Action参数)
  • 有些人会测试INotifyPropertyChanged属性(UserName在您的情况下) - 当属性值更改时会引发该事件。由于这是大量样板代码,因此强烈建议使用工具/来自动化此过程。
  • 您确实希望有两组测试,一组用于视图模型(如上例所示),另一组用于底层业务逻辑(您的原始测试)。在 MVVM 中,VM 是一个额外的层,它似乎没有什么用处——但这就是重点——在那里没有业务逻辑,而是专注于视图层的数据重新排列/准备。