您可以“模拟”(或“伪造”)依赖项,而不必超越IO
。
您的程序逻辑通过putStrLn
和getLine
与外部世界进行交互。但是,他们对他们有什么了解?实际上,除了它们的类型String -> IO ()
和IO String
之外,什么都没有。
因此我们可以将它们抽象化,将您的程序逻辑转换为函数myProgramLogic :: (String -> IO ()) -> IO String -> IO ()
,该函数将有效的操作作为参数接收。将依赖项作为函数参数传递是依赖项注入的低热量版本。
现在问题变成了:如何模拟putStrLn
和getLine
。显然,由于我们要进行自动测试,因此模拟不能是交互式的。但是它们也不会是像\_ -> return ()
和return "foo"
这样总是在做同样事情的无聊动作。他们必须具有状态,必须记录与程序逻辑的交互。
标准库中有一个名为Data.IORef
的模块,可让您创建和操作IO
中的可变引用。其他语言将其称为“只是一个无聊的常规变量”。
这段代码创建了一个包含字符串列表的可变引用,并且还定义了一个pseudoGetLine
函数,该函数每次执行时都会提取其中一个字符串:
main :: IO ()
main = do
inputsRef <- newIORef ["foo","bar","baz"]
let pseudoGetLine :: IO String
pseudoGetLine = do
atomicModifyIORef inputsRef (\inputs ->
case inputs of
i : is -> (is,i) -- the i becomes the return value of pseudoGetLine
[] -> error "fake inputs exhausted")
sample <- pseudoGetLine
print sample
您可以看到变化的方向:伪造两个依赖项并将它们传递给逻辑后,可以检查可变引用的状态(使用类似readIORef
的函数),以检查它们是否是预期的。
,
即使在Java或C#等面向对象的环境中,我也无法在Main
方法上使用Test Doubles('mocks'),因为您可以在入口点不进行依赖注入;它的签名是固定的。
您通常要做的是定义一个带有依赖项的MainImp
,然后使用Test Doubles进行测试,而将实际的Main
方法保留为Humble Executable。 / p>
生产代码
您可以在Haskell中执行相同的操作。一种简单的方法是按照 danidiaz 的建议进行操作,并将不纯行为作为参数传递给mainImp
:
mainImp :: Monad m => m String -> (String -> m ()) -> m ()
mainImp getInput displayOutput = do
displayOutput "What should I calcuclate? ex 3*(2+2) | quit to exit"
line <- getInput
if line /= "quit"
then do if correctInput line
then do displayOutput $ show $ calculate line
mainImp getInput displayOutput
else do displayOutput "Wrong input"
mainImp getInput displayOutput
else displayOutput "goodbye"
请注意,类型声明显式允许任何Monad m
。其中包括IO
,这意味着您可以像这样编写实际的main
动作:
main :: IO ()
main = mainImp getLine putStrLn
但是,在测试中,您可以使用另一个monad。通常,State
非常适合此任务。
测试
您可以从适当的导入开始测试模块:
module Main where
import Control.Monad.Trans.State
import Test.HUnit.Base (Test(..),(~:),(~=?),(@?))
import Test.Framework (defaultMain)
import Test.Framework.Providers.HUnit
import Q58750508
main :: IO ()
main = defaultMain $ hUnitTestToTests $ TestList tests
这使用了HUnit,稍后您将看到inline the tests in a list literal。
但是,在进行测试之前,我认为定义一个可以保留控制台状态的特定于测试的类型是有意义的:
data Console = Console { outputs :: [String],inputs :: [String] } deriving (Eq,Show)
您还需要一些与getInput
和displayOutput
相对应的功能,但必须在State Console
单子而不是IO
中运行。这是a technique that I've described before。
getInput :: State Console String
getInput = do
console <- get
let input = head $ inputs console
put $ console { inputs = tail $ inputs console }
return input
请注意,此函数不安全,因为它使用head
和tail
。我将其保留为安全性练习。
它使用get
检索控制台的当前状态,拉出head
的“队列”中的inputs
,并在返回input
之前更新状态
同样,您可以在displayOutput
monad中实现State Console
:
displayOutput :: String -> State Console ()
displayOutput s = do
console <- get
put $ console { outputs = s : outputs console }
这只会使用提供的String
更新状态。
您还需要一种在State Console
monad中运行测试的方法:
runStateTest :: State Console a -> a
runStateTest = flip evalState $ Console [] []
这总是以空inputs
和空outputs
开始任何测试,因此作为测试作者,您有责任确保inputs
始终以"quit"
结尾。您也可以编写一个辅助函数来执行此操作,或更改runStateTest
以始终包含此值。
那么一个简单的测试是:
tests :: [Test]
tests = [
"Quit right away" ~: runStateTest $ do
modify $ \console -> console { inputs = ["quit"] }
mainImp getInput displayOutput
Console actual _ <- get
return $ elem "goodbye" actual @? "\"goodbye\" wasn't found in " ++ show actual
-- other tests go here...
]
此测试只是验证如果您立即"quit"
,则出现"goodbye"
消息。
涉及程度更高的测试可能是:
,"Run single calcuation" ~: runStateTest $ do
modify $ \console -> console { inputs = ["3*(2+2)","quit"] }
mainImp getInput displayOutput
Console actual _ <- get
let expected =
[ "What should I calcuclate? ex 3*(2+2) | quit to exit","12","What should I calcuclate? ex 3*(2+2) | quit to exit","goodbye"]
return $ expected ~=? reverse actual
您可以将其插入到以上]
列表的结尾tests
之前,其中注释为-- other tests go here...
。
关于与Haskell进行单元测试的文章比我链接的文章多,因此请确保遵循那里的链接,并调查其他the Haskell tag和{{3 }}。
本文链接:https://www.f2er.com/3144203.html