使用 Moq 的单元测试未通过,对象为空,我错过了什么吗?
我想要测试的类是我的 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>();
}
}
[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 中的方法不可用?
The class I want to test is my ArticleManager class, specifically the LoadArticle method:
public class ArticleManager : IArticleManager
{
private IArticle _article;
public ArticleManger(IDBFactory dbFactory)
{
_dbFactory = dbFactory;
}
public void LoadArticle(string title)
{
_article = _dbFactory.GetArticleDAO().GetByTitle(title);
}
}
My ArticleDAO looks like:
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>();
}
}
My test code using NUnit and Moq:
[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);
}
The unit test is failing, the object _articleManager.Article is returning NULL.
Have I done everything correctly?
This is one of my first unit tests so I am probably missing something obvious?
One issue I had, was that I wanted to mock IArticleDao but since the class ArticleDao also inherits from the abstract class, if I just mocked IArticleDao then the methods in GenericNHibernateDao are not available?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
前言:我不熟悉使用 Moq(这里是 Rhino Mocks 用户),所以我可能会错过一些技巧。
我正在努力遵循这里的一些代码;正如马克·西曼(Mark Seemann)指出的那样,我不明白为什么它会在当前状态下编译。请您仔细检查一下代码好吗?
值得注意的一件事是,您正在将 IDBFactory 的模拟注入到文章管理器中。然后,您进行链式调用:
您尚未提供
GetArticleDAO
的实现。您仅模拟了GetARticleDAO
调用之后发生的LoadByTitle
位。测试中模拟和链式调用的组合通常表明测试将变得痛苦。德墨忒尔定律
此处的要点:尊重德墨忒尔定律。 ArticleManager 使用 IDBFactory 返回的 IArticleDAO。除非 IDBFactory 做了一些非常重要的事情,否则您应该将 IArticleDAO 注入到 ArticleManager 中。
Misko 雄辩地解释了为什么挖掘协作者是一个坏主意。这意味着您需要执行额外的繁琐步骤来设置,并且还会使 API 更加混乱。
此外,为什么要将返回的文章作为字段存储在 ArticleManager 中?你能直接退货吗?
如果可以进行这些更改,则将简化代码并使测试变得更加容易 10 倍。
您的代码将变成:
您将拥有一个更简单的 API,并且测试起来会容易得多,因为嵌套已经消失。
依赖持久性使测试变得更容易
在我对与持久性机制交互的代码进行单元测试的情况下,我通常使用 存储库模式并创建手动的、虚假的内存存储库以帮助测试。它们通常编写起来也很简单——它只是实现 IArticleRepository 接口的字典的包装。
使用这种技术允许您的 ArticleManager 使用假持久性机制,其行为与用于测试目的的数据库非常相似。然后,您可以轻松地用数据填充存储库,以帮助您以轻松的方式测试 ArticleManager。
模拟框架确实是很好的工具,但它们并不总是适合设置和验证复杂或连贯的交互;如果您需要在一个测试中模拟/存根多个内容(特别是嵌套的内容!),这通常表明测试过度指定,或者手动测试替身将是更好的选择。
测试很难
...在我看来,如果你从模拟框架开始,那就加倍困难。我见过很多人因为幕后发生的“魔法”而与嘲笑框架纠缠在一起。因此,我通常建议远离它们直到您对手卷存根/模拟感到满意为止/fakes/spies 等。
Preface: I'm not familiar with using Moq (Rhino Mocks user here) so I may miss a few tricks.
I'm struggling to follow some of the code here; as Mark Seemann pointed out I don't see why this would even compile in its current state. Can you double check the code, please?
One thing that sticks out is that you're injecting a mock of IDBFactory into Article manager. You then make a chained call of:
You've not provided an implementation of
GetArticleDAO
. You've only mocked theLoadByTitle
bit that happens after theGetARticleDAO
call. The combination of mocks and chained calls in a test are usually a sign that the test is about to get painful.Law of Demeter
Salient point here: Respect the Law of Demeter. ArticleManager uses the IArticleDAO returned by IDBFactory. Unless IDBFactory does something really important, you should inject IArticleDAO into ArticleManager.
Misko eloquently explains why Digging Into Collaborators is a bad idea. It means you have an extra finicky step to set up and also makes the API more confusing.
Furthermore, why do you store the returned article in the ArticleManager as a field? Could you just return it instead?
If it's possible to make these changes, it will simplify the code and make testing 10x easier.
Your code would become:
You would then have a simpler API and it'd be much easier to test, as the nesting has gone.
Making testing easier when relying on persistence
In situations where I'm unit testing code that interacts with persistence mechanisms, I usually use the repository pattern and create hand-rolled, fake, in-memory repositories to help with testing. They're usually simple to write too -- it's just a wrapper around a dictionary that implements the IArticleRepository interface.
Using this kind of technique allows your ArticleManager to use a fake persistence mechanism that behaves very similarly to a db for the purpose of testing. You can then easily fill the repository with data that helps you test the ArticleManager in a painless fashion.
Mocking frameworks are really good tools, but they're not always a good fit for setting up and verifying complicated or coherent interactions; if you need to mock/stub multiple things (particularly nested things!) in one test, it's often a sign that the test is over-specified or that a hand-rolled test double would be a better bet.
Testing is hard
... and in my opinion, doubly hard if you start with mocking frameworks. I've seen a lot of people tie themselves in knots with mocking frameworks due to the 'magic' that happens under the hood. As a result, I generally advocate staying away from them until you're comfortable with hand-rolled stubs/mocks/fakes/spies etc.
正如您当前提供的代码一样,我看不到它可以编译 - 有两个原因。
第一个可能只是一个疏忽,但 ArticleManager 类没有 Article 属性,但我假设它只是返回 _article 字段。
另一个问题是这行代码:
据我所知,这根本不应该编译,因为
ArticleDAO.GetByTitle
返回Article
,但你告诉它返回 IArticle 的实例(接口,而不是具体类)。您在代码描述中是否遗漏了一些内容?
无论如何,我怀疑问题出在这个
Setup
调用上。如果您错误地指定了设置,则它永远不会被调用,并且 Moq 默认为其默认行为,即返回类型的默认值(即,引用类型为 null)。顺便说一句,可以通过设置 DefaultValue 属性来更改这种行为,如下所示:
但是,这不太可能解决您的这个问题,所以您可以解决我上面指出的问题吗?我相信我们可以解决这个问题怎么了。
As you have currently presented the code, I can't see that it compiles - for two reasons.
The first one is probably just an oversight, but the ArticleManager class doesn't have an Article property, but I assume that it simply returns the _article field.
The other problem is this line of code:
As far as I can see, this shouldn't compile at all, since
ArticleDAO.GetByTitle
returnsArticle
, but you are telling it to return an instance ofIArticle
(the interface, not the concrete class).Did you miss something in your description of the code?
In any case, I suspect that the problem lies in this
Setup
call. If you incorrectly specify the setup, it never gets called, and Moq defaults to its default behavior which is to return the default for the type (that is, null for reference types).That behavior, BTW, can be changed by setting the DefaultValue property like this:
However, that's not likely to solve this problem of yours, so can you address the issues I've pointed out above, and I'm sure we can figure out what's wrong.
我不是 Moq 专家,但在我看来,问题在于你在嘲笑 ArticleDAO,而你应该嘲笑 IArticleDAO。
这与你的问题有关:
在模拟对象中,您不需要从 GenericNHibernateDao 类继承的方法。您只需要模拟对象来提供参与测试的方法,即:GetByTitle。您可以通过模拟提供此方法的行为。
如果方法已经存在于您尝试模拟的类型中,则 Moq 不会模拟方法。正如 API 文档中指定的:
具体来说,您对 GetByTitle 的模拟将被忽略,因为模拟类型 ArticleDao 提供了此方法的(非抽象)实现。
因此,我给你的建议是模拟接口 IArticleDao 而不是类。
I am not a Moq expert but it seems to me that the problem is in you mocking ArticleDAO where you should be mocking IArticleDAO.
this is related to your question:
In the mock object you don't need the methods inherited from the GenericNHibernateDao class. You just need the mock object to supply the methods that take part in your test, namely: GetByTitle. You provide the behavior of this method via mocking.
Moq will not mock methods if they already exist in the type that you're trying to mock. As specified in the API docs:
Specifically, your mocking of GetByTitle will be ignored as the mocked type, ArticleDao, offers a (non-abstract) implementation of this method.
Thus, my advise to you is to mock the interface IArticleDao and not the class.
正如 Mark Seeman 所提到的,我无法让它“按原样”编译,因为
.GetByTitle
期望返回错误的类型,从而导致编译时错误。纠正这个问题并添加缺少的 Article 属性后,测试通过了 - 让我认为你的问题的核心在某种程度上在翻译中丢失了,因为你把它写在了 SO 上。
然而,考虑到您正在报告问题,我想我应该提到一种方法,可以让 Moq 本身帮助您识别问题。
您得到 null
_articleManager.Article
的事实几乎可以肯定是因为没有匹配的期望.GetByTitle
。换句话说,您指定的不匹配。通过将模拟切换到严格模式,Moq 将在进行与期望不匹配的调用时引发错误。更重要的是,它将为您提供有关不匹配调用的完整信息,包括任何参数的值。有了这些信息,您应该能够立即确定为什么您的期望不匹配。
尝试使用严格的“失败”模拟集运行测试,看看它是否为您提供解决问题所需的信息。
这是测试的重写,模拟严格(折叠成单个方法以节省空间):
As mentioned by Mark Seeman, I couldn't get this to compile "as-is" as the
.GetByTitle
expectation returns the wrong type, resulting in a compile-time error.After correcting this, and adding the missing Article property, the test passed - leading me to think that the core of your problem has somehow become lost in translation as you wrote it up on SO.
However, given you are reporting a problem, I thought I'd mention an approach that will get Moq itself to help you identify your issue.
The fact you are getting a null
_articleManager.Article
is almost certainly because there is no matching expectation.GetByTitle
. In other words, the one that you do specify is not matching.By switching your mock to strict mode, Moq will raise an error the moment a call is made that has no matching expectation. More importantly, it will give you full information on what the unmatched call was, including the value of any arguments. With this information you should be able to immediately identify why your expectation is not matching.
Try running the test with the "failing" mock set as strict and see if it gives you the information you need to solve the problem.
Here is a rewrite of your test, with the mock as strict (collapsed into a single method to save space):