HMock——Haskell的灵活模拟框架
H锁定提供了灵活的Haskell的可组合模拟框架,其功能通常匹配或超过Java版Mockito、C++版GoogleMock和其他主流语言。
警告:Hmock的API可能很快就会改变。请确保使用鞋面绑定在版本号上。当前API可以很好地模拟MTL风格的课程。我希望HMock也能使用效果系统,仆人,haxl等。为了实现这一点,我需要对应用程序编程接口。
快速入门
-
为需要模拟的功能定义类。嘲笑任何事情使用HMock,需要使用莫纳德
子类。
导入前奏隐藏(readFile,writeFile)进口合格前奏曲类Monad m=>MonadFilesystem m,其中readFile::FilePath->m字符串writeFile::FilePath->String->m()实例MonadFilesystem IO,其中readFile=序曲.readFilewriteFile=序曲.writeFile
-
使用此类实现要测试的代码。
copyFile::MonadFilesystem m=>文件路径->文件路径->m()复制文件a b=读取文件a>>=写入文件b
-
制作课程可模拟
使用提供的Template Haskell拼接。
使Mockable[t|MonadFilesystem|]
-
设置期望值并运行代码。
test_copyFile::IO()test_copyFile=runMockT$do预期$ReadFile“foo.txt”|->“contents”预期$WriteFile“bar.txt”“contents”copyFile“foo.txt”“bar.txt”
运行MockT
在中运行代码模拟
单体变压器。
期待
期望只调用一次方法。
读取文件
和蓝色代码
匹配函数调用。它们已定义通过使可移动
.
|->
将方法调用与其结果分离。如果遗漏了方法将返回默认值(请参见数据。违约
),所以没有需要指定()
作为返回值。
为什么要嘲笑?
模型并不总是工作的合适工具,但它们发挥着重要作用在测试实践中。
-
如果可能,我们更喜欢用实际代码进行测试。哈斯克尔鼓励写作许多应用程序逻辑都具有纯函数,这可能是琐碎的测试。然而,这并不是所有的代码,错误很可能会出现在连接核心应用程序逻辑与其外部的粘合代码中影响。
-
如果无法测试实际代码,我们宁愿使用高质量虚假的实现。对于相对简单的效果,这些效果很好。然而,当外部系统复杂、指定不明确和/或经常更改。不完整或者,过于简化的假冒代码可以生成一些最容易出错的代码,例如错误处理和异常情况,很难测试。
-
模拟框架的使用允许程序员测试使用复杂有效的界面,包括讨厌的bug经常出现的所有黑暗角落隐藏起来。它们还有助于隔离测试失败:当组件损坏时,一个测试失败并且很容易找到,而不是所有下游测试都失败马上。
为什么要锁定?
HMock旨在帮助Haskell程序员在测试时养成良好的习惯带模拟。使用模拟进行测试时,需要注意一些危险:
-
过度使用当你的测试需要你不在乎的东西时关于。如果你读了两个文件,你通常不在乎它们是按哪个顺序排列的阅读,所以你的测试不需要命令。即使您的代码需要按照一定的方式行事,你通常不需要在每一次都检查测试。理想情况下,每个测试都应该测试一个属性。然而,过于简单化模拟的方法可能会迫使您仅为了运行代码而过度断言。
-
过度吸尘当您从您的代码,并最终假设要测试的部分逻辑。这使得你的测试没那么有用。同样,模拟的简单方法可以使您由于没有提供正确的选项来获得现实的行为,所以做了太多的事从你的方法。
-
脆性试验当你的期望过于匹配或意外的时间,导致不正确的行为,这只是模拟的问题。主流模拟框架并不特别组合,导致经常与意外规则发生冲突关闭时间和中断其他测试。
HMock旨在通过提供以下功能帮助您避免这些错误:
灵活的订购约束
使用HMock,可以选择对方法的顺序实施哪些约束。如果某些方法需要以固定的顺序发生,可以使用按顺序
检查一下。但是如果你不在乎订单,你就不需要检查。如果你根本不在乎某些方法,expect任何
会让你设置一个响应而不限制调用时间。使用预期N
,你可以方法是可选的,或者限制它的出现次数。
这些工具可以让您表达更多想要测试的确切属性,因此你不会陷入过度断言的陷阱。这也解锁了测试并发或其他非确定性代码的机会。
灵活的匹配器
在HMock中,通过使用谓语
s.答谓词a
本质上是a->布尔
,除了它可以打印以获得更好的错误消息。如果要匹配所有参数确切地说,这样做有一条捷径。但你也可以忽略你的论点不要在意,或者只对其价值做出部分断言。对于例如,您可以使用有基板
匹配日志消息中的关键字,无需复制整个字符串并将其粘贴到测试中。
因为您不需要比较每个参数,HMock甚至可以用于模拟参数没有的方法等式
实例。你可以写一个模拟例如,对于以函数作为参数的方法。你甚至可以模拟多态方法。
灵活的响应
在HMock中,对于调用方法时要做什么,您有很多选项。例如:
- 你可以看看这些论点。需要返回第三个参数吗?不问题;只需看看
行动
这已经过去了。
- 您可以调用其他方法。需要将一种方法转发给另一种方法吗?想要在不定义新类型和实例的情况下设置轻量级假?它是很容易做到。
- 您可以添加其他期望。需要确保每个打开的文件手柄关闭了吗?响应运行于
模拟T
,所以只需添加期望即可把手打开时。
- 您可以在基monad中执行操作。需要修改的某些状态复杂测试?需要保留信息日志,以便可以断言属性在测试结束时?只需运行测试
MockT(国家粮食局)
或MockT(作者[信息])
,然后致电得到
,放
,以及告诉
来自您的响应。
这些灵活的响应可以帮助您避免过度使用。您甚至可以使用HMock委托给一个轻量级假冒。这不仅避免了定义每个假实例都有一个新类型,但您也可以很容易地注入错误和作为伪实现例外的其他异常行为。
可组合模拟原语
HMock的期望扩展了Sveningsson等人在安嘲笑的表达语义.关键思想是提供合成基元。你能为单个调用,也可以对整个调用序列执行。你可以表达重复的调用序列,在两个选项之间进行选择等。因为核心模拟语言更具表现力,您不太可能需要使用二级机制,例如记录通话并稍后断言通话内容,你可能习惯于其他语言的方式,这也使你的测试更易于组合。
可重用模拟
使用HMock,模拟独立于特定的monad堆栈或代码使用的接口组合。您可以使用任何的组合可模拟
类,并且测试代码的每个部分仅取决于直接使用的类。这让您可以自由分享便利用于测试的库,并以不同的组合重用这些组件根据需要。
您还可以通过实现可模拟
手动类,将合理的默认值与派生的模拟绑定所有用户的实现。
可配置严重性
HMock允许您控制可能出现的几种情况的严重性测试期间。您可以将这些情况修改为忽略
日期:,触发a警告
但继续测试,或抛出错误
,通过传递严重程度为模拟
操作如下所述。
您可以调整其严重性的这些条件包括:
-
模棱两可的期望
当一个动作与多个动作匹配时,会出现模糊期望期望。默认情况下,这是忽略
d、 以及最近添加的选择期望值。
Svenningsson等人认为模糊性解决规则是不可组合的因此,增强歧义性对于可组合性是必要的。如果你同意,你可能想为模棱两可的期望设定更高的严重性。为此,请使用设置歧义检查
.
注意,与Sveningsson等人不同,HMock动态验证模糊性在运行时,因此只有当实际操作匹配超过一个期望。,而不是在这种情况只可能发生的时候。
-
无益行动
调用方法但没有预期时会发生无趣的操作已经为该方法添加了。默认情况下,这是一个错误
.然而,其他一些模拟框架(例如gMock)允许您忽略无趣的动作。要在HMock中执行此操作,请使用设置不感兴趣的操作检查
以更改此严重性。
忽略无趣的方法是不可组合的。添加对的期望测试的一部分中的方法将导致考虑该方法“有趣”,这可能导致测试中不相关的部分失败。
请注意setUninterestingActionCheck错误
(默认)实际处理毫无趣味的方法出乎意料。如果为设置较弱的严重性意外的操作、无趣的操作也会遵循这种严重性。
-
意外操作
当调用没有相应的期望。默认情况下,这是一个错误
注意,除非无趣的操作也设置为错误
,操作只是意外如果为该方法添加了至少一个期望。要更改意外操作的严重性,使用设置意外操作检查
.
这通常是一种临时的收集技术关于测试需要哪些期望的信息。你应该这样小心别让它这样。首选allow意外
如果你只是想忽略某些特定的意外操作。
-
无与伦比的期望
无与伦比的期望是指任何行动都无法匹配的期望测试期间发生的情况。默认情况下,这是一个错误
。您可以更改通过调用此严重性设置未满足预期检查
.
这通常是一种临时的收集技术关于测试需要哪些期望的信息。你应该这样小心别让它这样。
常见问题解答
以下是一些充分利用HMock的技巧。
两者的区别是什么|->
和|=>
?
在最常见的形式中,HMock规则包含表单的响应操作…->模拟时间m r
。该操作包含参数,并且模拟
monad可以用于添加期望值或在基础monad中执行操作。你可以使用构建这样的规则|=>
.
然而,很常见的情况是,您不需要这种灵活性,只需要以指定返回值。在这种情况下,您可以使用|->
而不是保留内容更具可读性。米|->r
是的缩写m|=>常数(返回r)
.
作为记忆区别的记忆装置,你可以想到:
|->
作为ASCII艺术↦
,它将函数与结果关联数学符号。
|=>
作为哈斯克尔的亲戚>>=
并将操作绑定到Kleisli箭头。
两者的区别是什么foo公司
,富
,以及富_
?
这三个名字有着微妙的不同含义:
foo公司
是您自己类的方法。这是代码中使用的函数你正在测试的。
富
是一个行动
表示方法调用的构造函数。你会通常在三个地方使用:当你知道确切的预期参数,作为mock方法
和朋友,以及作为模式以获取响应中的参数。
富_
是匹配器
构造函数,并期望谓语
可以匹配的参数以更通用的方式进行,但不指定其确切值。这比使用行动富
。您还必须使用富_
当方法缺少参数等式
或显示
实例。
我可以只模拟类的一些方法吗?
对!
这个使可移动
splice是为类设置mock的简单方法,并且将类中的所有内容委托给HMock以符合预期。然而,有时,您不能或不想将所有方法委托给H锁定。在这种情况下,请使用使用选项使Mockable
而是拼接,然后设置MockT的模拟派生
到False(错误)
。这实现了大多数更深入的样板文件对于HMock,但没有为定义实例模拟
。您将定义它您自己使用模拟方法
和朋友。
例如:
MonadFoo类,其中mockThis::String->m()但不此::Int->m字符串makeMockableWithOptions[t|MonadFoo|]def{mockDeriveForMockT=False}instance(Monad m,Typeable m)=>MonadFoo(MockT m),其中mockThis x=模拟方法(mockThis x)butNotThis _=return“假,非模拟”
如果你的类有HMock无法处理的方法,那么你必须这样做。其中包括关联类型、不在monad中运行的方法,或具有非-可键入
多态返回值。
如何模拟具有多态参数的方法?
HMock可以用于编写带有多态参数的模拟,但有几个要记住的怪癖。
首先,让我们区分两种类型的多态参数。考虑本课程:
类MonadPolyArgs a m其中foo::a->m()巴::b->米()
在foo公司
,参数类型一
受实例.实例绑定参数的行为在很大程度上与具体类型类似,但请查看关于多参数类型类的一些细节,请稍后提问。
在酒吧
,参数类型b条
受方法.正因为如此匹配器
对于酒吧
将被分配到rank-n类型(对于所有b.谓词b)->匹配器。。。
事实上,几乎只有谓语
你可以在这种类型中使用任何东西
(始终匹配,否注意参数值)。自等式
在这里是不合法的,相应的行动
类型将不会获得预期
实例,因此您可能不使用它将准确的呼叫匹配到酒吧
.
为了编写更具体的谓词,您需要向酒吧
在原来的课堂上。可以理解的是,你可能不愿意修改您的功能代码用于测试,但在本例中没有替代方案。添加到方法中的任何约束都可以用于谓语
中的匹配器
例如,如果酒吧
可以修改以添加可输入
约束,然后可以使用类似于键入@Int(lt 5)
,只匹配以下情况的呼叫b条
是国际
,也小于5。
如何模拟具有多态返回类型的方法?
同样,我们可以区分实例绑定的类型变量与方法。实例绑定的变量的工作方式与具体的变量基本相同类型,但请查看有关一些多参数类型类的问题细节。
要模拟具有由方法本身绑定的多态返回值的方法返回值必须具有可键入
约束。如果无法推断,您还需要将类型注释添加到为方法,以便GHC知道类型。如果该方法将用于不同的返回类型,则必须在中为每个类型的方法添加单独的期望它将被使用。
为什么需要默认实例模拟方法
?
模拟方法
使用违约
类来自数据默认值
决定做什么当没有对期望给出其他响应时返回。如果你的方法嘲弄没有违约
实例的返回类型,可以使用mockDefaultless方法
而不是。在这种情况下,如果没有响应指定时,该方法将返回未定义
.
如果派生的实例模拟
使用模板Haskell。
如何更改模拟方法的默认行为?
有几种方法可以做到这一点:
- 要只更改默认行为,请使用
默认情况下
。方法调用必须仍然是预期的,否则它将失败,但如果在期望值,将使用此默认值。
- 要成功地对方法进行意外调用,请使用
allow意外
.如果如果在参数中包含响应,它将成为除了允许意外的方法之外。
- 设置默认值的步骤全部的模拟的用户,将您的调用替换为
使可移动
打电话给使用选项使Mockable
和设置模拟清空设置
到False(错误)
。然后为写一个实例可模拟
和实施设置Mockable
做你喜欢做的事。此设置将始终在HMock第一次接触任何测试中的类之前运行。
如何阻止我不在乎的意外行为导致测试失败?
- 如果有一种你不关心的特定方法,请使用
allow意外
忽略该方法的出现。
- 还有一种启发式方法可以启用,即所谓的“无趣”方法被忽略。无趣的方法是指没有期望的方法在整个测试中添加。要使这些方法成功,请使用
setUntrestingActionCheck忽略
或setUninterestingActionCheck警告
.这与gMock的行为类似,后者具有类似的概念无趣的通话。
- 最后,最大的锤子是使用
setUnexpectedActionCheck警告
.这将允许您在测试中执行任何意外操作。你几乎可以肯定不想在期末考试中这样做,但它在编写测试的过程。
如果有两个期望与相同的方法相匹配怎么办?
默认情况下,匹配最近添加的期望。想想期望值是一个堆栈,因此它们是第一个匹配的。这个规则使HMock更具组合性,因为您可以在测试的一部分,不用担心来自更大范围的阻塞会干扰。
当多个期望匹配时,也可以选择失败。收件人启用此功能,只需包含setAmigityCheck为True
作为中的语句模拟
.从那时起,不明确的匹配将引发错误。
如何模拟多参数类型的类?
为了模拟多参数类型的类,monad参数米
必须是最后一个类型变量。那就用可移动[t|MonadMPTC|]
.
如何模拟具有函数依赖性的类?
我们将考虑该表单的类
类MonadMPTC a b c m | m->a b c
如果您尝试使用可移动[t|MonadMPTC|]
,它将失败。功能依赖性要求一
,b条
,以及c
取决于米
,但我们不能为模拟
实例。
处理这种情况的建议方法是将混凝土类型传递给使可移动
,如下所示:
makeMockable[t|MonadMPTC Int字符串Int|]
这将定义相同的可模拟
的实例MonadMPTC公司
,但实例对于模拟
将根据职能部门的要求定义具体类型附属国。
请注意模拟
实例是反模块的,因为您不能导入(甚至间接)两个不同的实例模拟
具有不同类型的相同模块。这些实例将是不相干的Haskell通常不会这么做允许。您可以通过使用使可模仿
在顶级测试中从未在其他地方导入的模块。如果您想共享期望代码,您可以使用使用选项使Mockable
和设置MockT的模拟派生
到False(错误)
在库代码中,然后使用使可移动
再次在顶级测试模块中定义模拟
实例。
如果您绝对需要在同一模块中使用不同的类型参数,则需要在基monad周围使用包装器模拟
消除实例的歧义。这有点复杂。这里有一个例子:
makeMockableWithOptions[t|MonadMPTC|]def{mockDeriveForMockT=False}newtype MyBase m a=MyBase{runMyBase::m a}派生(Functor、Applicative、Monad)实例(单幅m,可打印m)=>MonadMPTC字符串Int字符串(MockT(MyBase m))哪里foo x=模拟方法(foo x)
如何测试多线程代码?
如果您的代码使用MonadUnliftIO公司
要创建线程,可以直接测试它使用HMock。否则,您可以使用使用MockT
手动注入每个将同一个线程模拟
块。无论你以何种方式做,期望在线程之间共享,以便在一个线程中添加期望由另一方完成。
如果你不想分享期望,那么你可以使用运行MockT
每一次线程来运行每个线程及其自己的期望集。
如何测试有异常的代码?
您可以使用例外
或不自由的
要抛接的包裹使用测试的代码异常模拟
.
的行为H锁定
如果您在投掷后继续测试,则未指定线程的异步异常使用throwTo(通过)
。这种担心不会应用于引发的同步异常通过IO
或通过M
.
如何获得更好的堆栈跟踪?
HMock与使用HasCallStack(HasCallStack)
。这些可能非常便于查找代码出错的地方。然而,堆栈除非添加HasCallStack(哈斯调用堆栈)
对方法的约束你的班级。这很不幸,但在当前形势下,这是无法避免的哈斯克尔州。您可以在故障排除时添加约束,然后删除等你做完了再做一次。
我应该如何处理孤立实例警告?
如果启用了警告,GHC通常会对孤立实例发出警告当你使用使可移动
。我们建议为禁用此警告使用的模块使可移动
,通过添加行{-#OPTIONS_GHC-Wno-orphans#-}
到这些模块的顶部。
禁止孤立实例只是一种启发式的以降低这种可能性将为同一类型的类和参数定义两个不同的实例。启发式方法适用于大多数应用程序代码。确实如此不这样工作对于HMock来说,因为您模拟的类是非测试代码,但可移动底座
,可模拟
,以及模拟
实例应该在测试中定义代码。
由于孤立启发式不起作用,您必须负责管理多个实例的风险。最简单的方法是避免在库中定义这些实例。如果您确实在库中定义了实例,您必须为每个实例选择一个跨实例一致的规范位置所有使用库的代码。
为什么我的方法“太复杂了,无法用行动
”“?
添加期望时,只能使用行动
如果方法简单够了。具体来说,所有参数都必须具有等式
和显示
实例,没有参数可能依赖于该方法绑定的类型变量。
如果你的方法不够简单,解决方案是用一匹配器
而不是行动
。的参数匹配器
是谓语
秒它可以检查值并决定是否匹配。你现在有责任了决定如何匹配复杂参数。一些选项包括:
- 使用多态
谓语
喜欢任何东西
.
- 确保
可键入
约束可用,并且使用键入
谓词将参数强制转换为已知类型。
- 使用
是
或具有
谓语
s和您自己的多态代码类型并生成可以匹配的单态结果类型。
如何从迁移单体模拟
?
用单体模拟
包,您将使用该库的模板Haskell拼接调用make操作
。使用HMock时,应该使用使可移动
而不是。不同makeAction(生成操作)
,您将使用使可模仿
对于你想要模仿的每一个类分别进行。生成的代码仍然是可用于同一测试中其他类的任何组合。
您以前可能写过:
makeAction“MyAction[ts|MonadFilesystem,MonadDB|]
现在您将写下:
使Mockable[t|MonadFilesystem|]使Mockable[t|MonadDB|]
转换使用单体模拟
使用HMock进入测试,移动列表参数的期望运行MockT
进入之内期待
呼叫内部H锁定运行MockT
。要保留旧测试的准确行为,请将期待
s与按顺序
。您还需要从单体模拟
的:->
到HMock’s|->
,意思是一样的。
如果您以前写过(使用monad-mock):
运行MockT[ReadFile“foo.txt”:->“contents”,WriteFile“bar.txt”“contents”:->()](copyFile“foo.txt”“bar.txt”)
现在您将编写以下内容(使用HMock):
runMockT$do按顺序[预期$ReadFile“foo.txt”|->“contents”,预期$WriteFile“bar.txt”“contents”|->()]copyFile“foo.txt”“bar.txt”
既然您的测试已经在不改变其行为的情况下进行了迁移,那么您可以开始删除以下断言单体模拟
强迫你写作,即使你不打算测试它们。例如:
应用这两种简化方法,您将进行最终测试:
runMockT$do预期$ReadFile“foo.txt”|->“contents”应为$WriteFile“bar.txt”“内容”copyFile“foo.txt”“bar.txt”
支持哪些GHC版本?
HMock使用8.6到9.4的GHC版本进行测试。
案例研究:模仿模板Haskell
作为HMock使用中的一个非平凡案例研究,请考虑测试问题使用Template Haskell的代码。模板Haskell在名为准
,提供对帮助构建代码的操作的访问:生成新的名称、查找类型信息、报告错误和警告等。虽然有一个IO(输入输出)
实例准
类型类,它为抛出错误大多数操作,使其不适合测试模板Haskell。
作为HMock自己的测试套件的一部分准
monad是可模仿的(in测试/准模拟.hs
),然后使用(在测试/类别.hs
)测试HMock基于Haskell模板的代码生成。
乍一看,这似乎没有必要。毕竟,单元测试使使用使可移动
用于测试核心HMock功能,因此任何该代码中的问题会导致其中一个核心测试失败好。然而,使用模拟编写这些测试有两个显著的好处:
-
因为Template Haskell在编译时运行,所以测试覆盖率不能仔细斟酌的。模板Haskell代码在运行时生成准确的测试覆盖率使用高性能计算机
这反过来又有助于编写更全面的测试。
-
因为Template Haskell错误会阻止测试编译测试只能覆盖成功的使用。嘲笑准
允许测试模板Haskell也用于检查错误情况。
真的,嘲笑准
测试对HMock开发非常有价值。首先最初的测试揭示了一些测试没有运用关键逻辑的地方:用超类模拟类,以及方法等级为n的模拟类参数。当添加了相应的测试后,这两种情况都得到了证实不正确!接下来,为错误案例添加测试(可以在没有mock的情况下编写)揭示了检测mock的代码参数太多的类也被破坏了,因此HMock打印了一些关于内部错误的信息,建议用户报告错误。
实施准
模仿并不难,但也有一些它照亮的地方:
-
的几种方法准
HMock无法模拟类型类,因为它们有多态的返回类型,没有可输入
约束。这是真的没有阻止使用HMock,但确实需要使用手写模拟
实例,而不是使用使可移动
生成所有内容。
-
一种方法,q新名称
,本可以被嘲笑,但这是不对的选择这样做。模板Haskell已经在IO(输入输出)
monad,它已经适合测试了。这不是问题,因为这个模拟
实例可以写入以转发到IO(输入输出)
实施。
-
某些确实需要嘲笑的行为,例如抬头看等式
和显示
公共类型的实例对于许多不同的测试都很有用。收件人有助于重用,这些操作是从设置Mockable
所以他们是每次使用类时自动模拟。
-
模拟和设置代码需要少于50行非常笔直的代码代码。不过,需要编写更多的代码来派生电梯
和NFData公司
Template Haskell类的实例,以便正确的行为可以实现和测试模拟的。
综上所述,模仿加西并不难实现,而且提高了HMock开发的经验和对其的信心正确性。