You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为使用FParsec编写的解析器编写单元测试?

问题

我想为用FParsec编写的解析器生成的抽象语法树(AST)编写单元测试。现有解析器代码如下:

open FParsec

type FooValue = FooValue of int
let foo: Parser<string,unit> = pstring "foo" 
let fooValue = pint32
let p = foo >>. spaces >>. fooValue |>> FooValue.FooValue

目前我只能通过打印输出手动验证结果:

let resultSuccess = run p "foo 23"
let resultFailure = run p "foo noNumber"

printfn "%O" resultSuccess
printfn "%O" resultFailure

输出结果:

Success: FooValue 23
Failure:
Error in Ln: 1 Col: 5
foo noNumber
    ^
Expecting: integer number (32-bit, signed)

但我希望编写结构化的单元测试,比如用MSTest,但不知道如何构造预期的解析结果来做断言,也想知道如何通过序列化字符串来简化复杂AST的对比:

open FParsec
open Microsoft.VisualStudio.TestTools.UnitTesting

[<TestClass>]
type TestMyParser () =

    [<TestMethod>]
    member this.TestPSucceeds () =
        let actual = run p "foo 23"
        let expected = ......? 
        Assert.AreEqual(actual, expected);

    [<TestMethod>]
    member this.TestPFails () =
        let actual = run p "foo noNumber"
        let expected = ......? 
        Assert.AreEqual(actual, expected);
解决方案

1. 测试解析成功场景

run函数返回的是ParserResult<'a, unit>类型,成功时对应Success案例,包含你的AST值。你可以直接提取AST值做断言,或者构造完整的Success实例对比:

方式一:直接验证AST值(推荐)

这种方式无需处理解析位置等细节,专注于核心的AST正确性:

open FParsec
open Microsoft.VisualStudio.TestTools.UnitTesting

[<TestClass>]
type TestMyParser () =

    [<TestMethod>]
    member this.TestPSucceeds () =
        match run p "foo 23" with
        | Success(ast, _, _) -> Assert.AreEqual(ast, FooValue 23)
        | Failure(err, _, _) -> Assert.Fail($"解析失败:{err}")

方式二:构造完整Success实例

如果需要精确验证解析位置和状态,可以构造完整的Success对象:

[<TestMethod>]
member this.TestPSucceedsWithFullResult () =
    let actual = run p "foo 23"
    // 构造预期结果:AST值 + 空状态 + 解析结束位置
    let expected = Success(FooValue 23, "", Position(1, 6, 5))
    Assert.AreEqual(actual, expected)

2. 测试解析失败场景

失败时run返回Failure案例,包含错误信息、错误位置等。你可以选择构造完整Failure实例,或者验证关键的错误特征:

方式一:验证错误信息和位置(灵活健壮)

如果不需要精确匹配错误信息的每一个字符,只验证关键内容和位置更稳妥:

[<TestMethod>]
member this.TestPFails () =
    match run p "foo noNumber" with
    | Failure(err, _, pos) -> 
        Assert.IsTrue(err.Contains("integer number (32-bit, signed)"))
        Assert.AreEqual(pos.Column, 5)
        Assert.AreEqual(pos.Line, 1)
    | Success(_, _, _) -> Assert.Fail("解析应该失败,但意外成功了")

方式二:构造完整Failure实例

如果需要严格匹配所有失败细节,可以构造对应的Failure对象:

[<TestMethod>]
member this.TestPFailsWithFullResult () =
    let actual = run p "foo noNumber"
    let expectedError = "Expecting: integer number (32-bit, signed)"
    let expectedPos = Position(1, 5, 4)
    let expected = Failure(expectedError, ErrorMessageList([expectedError]), expectedPos)
    Assert.AreEqual(actual, expected)

3. 基于字符串序列化的简化对比

对于复杂AST,直接构造预期实例会很繁琐,你可以通过序列化字符串来简化对比:

自定义AST序列化函数

针对你的AST类型编写专用的字符串转换逻辑:

module FooValue =
    let toString (FooValue n) = $"FooValue {n}"

然后在测试中使用:

[<TestMethod>]
member this.TestPSucceedsWithCustomSerialization () =
    match run p "foo 23" with
    | Success(ast, _, _) -> 
        let actualStr = FooValue.toString ast
        Assert.AreEqual(actualStr, "FooValue 23")
    | Failure(err, _, _) -> Assert.Fail($"解析失败:{err}")

使用内置格式化输出

F#的判别联合(DU)默认有合理的ToString()实现,你可以直接用sprintf "%O"序列化:

[<TestMethod>]
member this.TestPSucceedsWithBuiltInFormat () =
    match run p "foo 23" with
    | Success(ast, _, _) -> 
        Assert.AreEqual(sprintf "%O" ast, "FooValue 23")
    | Failure(err, _, _) -> Assert.Fail($"解析失败:{err}")

对于失败场景,也可以序列化整个ParserResult的输出字符串:

[<TestMethod>]
member this.TestPFailsWithStringComparison () =
    let actual = run p "foo noNumber"
    let actualStr = sprintf "%O" actual
    let expectedStr = """Failure:
Error in Ln: 1 Col: 5
foo noNumber
    ^
Expecting: integer number (32-bit, signed)"""
    Assert.AreEqual(actualStr, expectedStr)

注意:这种方式依赖FParsec的输出格式,版本更新可能导致格式变化,适合快速编写简单测试。

内容的提问来源于stack exchange,提问作者bookofproofs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 02:07:03