我正在尝试使用Specs2和Mockito来测试一些Scala代码。我对这三个都比较新,并且在模拟方法返回null时遇到了困难。
以下(转录了一些名称更改)
"My Component's process(File)" should {
"pass file to Parser" in new modules {
val file = mock[File]
myComponent.process(file)
there was one(mockParser).parse(file)
}
"pass parse result to Translator" in new modules {
val file = mock[File]
val myType1 = mock[MyType1]
mockParser.parse(file) returns (Some(myType1))
myComponent.process(file)
there was one(mockTranslator).translate(myType1)
}
}
“将文件传递给Parser”一直有效,直到我在SUT中添加翻译器调用,然后因为mockParser.parse
方法返回了null而无法翻译,因为翻译器代码无法执行该操作。
类似地,“将解析结果传递给译者”直到我尝试在SUT中使用翻译结果。
这两种方法的实际代码永远不会返回null,但我不知道如何告诉Mockito让期望返回可用的结果。
我当然可以通过在SUT中放置空检查来解决这个问题,但我宁愿不这样做,因为我确保永远不会返回空值而是使用Option
,None
和Some
。
指向一个好的Scala / Specs2 / Mockito教程的指针会很精彩,就像一个如何改变一行的简单例子一样
there was one(mockParser).parse(file)
使它返回允许在SUT不处理空值时继续执行的内容。
试图弄明白这一点,我试图将该行更改为
there was one(mockParser).parse(file) returns myResult
myResult的值是我想要返回的类型。这给了我一个编译错误,因为它希望在那里找到MatchResult
而不是我的返回类型。
如果重要,我正在使用Scala 2.9.0。
答案 0 :(得分:3)
如果您没有看到它,可以查看specs2文档的mock expectation page。
在您的代码中,存根应为mockParser.parse(file) returns myResult
在Don编辑后编辑:
有一种误解。你在第二个例子中的表现方式是好的,你应该在第一个例子中做同样的事情:
val file = mock[File]
val myType1 = mock[MyType1]
mockParser.parse(file) returns (Some(myType1))
myComponent.process(file)
there was one(mockParser).parse(file)
使用mock进行单元测试的想法总是一样的:解释你的模拟如何工作(存根),执行,验证。
那应该回答这个问题,现在是个人建议:
大多数情况下,除非您要验证某些算法行为(在第一次成功时停止,以相反的顺序处理列表),否则您不应在单元测试中测试期望。
在您的示例中,process
方法应该“翻译事物”,因此您的单元测试应该关注它:模拟您的解析器和翻译器,存根它们并仅检查整个过程的结果。它的细粒度较小,但单元测试的目标不是检查方法的每一步。如果要更改实现,则不必修改一组验证方法的每一行的单元测试。
答案 1 :(得分:1)
我设法解决了这个问题,虽然可能有更好的解决方案,所以我要发布自己的答案,但不要立即接受。
我需要做的是为模拟提供合理的默认返回值,形式为org.mockito.stubbing.Answer<T>
,其中T为返回类型。
我可以使用以下模拟设置执行此操作:
val defaultParseResult = new Answer[Option[MyType1]] {
def answer(p1: InvocationOnMock): Option[MyType1] = None
}
val mockParser = org.mockito.Mockito.mock(implicitly[ClassManifest[Parser]].erasure,
defaultParseResult).asInstanceOf[Parser]
在浏览了org.specs2.mock.Mockito
特征及其调用的内容之后。
现在,不是返回null,解析在没有存根时返回None
(包括在第一次测试时预期的那样),这允许测试通过在被测试的代码中使用此值
我可能会制作一个测试支持方法,隐藏mockParser
赋值中的混乱,并让我对各种返回类型执行相同操作,因为我将需要具有多个返回类型的相同功能这套测试。
我找不到支持在org.specs2.mock.Mockito
中执行此操作的更短方式,但这可能会激励Eric添加此类内容。很高兴让作者参与对话......
修改强>
进一步阅读源代码后,我发现我应该能够调用方法
def mock[T, A](implicit m: ClassManifest[T], a: org.mockito.stubbing.Answer[A]): T = org.mockito.Mockito.mock(implicitly[ClassManifest[T]].erasure, a).asInstanceOf[T]
在org.specs2.mock.MockitoMocker
中定义,这实际上是我上述解决方案的灵感来源。但我无法弄清楚这个电话。 mock
相当重载,我的所有尝试似乎最终都调用了不同的版本而不喜欢我的参数。
所以看起来Eric 已经已经包含对此的支持,但我不明白如何实现它。
<强>更新强>
我已经定义了一个包含以下内容的特征:
def mock[T, A](implicit m: ClassManifest[T], default: A): T = {
org.mockito.Mockito.mock(
implicitly[ClassManifest[T]].erasure,
new Answer[A] {
def answer(p1: InvocationOnMock): A = default
}).asInstanceOf[T]
}
现在通过使用该特性,我可以将我的模拟设置为
implicit val defaultParseResult = None
val mockParser = mock[Parser,Option[MyType1]]
在这个特定的测试中,我并不需要更多这方面的用法,因为为此提供一个可用的值使得我的所有测试都可以在测试中的代码中进行无效检查。但在其他测试中可能需要它。
我仍然对如何在不添加此特性的情况下处理此问题感兴趣。
答案 2 :(得分:0)
如果没有完整的话很难说,但是请你检查一下你试图模拟的方法不是最终的方法吗?因为在那种情况下,Mockito将无法模拟它并将返回null。
另一条建议是,当某些东西不起作用时,就是在标准的JUnit测试中用Mockito重写代码。然后,如果失败,Mockito邮件列表上的某个人可能会最好地回答您的问题。