编写软件的文档可能很有挑战性,但也不一定要这样。
在这篇文章中,我们将介绍doctests:一个使文档过程变得愉快和有效的概念。
通过将测试放在模块文档中,doctests允许功能自己说话,并帮助你从你的测试工作中获得最大的收益。
阅读这篇文章来了解。
- 什么是doctests?
- 如何在Haskell中定义它们。
- 使用哪个库来进行Haskell测试。
什么是doctests?
Doctests是简单的嵌入在文档中的文本片段,看起来像交互式会话。 通过一个特殊的库,你可以运行这些会话并验证它们是否返回正确的值。
这个想法来自于Python的令人敬畏的doctest模块,但从那时起,它已经扩散到几乎所有的编程语言。
在Haskell中,doctests是Haddock注释中的GHCi会话。
如果你曾经不得不略过Haskell的源代码文档,你可能已经注意到那些在开头有花哨的>>> 符号的行。
-- | @const x@ is a unary function which evaluates to @x@ for all inputs.
--
-- >>> const 42 "hello"
-- 42
--
-- >>> map (const 42) [0..3]
-- [42,42,42,42]
正如你可能已经猜到的,那些是测试。
如何在Haskell中定义doctest?
让我们来看看一个基本测试的例子。
-- | 1 + 2 is 3.
-- >>> 1 + 2
-- 3
正如你所看到的,Haskell doctests有三个要求。
-
每一个doctest的例子都应该放在有效的Haddock文档中,其标志是
-- |或{- |。 -
每个doctest例子都应该以
>>>开始,并包含一个有效的Haskell表达式,该表达式在范围内(有时你必须明确import语句 - 我们将在文章中进一步介绍)。 -
每个doctest例子后面都应该有一行,包含评估该表达式的预期结果。
你可以在doctest 库的readme中找到关于doctest标记的额外信息。
现在,让我们创建一个带有doctests的项目,并看看你可以用来运行这些doctests的Haskell库。
创建一个带有doctests的Haskell项目
为了使用测试库,我们需要创建一个带有doctests的Haskell项目。
首先,用以下方法启动一个项目 stack:
stack new doctests-demo
之后,进入项目的根目录,创建一个Haskell模块。
cd doctests-demo
touch src/Sample.hs
最后,在新创建的Sample.hs 模块中添加一些带有doctests的函数。
-- src/Sample.hs
module Sample where
-- |
-- >>> foo + 13
-- 55
foo :: Integer
foo = 42
-- |
-- >>> bar
-- "bar"
bar :: String
bar = "bar"
该项目现在已经准备好运行doctests了。
Haskell中的Doctest库
我们将介绍Haskell生态系统中的两个doctest库。 doctest和 cabal-docspec.第一个比较老,也比较流行,第二个不太流行,但解决了doctest'的一些问题。
doctest
doctest语法库是最常用的、维护最积极的语法库之一,同时,它也有一些缺点,比如对大规模项目来说性能不好,以及对GHC这个库的依赖性。
如何使用doctest
首先,通过stack 安装该库。
stack install doctest
之后,进入项目的根目录,使用该库的可执行程序。
cd doctests-demo
doctest src
上面的命令应该输出这样的结果。
Examples: 2 Tried: 2 Errors: 0 Failures: 0
使用的缺点doctest
虽然它是一个很棒的软件,但该库也有一些缺点。
最大的缺点是该库对于大型项目来说似乎太慢了。 导致性能问题的原因之一是该库在每一组doctest例子之间都会重新加载源代码。 这样做是为了避免例子组之间相互影响。 你可以在该库的readme中了解更多这方面的信息。
还有几个比较小的缺点。
首先,它依赖于GHC这个库,这意味着当你把项目切换到一个新的编译器版本时,很可能最终会出现问题。
其次,当通过 stack时,你可能需要创建一个额外的测试套件来将项目的依赖性纳入范围。
依赖性缺点的说明。
让我们通过稍微编辑一下我们的doctest例子来说明这个问题(别忘了把aeson 和text 加入到依赖关系列表中)。
-- src/Sample.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Sample where
import Data.Aeson
import Data.Aeson.TH
import Data.Text
data Anime =
Anime { title :: Text
, rating :: Double
}
$(deriveJSON defaultOptions ''Anime)
-- |
-- >>> encode favourite
-- "{\"title\":\"One-Punch Man\",\"rating\":8.9}"
--
favourite :: Anime
favourite = Anime "One-Punch Man" 8.9
现在,如果我们试图使用doctest src 来运行这些测试,我们会失败,出现以下信息。
src/Sample.hs:9:1: error:
Could not find module ‘Data.Aeson’
Perhaps you meant Data.Version (from base-4.14.3.0)
Use -v (or `:set -v` in ghci) to see a list of the files searched for.
|
9 | import Data.Aeson
| ^^^^^^^^^^^^^^^^^
解决办法是创建一个额外的测试套件。
tests:
doctests:
source-dirs: doctests
main: Main.hs
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- doctest
# bring in your project into the scope
# as well as its dependencies
- doctests-demo
之后,你需要添加测试套件目录和Main.hs 可执行文件。
mkdir doctests
touch doctests/Main.hs
-- doctests/Main.hs
import Test.DocTest
-- This test suite exists only to add dependencies
main :: IO ()
main = doctest ["src"]
最后,使用以下命令运行测试。
stack test :doctests
cabal-docspec
cabal-docspec在这个库中,贡献者较少,社区的整体关注度也较低,但已经有很多人在实际项目中使用它。
如何使用cabal-docspec
让我们使用cabal-docspec 来运行我们的doctests-demo 项目中的 doctests。
首先,设置cabal-install 和全局编译器。我们建议使用ghcup来实现这一点。
之后,用Cabal构建项目。
cabal v2-build
然后从发布页面下载cabal-docspec 二进制文件。
curl -sL https://github.com/phadej/cabal-extras/releases/download/cabal-docspec-0.0.0.20211114/cabal-docspec-0.0.0.20211114.xz > cabal-docspec.xz
xz -d < cabal-docspec.xz > "$HOME"/.local/bin/cabal-docspec
rm -f cabal-docspec.xz
chmod a+x "$HOME"/.local/bin/cabal-docspec
现在,运行doctest的例子。
cabal-docspec
上面的命令应该失败,出现以下错误。
expected: "{\"title\":\"One-Punch Man\",\"rating\":8.9}"
but got:
^
<interactive>:10:1: error:
Variable not in scope: encode :: Anime -> t
出现上述情况是因为库的处理方式有些不同--它要求模块被明确地导入/导出。
让我们编辑一下我们的doctests,使cabal-docspec 工作。
-- |
+ -- >>> import Data.Aeson
-- >>> encode favourite
-- "{\"title\":\"One-Punch Man\",\"rating\":8.9}"
--
favourite :: Anime
favourite = Anime "One-Punch Man" 8.9
现在,cabal-docspec 命令应该成功了。
Total: 2; Tried: 2; Skipped: 0; Success: 2; Errors: 0; Failures 0
Examples: 2; Tried: 2; Skipped: 0; Success: 2; Errors: 0; Failures 0
为什么你应该使用cabal-docspec
以下是你可能想为你的项目使用cabal-docspec 的原因。
-
这个库比其他的库快很多,因为它使用的是编译过的代码。
-
该库不依赖于GHC这个库,所以它对GHC的版本变化有更强的适应性。
-
如果你只改变测试,该库不需要重新编译源代码--这为开发者节省了额外的时间,并使编写文档变得愉快。
所有这些都使它成为一个可行的替代方案。
然而,请注意,该库似乎与 cabal-install因为它使用cabal-install 生成的元数据--plan.json 文件。
结论
谢谢你的阅读!
在这篇文章中,我们了解了doctests的概念,学习了如何在Haskell中定义它们,并简要介绍了两个可以帮助我们验证它们的库:doctest 和cabal-docspec 。
在本系列的下一部分,我们将更详细地介绍cabal-docspec 的配置和内部情况。要保持更新,请在Twitter上关注我们,或通过下面的表格订阅通讯。