【问题标题】:Customise FsCheck output自定义 FsCheck 输出
【发布时间】:2017-08-19 06:05:34
【问题描述】:

我正在 VisualStudio 中使用 FsCheck 和 NUnit 进行测试。

目前的问题是:我设法生成了随机图(用于测试某些图功能),但是当测试失败时,FsCheck 会吐出整个图并且它不使用 ToString 所以它实际上会转储原始记录列表,你不能看看里面有什么。

此外,我不仅需要用于检查的输入图,还需要我在运行属性时创建的一些其他数据。

那么我怎样才能改变 FsCheck 的输出行为,以便

  • 实际上是在输入图上调用我的 ToString 方法
  • 输出更多信息

什么时候测试失败?

编辑: 这是我当前的测试设置。

module GraphProperties

open NUnit.Framework
open FsCheck
open FsCheck.NUnit

let generateRandomGraph =
    gen {
        let graph: Graph<int,int> = Graph<_,_>.Empty()
        // fill in random nodes and transitions...
        return graph
    }

type MyGenerators =
    static member Graph() =
        {new Arbitrary<Graph<int,int>>() with
            override this.Generator = generateRandomGraph
            override this.Shrinker _ = Seq.empty }

[<TestFixture>]
type NUnitTest() =
    [<Property(Arbitrary=[|typeof<MyGenerators>|], QuietOnSuccess = true)>]
    member __.cloningDoesNotChangeTheGraph (originalGraph: Graph<int,int>) =
        let newGraph = clone originalGraph
        newGraph = originalGraph

【问题讨论】:

  • 你的断言怎么样?即使在使用 NUnit 运行测试时,也有不同的断言方式。这将有助于提供一个示例测试,甚至是最后一行。
  • 我刚刚添加了一些代码示例。最困难的部分是让发电机工作。对于属性本身,我得到随机输入,用这个输入调用被测方法并返回一个布尔值(在这种情况下是比较的结果)。
  • Graph&lt;_,_&gt; 是记录还是类?你能提供它的(也许是精简的)定义吗?你看到的实际输出是什么?

标签: f# fscheck


【解决方案1】:

FsCheck 使用sprintf "%A" 将测试参数转换为测试输出中的字符串,因此您需要做的是控制%A 格式化程序如何格式化您的类型。根据How do I customize output of a custom type using printf?,方法是使用StructuredFormatDisplay attribute。该属性的值应该是格式为PreText {PropertyName} PostText 的字符串,其中PropertyName 应该是您的类型的属性(不是函数!)。例如,假设您有一个树结构,其中叶子中有一些复杂的信息,但对于您的测试,您只需要知道叶子的数量,而不是叶子中的内容。所以你会从这样的数据类型开始:

// Example 1
type ComplicatedRecord = { ... }
type Tree =
    | Leaf of ComplicatedRecord
    | Node of Tree list
    with
        member x.LeafCount =
            match x with
            | Leaf _ -> 1
            | Node leaves -> leaves |> List.sumBy (fun x -> x.LeafCount)
        override x.ToString() =
            // For test output, we don't care about leaf data, just count
            match x with
            | Leaf -> "Tree with a total of 1 leaf"
            | Node -> sprintf "Tree with a total of %d leaves" x.LeafCount

现在,到目前为止,这不是您想要的。这种类型没有声明了自定义的%A 格式,因此 FsCheck(以及任何其他使用sprintf "%A" 对其进行格式化的东西)最终会输出树的整个复杂结构及其所有不相关的-to-the-test 叶数据。要让 FsCheck 输出您想看到的内容,您需要设置一个属性,而不是一个函数(ToString 不能用于此目的)来输出您想看到的内容.例如:

// Example 2
type ComplicatedRecord = { ... }
[<StructuredFormatDisplay("{LeafCountAsString}")>]
type Tree =
    | Leaf of ComplicatedRecord
    | Node of Tree list
    with
        member x.LeafCount =
            match x with
            | Leaf _ -> 1
            | Node leaves -> leaves |> List.sumBy (fun x -> x.LeafCount)
        member x.LeafCountAsString = x.ToString()
        override x.ToString() =
            // For test output, we don't care about leaf data, just count
            match x with
            | Leaf -> "Tree with a total of 1 leaf"
            | Node -> sprintf "Tree with a total of %d leaves" x.LeafCount

注意:我没有在 F# 中对此进行测试,只是将它输入到 Stack Overflow 评论框中——所以我可能搞砸了 ToString() 部分。 (我不记得,也无法通过快速的 Google 找到,覆盖应该在 with 关键字之后还是之前)。但我知道 StructuredFormatDisplay 属性是您想要的,因为我自己使用它来从 FsCheck 中获取自定义输出。

顺便说一句,您也可以在我的示例中为复杂的记录类型设置StructuredFormatDisplay 属性。例如,如果你有一个关心树结构而不关心叶子内容的测试,你可以这样写:

// Example 3
[<StructuredFormatDisplay("LeafRecord")>] // Note no {} and no property
type ComplicatedRecord = { ... }
type Tree =
    | Leaf of ComplicatedRecord
    | Node of Tree list
    with
        member x.LeafCount =
            match x with
            | Leaf _ -> 1
            | Node leaves -> leaves |> List.sumBy (fun x -> x.LeafCount)
        override x.ToString() =
            // For test output, we don't care about leaf data, just count
            match x with
            | Leaf -> "Tree with a total of 1 leaf"
            | Node -> sprintf "Tree with a total of %d leaves" x.LeafCount

现在,您的所有 ComplicatedRecord 实例,无论其内容如何,​​都将在您的输出中显示为文本 LeafRecord,您将能够更好地专注于树形结构——而且没有必要在Tree 类型上设置StructuredFormatDisplay 属性。

这不是一个完全理想的解决方案,因为您可能需要根据正在运行的各种测试的需要不时调整StructuredFormatDisplay 属性。 (对于某些测试,您可能希望关注叶子数据的一部分,对于其他测试,您可能希望完全忽略叶子数据,等等)。而且您可能希望在投入生产之前删除该属性。但在 FsCheck 获得“Give me a function to format failed test data with”配置参数之前,这是让您的测试数据按照您需要的方式格式化的最佳方式。

【讨论】:

  • 谢谢,现在我可以根据需要打印图表了。还剩一点。您提到无法更改 FsCheck 的输出功能。真的没有办法以某种方式继承和覆盖功能(是的,这是邪恶的 OO 思维)?这是否意味着关于我问题的第二个项目符号,我没有机会输出例如在我的示例中测试失败时生成的图表及其克隆?
  • 如果您查看FsCheck source code,您会看到argumentsToString 函数是调用sprintf "%A" 的函数,而它又是从onFailureToString 调用的。您实际上可以通过实现IRunner 接口来实现自己的测试运行器,所以我认为这是唯一的方法并不完全正确。不过,这当然是最简单的方法。
  • 如果您想输出原始图形及其克隆,最简单的方法是在您的测试中:let result = (originalGraph = clonedGraph); if not result then printfn "Original graph: %A" originalGraph ; printfn "Cloned graph: %A" clonedGraph; result。像这样简单的东西。
  • 无法在 cmets 中正确格式化代码,但想法是两个 printfn 调用在 if 下缩进,result 行位于函数的顶层缩进.所以这个测试无论如何都会返回真或假,但如果测试失败,则会打印出图表。或者,可能更好的是,您可以使用像 Unquote 这样的库来自动打印失败的相等比较的左侧和右侧。
  • 非常感谢您提出这些想法。你的链接真的很有用!
【解决方案2】:

您还可以在测试失败时使用标签显示您想要的任何内容:https://fscheck.github.io/FsCheck/Properties.html#And-Or-and-Labels

【讨论】:

    猜你喜欢
    • 2020-01-06
    • 2011-08-10
    • 2010-09-14
    • 2011-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多