【问题标题】:Unit-testing an action which calls session object对调用会话对象的操作进行单元测试
【发布时间】:2011-10-03 06:25:41
【问题描述】:

如何对在其主体内使用会话对象的方法进行单元测试?

假设我有以下操作:

[HttpPost]
public JsonResult GetSearchResultGrid(JqGridParams gridParams, Guid campaignId, string queryItemsString)
{
    var queryItems = new JavaScriptSerializer().Deserialize<IList<FilledQueryItem>>(queryItemsString);
    IPageData pageData = gridParams.ToPageData();
    var extraFieldLinker = SessionHandler.CurrentExtraFieldsLinker;
    var searchParams = new SearchParamsModel(extraFieldLinker, queryItems);
    IList<CustomerSearchResultRow> searchResults = null;
    searchResults = _customerService.SearchCustomersByUrlAndCampaign(campaignId,
        searchParams.SearchString,
        searchParams.AddressFilterPredicate,
        pageData);
    return GetGridData<CustomerSearchResultGridDefinition, CustomerSearchResultRow>(searchResults, pageData);
}

由于会话问题,我进行了以下单元测试,但到目前为止失败:

[Test]
public void CanGetSearchResultGrid()
{
    //Initialize
    var mockJqGridParams = new Mock<JqGridParams>();
    var mockPageData = new Mock<IPageData>();
    IPagedList<CustomerSearchResultRow> mockPagedResult = new PagedList<CustomerSearchResultRow>(mockPageData.Object);
    var guid= Guid.NewGuid();
    const string searchString =
        "[{\"Caption\":\"FirstName\",\"ConditionType\":\"contains\",\"Value\":\"d\",\"NextItem\":\"Last\"}]";
    Func<Address,bool> addressFilterPredicate = (x => true);

    //Setup
    mockJqGridParams.Setup(x => x.ToPageData()).Returns(mockPageData.Object);
    _customerService.Setup(x => x.SearchCustomersByUrlAndCampaign(guid, searchString, addressFilterPredicate, mockPageData.Object))
        .Returns(mockPagedResult);

    //Call
    var result = _homeController.GetSearchResultGrid(mockJqGridParams.Object, guid, searchString);

    mockJqGridParams.Verify(x => x.ToPageData(), Times.Once());
    _customerService.Verify(x => x.SearchCustomersByUrlAndCampaign(guid, searchString, addressFilterPredicate, mockPageData.Object)
        , Times.Once());

    //Verify
    Assert.That(result, Is.Not.Null);
    Assert.That(result, Is.TypeOf(typeof(JsonResult)));
}

当然还有助手的方法:

   public static ExtraFieldsLinker CurrentExtraFieldsLinker
    {
        get
        {
            object extraFieldLinker = GetSessionObject(EXTRA_FIELDS_LINKER);
            return extraFieldLinker as ExtraFieldsLinker;
        }
        set { SetSessionObject(EXTRA_FIELDS_LINKER, value); }
    }

【问题讨论】:

  • 你能模拟 ExtraFieldsLinker 吗?
  • GetSessionObject 和 SetSessionObject 方法长什么样?他们是否有机会使用HttpContext.Current 来参加会议?如果是,则需要对代码进行一些重构以使其可单元测试。
  • 是的,我正在使用当前的 HttpContext。你会建议什么样的重构?
  • @Hohhi,使用HttpContext 没问题。使用HttpContext.Current 是个问题。 :)

标签: c# .net asp.net-mvc


【解决方案1】:

我已经解决了类似的问题(使用对模拟不友好的静态数据访问器 - 特别是 HttpContext.Current),方法是将访问包装在另一个对象中,并通过接口访问它。你可以这样做:

pubic interface ISessionData
{
    ExtraFieldsLinker CurrentExtraFieldsLinker { get; set; }
}

public class SessionDataImpl : ISessionData
{
    ExtraFieldsLinker CurrentExtraFieldsLinker
    {
        // Note: this code is somewhat bogus,
        // since I think these are methods of your class.
        // But it illustrates the point.  You'd put all the access here
        get { return (ExtraFieldsLinker)GetSessionObject(EXTRA_FIELDS_LINKER); }
        set { SetSessionObject(EXTRA_FIELDS_LINKER, value); }
    }
}

public class ClassThatContainsYourAction
{
    static ClassThatContainsYourAction()
    {
        SessionData = new SessionDataImpl();
    }

    public static ISessionData SessionData { get; private set; }

    // Making this access very ugly so you don't do it by accident
    public void SetSessionDataForUnitTests(ISessionData sessionData)
    {
        SessionData = sessionData;
    }

    [HttpPost]
    public JsonResult GetSearchResultGrid(JqGridParams gridParams,
        Guid campaignId, string queryItemsString)
    {
        var queryItems = // ...
        IPageData pageData = // ...

        // Access your shared state only through SessionData
        var extraFieldLinker = SessionData.CurrentExtraFieldsLinker;

        // ...
    }
}

然后您的单元测试可以在调用GetSearchResultGrid 之前将ISessionData 实例设置为模拟对象。

理想情况下,您会在某个时候使用依赖注入库,并摆脱静态构造函数。

如果你能想办法让你的ISessionData 成为实例对象而不是静态对象,那就更好了。模拟对象框架倾向于为每个测试用例创建一个新的模拟类型,并且在以前的测试中放置模拟是一种恶心。我相信会话状态无论如何都会对你的会话来说是全局的,所以你可能不需要做任何棘手的事情来使非静态对象工作。

【讨论】:

  • 可能将此操作标记为虚拟并在存根中覆盖它会更好?
  • 我喜欢你的建议,但我觉得控制器类 ClassThatContainsYourAction- 受到测试信息的影响
  • @Hohhi:你最喜欢的。我们的想法是让这段代码一开始就可测试,这两者都可以完成。
  • @Hohhi:所以我的示例代码不是我会怎么做。我只是想演示抽象的概念,而不是实现它的最佳代码。如果你想知道我会怎么做,首先让它成为非静态的。然后查找“依赖注入”,特别是“构造函数注入”。然后设置支持字段readonly,这样您不会意外设置它。并摆脱公共财产。如果没有 DI 框架,我编写的代码非常接近我的编写方式。它(故意)很粗糙,但它可以完成工作,并有助于防止错误。
猜你喜欢
  • 2011-09-26
  • 2018-02-11
  • 2014-05-21
  • 1970-01-01
  • 1970-01-01
  • 2013-01-20
  • 2017-05-28
  • 2013-04-16
  • 2023-03-06
相关资源
最近更新 更多