一、简介

单元测试覆盖率越高,就能越早阶段发现错误,减少因设计、人员、变更带来的质量风险,这对于一个产品而言尤为重要。特别是对网络和环境的各种模拟,可以解决很多我们很多哪怕用人工测试手段都很难发现的问题。而Mock技术就是用来创建一个模拟的世界,这极大的简化接口本身对各种依赖所带来的测试难度。

二、检查用户名和密码

我们假定有个方法需要根据用户名和密码来检索用户信息,这可能会有以下规则:

  • 用户名不存在。
  • 密码不正确。
  • 用户被锁定。
  • 正确用户名和密码。

1、实现代码

public class UserRepository : IUserRepository
{
  private readonly IDataStore _store;

  public UserRepository(IDataStore store)
  {
    _store = store;
  }

  public User FindByCredentials(string username, string password)
  {
    var user = _store.FindOneByNamedQuery("FindUserByUserName", username);
    if (user == null) { return null; }
    return user.Password == password ? user : null;
  }
}

每一种规则可能返回的结果都不相同,但我们可以肯定的是想要做到这个测试,必须先保证 IDataStore 正确的加载,而正常他是访问数据库。

2、测试代码

[TestMethod]
public void ReturnsNullIfTheUserNameDontMatch()
{
    // 1、构建数据接口
    var store = new SqlDataStore();
    // 2、创建仓储实例
    IUserRepository userRep = new UserRepository(store);
    // 3、调用接口
    var res = userRep.FindByCredentials("asdf", "1");

    Assert.IsNull(res);
}

以上是针对当密码无法匹配时,其他情况的写法差不多;我简化很多代码,但是一个完整的单元示例就需要这么多。那么问题来了,数据库当中必须要先保证有一条 asdf 数据,而且为了保证测试通过还需要保证往后不能修改这条信息,否则这个测试将不再是有效的。

很自然看出这样的单元测试代码必须是依赖于数据库,我们示例给出的过于简单,现实需要的依赖的更多,比较我们不可能用明文保存密码,可能我们还需要依赖加密处理类。

3、使用Mock

Mock框架有很多,这里我采用 Moq。改写后的代码为:

[TestMethod]
public void ReturnsNullIfTheUserNameDontMatchByMock()
{
    // 通过Mock构建一个UserRepository实例,
    var userRep = new Mock<IUserRepository>();
    // 模拟用户名asdf 密码1 的结果
    userRep.Setup<User>(x => x.FindByCredentials("asdf", "1"))
           .Returns(new User()
           {
               IsNormal = true,
               Password = "1",
               UserName = "asdf"
           });

    // 当密码为1时应该返回一个User对象
    Assert.IsNotNull(userRep.Object.FindByCredentials("asdf", "1"));
    // 当密码为2时应该返回一个Null
    Assert.IsNull(userRep.Object.FindByCredentials("asdf", "2"));
}

相比较使用常规的写法,不再依赖于具体UserRepository,由接口模拟一条数据。