在ASP.NET MVC测试项目中模拟静态方法

3
我有一个类似下面这样的方法:
public List<Rajnikanth> GetRajnis()
{
    string username = Utility.Helpers.GetLoggedInUserName();
    return _service.GetRajni(username);
}   

Utility.Helper是一个静态类, public static class Helpers {

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()?

2个回答

4

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

如果您想要模拟一个返回值,那么这非常简单。您可以修改 Utility.Helper 类,添加一个名为 OverrideLoggedInUserName 的属性。当有人调用 GetLogedInUserName() 时,如果设置了覆盖属性,则返回该属性的值,否则使用从 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。因此,你必须记得重新设置为null。
更好的方法是注入依赖项。可能更好的方法是创建一个检索已登录用户名称的类,并将其传递到SomeController的构造函数中。我们称之为依赖注入。这样,你可以将一个模拟实例注入其中进行测试,但在不进行测试时传递真实实例(从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或同样强大的工具,否则无法更改静态类的逻辑。我也不建议这样做。如果您必须存根(stub)一个静态类,那么它很可能不应该是一个静态类。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接