我要测试的类是我的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中的方法是不可用的?
答案 0 :(得分:5)
前言:我不熟悉使用Moq(Rhino Mocks用户),所以我可能会错过一些技巧。
我在努力遵循这里的一些代码;正如马克·西曼(Mark Seemann)所指出的那样,我不明白为什么这种情况甚至可以在目前的状你能仔细检查代码吗?
有一点需要注意的是,你正在向文章管理员注入IDBFactory的模拟。然后你进行链接调用:
_article = _dbFactory.GetArticleDAO().GetByTitle(title)
您尚未提供GetArticleDAO
的实施方案。您只是嘲笑LoadByTitle
调用之后发生的GetARticleDAO
位。测试中模拟和链接调用的组合通常表明测试即将变得痛苦。
得墨忒耳法
突出点:Respect the Law of Demeter。 ArticleManager使用IDBFactory返回的IArticleDAO。除非IDBFactory做了非常重要的事情,否则你应该将IArticleDAO注入到ArticleManager中。
Misko雄辩地解释了为什么Digging Into Collaborators是一个坏主意。这意味着你有一个额外的挑战步骤来设置,也使API更加混乱。
此外,为什么将返回的文章作为字段存储在ArticleManager中?你可以回来吗?
如果可以进行这些更改,它将简化代码并使测试变得更容易。
您的代码将变为:
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可以使用伪造的持久性机制,其行为与数据库非常相似,以便进行测试。然后,您可以轻松地使用可帮助您以无痛方式测试ArticleManager的数据填充存储库。
模拟框架确实是很好的工具,但它们并不总是适合建立和验证复杂或连贯的交互;如果你需要在一次测试中模拟/存储多个东西(特别是嵌套的东西!),这通常表明测试是过度指定的,或者手动测试双重是更好的选择。
测试很难
......在我看来,如果你从模拟框架开始,那就更加困难了。我已经看到很多人因为在引擎盖下发生的“魔法”而与嘲弄框架结合在一起。因此,我一般主张远离他们until you're comfortable with hand-rolled stubs/mocks/fakes/spies etc。
答案 1 :(得分:1)
正如您目前提供的代码,我无法看到它编译 - 有两个原因。
第一个可能只是一个疏忽,但是ArticleManager类没有Article属性,但我认为它只返回_article字段。
另一个问题是这行代码:
_mockArticleDao.Setup(x => x.GetByTitle(It.IsAny<string>())).Returns(article1.Object);
据我所知,这根本不应该编译,因为ArticleDAO.GetByTitle
返回Article
,但是你告诉它返回IArticle
的实例(接口,不是具体的课程。)
您是否遗漏了代码说明中的内容?
无论如何,我怀疑问题在于Setup
这个问题。如果您错误地指定了设置,它永远不会被调用,并且Moq默认为其默认行为,即返回该类型的默认值(即,对于引用类型为null)。
可以通过设置DefaultValue属性来改变BTW的行为:
myMock.DefaultValue = DefaultValue.Mock;
但是,这不太可能解决你的这个问题,所以你能否解决我上面提到的问题,我相信我们可以弄清楚什么是错的。
答案 2 :(得分:0)
我不是Moq专家,但在我看来,问题在于你在嘲笑ArticleDAO,你应该嘲笑IArticleDAO。
这与你的问题有关:
我遇到的一个问题是,我想模仿IArticleDao,但由于ArticleDao类也继承自抽象类,如果我只是模拟了IArticleDao,那么GenericNHibernateDao中的方法是不可用的?
在mock对象中,您不需要从GenericNHibernateDao类继承的方法。您只需要模拟对象来提供参与测试的方法,即:GetByTitle。您通过模拟提供此方法的行为。
Moq不会模拟方法,如果它们已经存在于您尝试模拟的类型中。正如API docs:
中所述任何接口类型都可以用于模拟,但对于类,只能模拟抽象和虚拟成员。
具体来说,您的模拟GetByTitle将被忽略,因为模拟类型ArticleDao提供了此方法的(非抽象)实现。
因此,我的建议是模拟界面IArticleDao而不是类。
答案 3 :(得分:0)
正如Mark Seeman所提到的,由于.GetByTitle
期望返回错误的类型,我无法将其编译为“as-is”,从而导致编译时错误。
在纠正了这个问题并添加了缺少的文章属性之后,测试通过了 - 让我认为你的问题的核心在翻译时会因为你在SO上写下来而丢失。
但是,鉴于您报告了一个问题,我想我会提到一种让Moq本身可以帮助您确定问题的方法。
您获得null _articleManager.Article
的事实几乎可以肯定是因为没有匹配的期望.GetByTitle
。换句话说,您指定的那个不匹配。
通过将模拟切换到严格模式,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);
}