2012-09-08 77 views
11

我不是MVVM模式的常规人员,这基本上是我第一次玩它。我曾经做过的事情(“普通”WPF),是通过业务层和数据层(通常包含由服务或实体框架创建的实体)创建我的视图。WPF MVVM光单元测试ViewModels

现在有些玩弄后,我创建了从MVVM光一个标准模板,这样做:

定位:

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

登录视图模型:

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是一个静态类,这使得调用数据库(实体框架)。

现在我有我的测试:

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

现在我的测试是不是在视图模型本身,而是直接到业务层。

基本上我有2个问题:

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

  • 我该如何测试我的ViewModel?

回答

15

您的视图模型有一个值得测试的相关代码片段,即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你的情况) - 该事件引发的属性值更改时。由于这是很多样板代码,强烈建议使用工具/ library自动执行此过程。
  • 您确实希望有两组测试,一个用于查看模型(如上例所示),另一个用于底层业务逻辑(您的原始测试)。在MVVM中,虚拟机是那个似乎没什么用处的额外层 - 但这就是整个观点 - 没有业务逻辑,而是专注于数据重新布置/准备视图层。
+0

感谢您的回答!但我在看,你还在测试命令的结果还是只是命令执行正确? – YesMan85

+1

@ Rogier21:你通过*“命令的结果”*了解了什么?直接结果将是对业务层的调用(覆盖上面的测试) - 间接结果将是业务层代码所做的任何事情。这应该通过业务层测试进行测试(并且据我所知,您已经使用'DesignUserService'测试来完成这项工作)。 –

+0

是的,我明白你的观点。你的意思是你会得到2个测试:1测试在业务层中的方法的正确结果(例如登录),1测试以测试方法是否从ViewModel正确调用? – YesMan85