【问题标题】:Mocking static method in ASP.NET MVC Test project在 ASP.NET MVC 测试项目中模拟静态方法
【发布时间】:2012-10-08 14:00:14
【问题描述】:

我有一个类似下面的方法

public List<Rajnikanth> GetRajnis()
{
    string username = Utility.Helpers.GetLoggedInUserName();
    return _service.GetRajni(username);
}   

Utility.Helper 是一个静态类, 公共静态类助手 {

public static String GetLoggedInUserName()
{
    string username = "";
    if (System.Web.HttpContext.Current.User.Identity.IsAuthenticated)
    {
        username = ((System.Web.Security.FormsIdentity)HttpContext.Current.User.Identity).Ticket.Name;
    }
    return username;

}

}

我想测试:GetRajnis()

我想模拟:GetLoggedInUserName()

所以我的测试方法看起来像......

[TestMethod]
public void TestGetRajnis()
{
    SomeController s = new SomeController(new SomeService());
    var data = s.GetRajnis();
    Assert.IsNotNull(data);
}

如何模拟静态方法 GetLoggedInUserName() ?

【问题讨论】:

    标签: asp.net-mvc unit-testing asp.net-mvc-4 moq


    【解决方案1】:

    最简单的方法:覆盖返回值

    如果你想模拟一个返回值,那么这很简单。您可以修改Utility.Helper 类以包含一个名为OverrideLoggedInUserName 的属性。当有人调用GetLogedInUserName()时,如果设置了override属性,则返回,否则使用正常的从HttpContext中取值的代码来获取返回值。

    public static class Helper
    {
        // Set this value to override the return value of GetLoggedInUserName().
        public static string OverrideLoggedInUserName { get; set; };
    
        public static string GetLoggedInUserName()
        {
            // Return mocked value if one is specified.
            if ( !string.IsNullOrEmpty( OverrideLoggedInUserName ) )
                return OverrideLoggedInUserName;
    
            // Normal implementation.
            string username = "";
            if ( System.Web.HttpContext.Current.User.Identity.IsAuthenticated )
            {
                username = ( (System.Web.Security.FormsIdentity)HttpContext.Current.User.Identity ).Ticket.Name;
            }
            return username;
        }
    }
    

    这将有效地允许您覆盖返回值,从技术上讲,它不是一个模拟——它是一个存根(根据 Martin Fowler 的优秀文章 Mocks Aren't Stubs。这允许您存根返回值,但不允许您断言该方法是否被调用。无论如何,只要您只想操纵返回值,它就可以正常工作。

    这是您在测试中使用它的方法。

    [ TestMethod ]
    public void TestGetRajnis()
    {
        // Set logged in user name to be "Bob".
        Helper.OverrideLoggedInUserName = "Bob";
    
        SomeController s = new SomeController( new SomeService() );
        var data = s.GetRajnis();
    
        // Any assertions...
    }
    

    这种设计确实有一个缺点。因为它是一个静态类,如果您设置了覆盖值,它会一直保持设置,直到您取消设置它。所以你一定要记得把它重新设置为null。

    更好的方法:注入依赖项

    更好的方法可能是创建一个检索登录用户名的类,并将其传递给SomeController 的构造函数。我们称之为dependency injection。这样,您可以将模拟实例注入其中进行测试,但在不测试时传递真实实例(从 HttpContext 获取用户)。这是一种更清晰、更清晰的方法。此外,您可以利用所使用的任何模拟框架的所有功能,因为它们是专门为处理这种方法而设计的。这就是它的样子。

    // Define interface to get the logged in user name.
    public interface ILoggedInUserInfo
    {
        string GetLoggedInUserName();
    }
    
    // Implementation that gets logged in user name from HttpContext. 
    // This class will be used in production code.
    public class LoggedInUserInfo : ILoggedInUserInfo
    {
        public string GetLoggedInUserName()
        {
            // This is the same code you had in your example.
            string username = "";
            if ( System.Web.HttpContext.Current.User.Identity.IsAuthenticated )
            {
                username = ( (System.Web.Security.FormsIdentity)HttpContext.Current.User.Identity ).Ticket.Name;
            }
            return username;
        }
    }
    
    // This controller uses the ILoggedInUserInfo interface 
    // to get the logged in user name.
    public class SomeController
    {
        private SomeService _service;
        private ILoggedInUserInfo _userInfo;
    
        // Constructor allows you inject an object that tells it 
        // how to get the logged in user info.
        public SomeController( SomeService service, ILoggedInUserInfo userInfo )
        {
            _service = service;
            _userInfo = userInfo;
        }
    
        public List< Rajnikanth > GetRajnis()
        {
            // Use the injected object to get the logged in user name.
            string username = _userInfo.GetLoggedInUserName();
            return _service.GetRajni( username );
        }
    }
    

    这是一个使用 Rhino Mocks 将存根对象注入控制器的测试。

    [ TestMethod ]
    public void TestGetRajnis()
    {
        // Create a stub that returns "Bob" as the current logged in user name.
        // This code uses Rhino Mocks mocking framework...
        var userInfo = MockRepository.GenerateStub< ILoggedInUserInfo >();
        userInfo.Stub( x => x.GetLoggedInUserName() ).Return( "Bob" );
    
        SomeController s = new SomeController( new SomeService(), userInfo );
        var data = s.GetRajnis();
    
        // Any assertions...
    }
    

    这里的缺点是您不能只从代码中的任何位置调用Helper.GetLoggedInUserName(),因为它不再是静态的。但是,您不再需要在每次完成测试时重新设置存根用户名。因为它不是静态的,所以它会自动重置。您只需为下一次测试重新创建它并设置一个新的返回值。

    我希望这会有所帮助。

    【讨论】:

      【解决方案2】:

      如果您正在寻找可测试性,请摆脱静态类。现在一个简单的解决方法是在静态类周围创建一个包装器。除非您使用 TypeMock 之类的东西或同样强大的东西,否则您无法更改静态类的逻辑。我也不建议这样做。如果你必须存根一个静态类,它可能不应该是一个静态类。

      public class StaticWrapper
      {
          public virtual String GetLoggedInUserName()
          {
              Utility.Helpers.GetLoggedInUserName();
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2017-05-04
        • 1970-01-01
        • 1970-01-01
        • 2018-12-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-21
        相关资源
        最近更新 更多