2

我有一个将 NHibernate 用于 ORM 的 MVC 应用程序。每个控制器都采用一个 ISession 构造参数,然后使用该参数对域模型对象执行 CRUD 操作。例如,

public class HomeController : Controller
{
    public HomeController(ISession session)
    {
        _session = session;
    }
    public ViewResult Index(DateTime minDate, DateTime maxDate)
    {
        var surveys = _session.CreateCriteria<Survey>()
                              .Add( Expression.Like("Name", "Sm%") )
                              .Add( Expression.Between("EntryDate", minDate, maxDate) )
                              .AddOrder( Order.Desc("EntryDate") )
                              .SetMaxResults(10)
                              .List<Survey>();

        // other logic that I want to unit test that does operations on the surveys variable
        return View(someObject);
    }
    private ISession _session;
}

我想通过使用 Moq 或 RhinoMocks 模拟 ISession 对象来单独对该控制器进行单元测试,而无需实际访问数据库。但是,在单元测试中模拟 ISession 接口将非常困难,因为它是通过一个流畅的接口使用的,该接口将许多调用链接在一起。

一种替代方法是通过存储库模式包装 ISession 使用。我可以写一个像这样的包装类:

public interface IRepository
{
   List<Survey> SearchSurveyByDate(DateTime minDate, DateTime maxDate);
}

public class SurveyRepository : IRepository
{
    public SurveyRepository(ISession session)
    {    
        _session = session;
    }
    public List<Survey> SearchSurveyByDate(DateTime minDate, DateTime maxDate)
    {
        return _session.CreateCriteria<Survey>()
                          .Add( Expression.Like("Name", "Sm%") )
                          .Add( Expression.Between("EntryDate", minDate, maxDate) )
                          .AddOrder( Order.Desc("EntryDate") )
                          .SetMaxResults(10)
                          .List<Survey>();
    }
    private ISession _session;
}

然后我可以重写我的控制器以采用 IRepository 构造函数参数,而不是 ISession 参数:

public class HomeController : Controller
{
    public HomeController(IRepository repository)
    {
        _repository = repository;
    }

    public ViewResult Index(DateTime minDate, DateTime maxDate)
    {
        var surveys = _repository.SearchSurveyByDate(minDate, maxDate);
         // other logic that I want to unit test that does operations on the surveys variable
        return View(someObject);
    }
    private IRepository _repository;
}

第二种方法更容易进行单元测试,因为 IRepository 接口比 ISession 接口更容易模拟,因为它只是一个方法调用。但是,我真的不想走这条路,因为:

1)创建一个全新的抽象层和更多的复杂性只是为了使单元测试更容易,这似乎是一个非常糟糕的主意,并且

2) 有很多评论反对在 nHibernate 中使用存储库模式的想法,因为 ISession 接口已经是一个类似于存储库的接口。(特别是在这里这里查看 Ayende 的帖子)我倾向于同意这个评论。

所以我的问题是,有什么方法可以通过模拟 ISession 对象来对我的初始实现进行单元测试?如果不是,我唯一的办法是使用存储库模式包装 ISession 查询,还是有其他方法可以解决这个问题?

4

3 回答 3

3

奥伦经常四处游荡。他曾经是 Repositories 和 Unit of Work 的大力支持者。他可能会再次转向它,但有不同的要求。

Repository 有一些非常具体的优势,Oren 的评论都没有找到解决方案。此外,他的建议有它自己的一系列限制和问题。有时我觉得他只是在用一组问题交换另一组问题。当您需要提供相同数据的不同视图(例如 Web 服务或桌面应用程序)同时仍保留 Web 应用程序时,它也很好。

话虽如此,他有很多优点。我只是不确定他们是否有好的解决方案。

存储库对于高度测试驱动的场景仍然非常有用。如果您不知道您是否会坚持使用给定的 ORM 或持久层并且可能希望将其换成另一个,它仍然很有用。

Oren 的解决方案倾向于将 nHimbernate 更紧密地耦合到应用程序中。在许多情况下这可能不是问题,但在其他情况下可能是。

他创建专用查询类的方法很有趣,并且是CQRS的第一步,这可能是一个更好的整体解决方案。但是软件开发仍然是艺术或工艺,而不是科学。我们还在学习。

于 2012-05-27T05:12:55.463 回答
2

您是否考虑过让您的测试从使用 SQLite 的基本夹具继承,而不是模拟 ISession?

public class FixtureBase
{
    protected ISession Session { get; private set; }
    private static ISessionFactory _sessionFactory { get; set; }
    private static Configuration _configuration { get; set; }

    [SetUp]
    public void SetUp()
    {
        Session = SessionFactory.OpenSession();
        BuildSchema(Session);
    }

    private static ISessionFactory SessionFactory
    {
        get
        {
           if (_sessionFactory == null)
           {
                var cfg = Fluently.Configure()
                    .Database(FluentNHibernate.Cfg.Db.SQLiteConfiguration.Standard.ShowSql().InMemory())
                    .Mappings(configuration => configuration.FluentMappings.AddFromAssemblyOf<Residential>())
                    .ExposeConfiguration(c => _configuration = c);

                _sessionFactory = cfg.BuildSessionFactory();
           }

            return _sessionFactory;
        }
    }

    private static void BuildSchema(ISession session)
    {
        var export = new SchemaExport(_configuration);
        export.Execute(true, true, false, session.Connection, null);
    }

    [TearDown]
    public void TearDownContext()
    {
        Session.Close();
        Session.Dispose();
    }


}
于 2012-05-26T23:46:30.080 回答
2

使用命名查询方法引入存储库不会增加系统的复杂性。实际上,它降低了复杂性并使您的代码更易于理解和维护。对比原版:

public ViewResult Index(DateTime minDate, DateTime maxDate)
{
    var surveys = _session.CreateCriteria<Survey>()
                          .Add(Expression.Like("Name", "Sm%"))
                          .Add(Expression.Between("EntryDate", minDate, maxDate))
                          .AddOrder(Order.Desc("EntryDate"))
                          .SetMaxResults(10)
                          .List<Survey>();

     // other logic which operates on the surveys variable
     return View(someObject);
}

坦率地说,在我了解您方法的实际逻辑之前,我所有的内存插槽都已经被占用了。读者需要时间来了解您正在构建哪些标准、您传递了哪些参数以及返回了哪些值。而且我需要在代码行之间切换上下文。我开始考虑数据访问和 Hibernate,然后突然回到业务逻辑级别。如果您有多个地方需要按日期搜索调查怎么办?复制所有这些员工?

现在我正在阅读带有存储库的版本:

public ViewResult Index(DateTime minDate, DateTime maxDate)
{
    var surveys = _repository.SearchSurveyByDate(minDate, maxDate);
    // other logic which operates on the surveys variable
    return View(someObject);
}

我需要零努力才能理解这里发生的事情。此方法具有单一职责和单一抽象级别。所有数据访问相关的逻辑都消失了。查询逻辑不会在不同的地方重复。其实我不在乎它是如何实现的。如果这种方法的主要目标是,我应该关心some other logic吗?

而且,当然,您可以毫不费力地为您的业务逻辑编写单元测试(如果您使用 TDD 存储库,您可以在实际编写数据访问逻辑之前测试您的控制器,当您开始编写存储库实现时,您将已经设计了存储库接口):

[Test]
public void ShouldDoOtherLogic()
{
    // Arrange
    Mock<ISurveryRepository> repository = new Mock<ISurveryRepository>();
    repository.Setup(r => r.SearchSurveyByDate(minDate, maxDate))
              .Returns(surveys);

    // Act
    HomeController controller = new HomeController(repository.Object);
    ViewResult result = controller.Index(minDate, maxDate);

    // Assert
}

顺便说一句,内存数据库的使用对于验收测试很有用,但对于单元测试,我认为它有点过头了。

还可以查看 NHibernate 3.0 中的NHibernate Lambda Extensions或 QueryOver,它们使用表达式而不是字符串来构建标准。如果您重命名某个字段,您的数据访问代码不会中断。

并查看Range以传递成对的最小值/最大值。

于 2012-05-28T08:51:12.963 回答