【问题标题】:Need Help Writing Unit Test that on Method that requires HttpContext.Current.User需要帮助在需要 HttpContext.Current.User 的方法上编写单元测试
【发布时间】:2012-02-20 21:15:27
【问题描述】:

我正在尝试为我的 MVC3 项目(该项目的第一个测试)编写一些单元测试,但我被难住了。基本上,我有一个 MemberQueries 类,我的 MemberController 使用它来处理所有逻辑。

我想开始编写这个类的测试,并想从一个简单的例子开始。我在这个类中有一个名为 IsEditModeAvailable 的方法,它确定用户是否是“站点管理员”角色的成员,或者用户是否能够编辑自己的数据,但不能编辑其他人。我通过将传入的 Id 值与 HttpContext User 属性进行比较来确定最后一个要求。

我遇到的问题是,在创建 MemberQueries 对象时,我不知道如何模拟或将正确的参数注入到我的单元测试中。我正在使用 NUnit、Moq 和 Ninject,但我只是不确定如何编写代码。如果我的结构不正确,请告诉我,因为我完全是单元测试的菜鸟。

这是我的 MemberQueries 类的代码示例:

public class MemberQueries : IMemberQueries
{
  private readonly IRepository<Member> _memberRepository;
  private readonly IMemberServices _memberServices;
  private readonly IPrincipal _currentUser;

  public MemberQueries(IUnitOfWork unitOfWork, IMemberServices memberServices, IPrincipal currentUser)
  {
    _memberRepository = unitOfWork.RepositoryFor<Member>();
    _memberServices = memberServices;
    _currentUser = currentUser;
  }

  public bool IsEditModeAvailable(int memberIdToEdit)
  {
    if (_currentUser.IsInRole("Site Administrator")) return true;
    if (MemberIsLoggedInUser(memberIdToEdit)) return true;
    return false;
  }

  public bool MemberIsLoggedInUser(int memberIdToEdit)
  {
    var loggedInUser = _memberServices.FindByEmail(_currentUser.Identity.Name);
    if (loggedInUser != null && loggedInUser.Id == memberIdToEdit) return true;
    return false;
  }

}

这是我的 MemberServices 类的示例(在我的域项目中,由 MemberQueries 引用):

public class MemberServices : IMemberServices
{
  private readonly IRepository<Member> _memberRepository;

  public MemberServices(IUnitOfWork unitOfWork)
  {
    _memberRepository = unitOfWork.RepositoryFor<Member>();
  }

  public Member Find(int id)
  {
    return _memberRepository.FindById(id);
  }

  public Member FindByEmail(string email)
  {
    return _memberRepository.Find(m => m.Email == email).SingleOrDefault();
  }
}

最后,这是我正在尝试编写的单元测试的存根:

[Test]
public void LoggedInUserCanEditTheirOwnInformation()
{
  var unitOfWork = new UnitOfWork();

  var currentUser = new Mock<IPrincipal>();
  // I need to somehow tell Moq that the logged in user has a HttpContext.User.Name of "jdoe@acme.com"

  var memberServices = new Mock<MemberServices>();
  // I then need to tell Moq that it's FindByEmail("jdoe@acme.com") method should return a member with a UserId of 1

  var memberQueries = new MemberQueries(unitOfWork, memberServices.Object, currentUser.Object);

  // If the logged in user is "jdoe@acme.com" who has an Id of 1, then IsEditModeAvailable(1) should return true
  Assert.IsTrue(memberQueries.IsEditModeAvailable(1));
}

【问题讨论】:

  • 我在你的代码中没有看到对HttpContext.Current的引用。
  • 达林,我通过ninject注入HttpContext如下:`kernel.Bind().ToMethod(ctx => HttpContext.Current.User).InRequestScope();`
  • 您尝试测试的代码不依赖于它。所以你可以完美地模拟一切。

标签: asp.net-mvc-3 unit-testing httpcontext


【解决方案1】:

您似乎正在尝试测试MemberQueries.IsEditModeAvailable 方法。您需要在这里介绍 2 个案例。 Site Administrators 情况和当前登录用户的 id 与作为参数传递的用户匹配的情况。由于MemberQueries 类完全依赖于接口,因此您可以模拟所有内容:

[TestMethod]
public void EditMode_Must_Be_Available_For_Site_Administrators()
{
    // arrange
    var unitOfWork = new Mock<IUnitOfWork>();
    var currentUser = new Mock<IPrincipal>();
    currentUser.Setup(x => x.IsInRole("Site Administrator")).Returns(true);
    var memberServices = new Mock<IMemberServices>();
    var memberQueries = new MemberQueries(unitOfWork.Object, memberServices.Object, currentUser.Object);

    // act
    var actual = memberQueries.IsEditModeAvailable(1);

    // assert
    Assert.IsTrue(actual);
}

[TestMethod]
public void EditMode_Must_Be_Available_For_Logged_In_Users_If_His_Id_Matches()
{
    // arrange
    var unitOfWork = new Mock<IUnitOfWork>();
    var currentUser = new Mock<IPrincipal>();
    var identity = new Mock<IIdentity>();
    identity.Setup(x => x.Name).Returns("john.doe@gmail.com");
    currentUser.Setup(x => x.Identity).Returns(identity.Object);
    currentUser.Setup(x => x.IsInRole("Site Administrator")).Returns(false);
    var memberServices = new Mock<IMemberServices>();
    var member = new Member
    {
        Id = 1
    };
    memberServices.Setup(x => x.FindByEmail("john.doe@gmail.com")).Returns(member);
    var memberQueries = new MemberQueries(unitOfWork.Object, memberServices.Object, currentUser.Object);

    // act
    var actual = memberQueries.IsEditModeAvailable(1);

    // assert
    Assert.IsTrue(actual);
}

实际上,您需要涵盖第三种情况:您有一个当前登录的用户,该用户不是站点管理员,并且其 id 与作为参数传递的不匹配:

[TestMethod]
public void EditMode_Should_Not_Be_Available_For_Logged_In_Users_If_His_Id_Doesnt_Match()
{
    // arrange
    var unitOfWork = new Mock<IUnitOfWork>();
    var currentUser = new Mock<IPrincipal>();
    var identity = new Mock<IIdentity>();
    identity.Setup(x => x.Name).Returns("john.doe@gmail.com");
    currentUser.Setup(x => x.Identity).Returns(identity.Object);
    currentUser.Setup(x => x.IsInRole("Site Administrator")).Returns(false);
    var memberServices = new Mock<IMemberServices>();
    var member = new Member
    {
        Id = 2
    };
    memberServices.Setup(x => x.FindByEmail("john.doe@gmail.com")).Returns(member);
    var memberQueries = new MemberQueries(unitOfWork.Object, memberServices.Object, currentUser.Object);

    // act
    var actual = memberQueries.IsEditModeAvailable(1);

    // assert
    Assert.IsFalse(actual);
}

【讨论】:

  • 达林,你摇滚!谢谢你。这非常有效,这将极大地帮助我为这门课编写其他测试。我真的很感激。
【解决方案2】:

好消息是您将用户作为 IPrincipal 传递到需要它的代码中,而不是引用 HttpContext.Current.User。您需要做的就是设置模拟 IPrincipal 以便它返回该测试所需的值。

var mockIdentity = new Mock<IIdentity>();
mockIdentity.Setup(x => x.Name).Returns("joe@acme.com");

var mockPrincipal = new Mock<IPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(mockIdentity.Object);

【讨论】:

  • 特雷弗,感谢您的回复。它帮助我开始,至少在达林写下我需要的一切之前。 ;-) 我很感激。