【问题标题】:Unit test using Moq doesn't pass, object is null, did I miss something?使用 Moq 的单元测试未通过,对象为空,我错过了什么吗?
【发布时间】:2010-12-30 03:57:15
【问题描述】:

我要测试的类是我的 ArticleManager 类,特别是 LoadArticle 方法:

public class ArticleManager : IArticleManager
{
      private IArticle _article;

      public ArticleManger(IDBFactory dbFactory)
      {
            _dbFactory = dbFactory;
      }

      public void LoadArticle(string title)
      {
            _article = _dbFactory.GetArticleDAO().GetByTitle(title);

      }
}

我的 ArticleDAO 看起来像:

public class ArticleDAO : GenericNHibernateDAO<IArticle, int>, IArticleDAO
{
       public virtual Article GetByTitle(string title)
       {
           return Session.CreateCriteria(typeof(Article))
               .Add(Expression.Eq("Title", title))
               .UniqueResult<Article>();
       }
}

我使用NUnitMoq的测试代码:

[SetUp]
public void SetUp()
{
        _mockDbFactory = new Mock<IDBFactory>();
        _mockArticleDao = new Mock<ArticleDAO>();

        _mockDbFactory.Setup(x => x.GetArticleDAO()).Returns(_mockArticleDao.Object);

        _articleManager = new ArticleManager(_mockDbFactory.Object);
}


[Test]
public void load_article_by_title()
{
     var article1 = new Mock<IArticle>();

     _mockArticleDao.Setup(x => x.GetByTitle(It.IsAny<string>())).Returns(article1.Object);

     _articleManager.LoadArticle("some title");

     Assert.IsNotNull(_articleManager.Article);
}

单元测试失败,_articleManager.Article 对象返回 NULL。

我做的一切都正确吗?

这是我的第一个单元测试之一,所以我可能遗漏了一些明显的东西?

我遇到的一个问题是,我想模拟 IArticleDao,但由于 ArticleDao 类也继承自抽象类,如果我只是模拟 IArticleDao,那么 GenericNHibernateDao 中的方法不可用?

【问题讨论】:

  • 据我所知,安装程序代码根本无法编译,因为 ArticleManager 构造函数采用 IDBFactory 的实例,但您提供的是 Mock&lt;IDBFactory&gt; 的实例。这让我觉得发布的代码还有其他问题。您是否不小心遗漏了什么?
  • 对不起,我把它改成了 _mockDbFactory.Object。你指的是什么?

标签: c# unit-testing nhibernate moq


【解决方案1】:

前言:我不熟悉使用 Moq(这里是 Rhino Mocks 用户),所以我可能会错过一些技巧。

我正在努力遵循这里的一些代码;正如 Mark Seemann 指出的那样,我不明白为什么它甚至会在当前状态下编译。请您仔细检查一下代码吗?

突出的一点是您将 IDBFactory 的模拟注入到文章管理器中。然后,您进行链式调用:

_article = _dbFactory.GetArticleDAO().GetByTitle(title)

您尚未提供GetArticleDAO 的实现。您只嘲笑了在GetARticleDAO 调用之后 发生的LoadByTitle 位。测试中模拟和链式调用的组合通常表明测试即将变得痛苦。

得墨忒耳法则

这里的重点:Respect the Law of Demeter。 ArticleManager 使用 IDBFactory 返回的 IArticleDAO。除非 IDBFactory 做了一些非常重要的事情,否则您应该将 IArticleDAO 注入 ArticleManager。

Misko 雄辩地解释了为什么 Digging Into Collaborators 是个坏主意。这意味着您需要进行额外的设置步骤,并且还会使 API 更加混乱。

另外,为什么将返回的文章作为字段存储在 ArticleManager 中?可以直接退货吗?

如果可以进行这些更改,它将简化代码并使测试变得容易 10 倍。

您的代码将变为:

public class ArticleManager : IArticleManager
{
      private IArticleDAO _articleDAO

      public ArticleManger(IArticleDAO articleDAO)
      {
            _articleDAO = articleDAO;
      }

      public IArticle LoadArticle(string title)
      {
            return _articleDAO.GetByTitle(title);
      } 
}

您将拥有一个更简单的 API,并且它会更容易测试,因为嵌套已经消失。

依靠持久性使测试更容易

在我对与持久性机制交互的代码进行单元测试的情况下,我通常使用repository pattern 并创建手动、伪造的内存存储库来帮助进行测试。它们通常也很容易编写——它只是实现 IArticleRepository 接口的字典的包装器。

使用这种技术可以让您的 ArticleManager 使用假的持久性机制,该机制的行为与用于测试的 db 非常相似。然后,您可以轻松地用数据填充存储库,帮助您以轻松的方式测试 ArticleManager。

模拟框架确实是很好的工具,但它们并不总是适合设置和验证复杂或连贯的交互;如果您需要在一个测试中模拟/存根多个事物(尤其是嵌套事物!),这通常表明该测试被过度指定或手动测试替身将是更好的选择。

测试很难

...在我看来,如果从模拟框架开始,难度会加倍。由于引擎盖下发生的“魔术”,我已经看到很多人将自己与模拟框架联系在一起。因此,我一般主张远离他们until you're comfortable with hand-rolled stubs/mocks/fakes/spies etc

【讨论】:

  • 我忘了写我模拟调用 GetArticleDAO() 的那一行,我已将它添加到 SetUp。我明白你的意思,但我喜欢在构造函数中将 DAOFactory 作为参数传递。
  • “喜欢”是主观的。有深思熟虑的理由说明你不应该违反得墨忒耳定律。如果 ArticleManager 使用 IArticleDAO,理所当然你应该给它一个 IArticleDAO :)
  • 标记,但它还需要我不想传递给构造函数的其他 DAO。是的,那么你会说使用 DI :)
  • 是的,这就是我要说的 :) 我有一些类似于你的 IDBFactory 的东西;我调用它来在应用程序连接阶段创建我的存储库,然后将实例传递给需要它们的类,而不是传入工厂本身。
【解决方案2】:

正如您目前提供的代码,我看不出它可以编译 - 有两个原因。

第一个可能只是疏忽,但 ArticleManager 类没有 Article 属性,但我假设它只是返回 _article 字段。

另一个问题是这行代码:

_mockArticleDao.Setup(x => x.GetByTitle(It.IsAny<string>())).Returns(article1.Object);

据我所知,这根本不应该编译,因为ArticleDAO.GetByTitle 返回Article,但是您告诉它返回IArticle 的实例(接口,而不是具体类)。

您是否遗漏了代码描述中的某些内容?

无论如何,我怀疑问题在于这个Setup 调用。如果您错误地指定了设置,它将永远不会被调用,并且 Moq 默认为其默认行为,即返回该类型的默认值(即,对于引用类型,为 null)。

顺便说一句,可以通过像这样设置 DefaultValue 属性来更改该行为:

 myMock.DefaultValue = DefaultValue.Mock;

但是,这不太可能解决你的这个问题,所以你能解决我上面指出的问题吗,我相信我们能找出问题所在。

【讨论】:

    【解决方案3】:

    我不是起订量专家,但在我看来,问题在于您在嘲笑 ArticleDAO,而您应该在嘲笑 IArticleDAO。

    这与您的问题有关:

    我遇到的一个问题是,我想模拟 IArticleDao,但由于 ArticleDao 类也继承自抽象类,如果我只是模拟 IArticleDao,那么 GenericNHibernateDao 中的方法不可用?

    在模拟对象中,您不需要从 GenericNHibernateDao 类继承的方法。您只需要模拟对象来提供参与测试的方法,即:GetByTitle。您通过模拟提供此方法的行为。

    如果您尝试模拟的类型中已经存在方法,Moq 将不会模拟它们。在API docs中指定:

    任何接口类型都可以用于模拟,但对于类,只能模拟抽象和虚拟成员。

    具体来说,您对 GetByTitle 的模拟将被忽略,因为模拟类型 ArticleDao 提供了此方法的(非抽象)实现。

    因此,我给你的建议是模拟接口 IArticleDao 而不是类。

    【讨论】:

      【解决方案4】:

      正如 Mark Seeman 所说,我无法让它“按原样”编译,因为 .GetByTitle 期望返回错误的类型,从而导致编译时错误。

      在更正此问题并添加缺少的 Article 属性后,测试通过了 - 导致我认为您在 SO 上写的问题的核心不知何故在翻译中丢失了。

      但是,鉴于您要报告问题,我想我会提到一种可以让 Moq 本身帮助您确定问题的方法。

      你得到一个空的_articleManager.Article 几乎可以肯定是因为没有匹配的期望.GetByTitle。换句话说,您指定的那个不匹配。

      通过将您的模拟切换到 strict 模式,Moq 将在调用没有匹配期望的那一刻引发错误。更重要的是,它将为您提供有关不匹配调用的完整信息,包括任何参数的值。有了这些信息,您应该能够立即确定您的期望不匹配的原因。

      尝试将“失败”模拟设置为严格运行测试,看看它是否为您提供解决问题所需的信息。

      这是对您的测试的重写,模拟是严格的(折叠成一个方法以节省空间):

      [Test]
      public void load_article_by_title()
      {
          var article1 = new Mock<Article>();
          var mockArticleDao = new Mock<ArticleDAO>(MockBehavior.Strict); //mock set up as strict
          var mockDbFactory = new Mock<IDBFactory>(MockBehavior.Strict); //mock set up as strict
      
          mockDbFactory.Setup(x => x.GetArticleDAO()).Returns(mockArticleDao.Object);
          mockArticleDao.Setup(x => x.GetByTitle(It.IsAny<string>())).Returns(article1.Object);
      
          var articleManager = new ArticleManager(mockDbFactory.Object);
          articleManager.LoadArticle("some title");
      
          Assert.IsNotNull(articleManager.Article);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多