如何为使用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
相关产品推荐
相关产品推荐

