具有副作用的 F# UnitTesting 函数
F# UnitTesting function with side effect
我是刚开始学习F#的C#开发者,有几个关于单元测试的问题。假设我想要以下代码:
let input () = Console.In.ReadLine()
type MyType= {Name:string; Coordinate:Coordinate}
let readMyType =
input().Split(';')
|> fun x -> {Name=x.[1]; Coordinate = {
Longitude = float(x.[4].Replace(",","."))
Latitude =float(x.[5].Replace(",","."))
}}
如您所见,有几点需要考虑:
- readMyType 调用 input() 有副作用。
- readMyType 假设读取的字符串有很多内容(包含“;”至少 6 列,有些列是浮动的,带有“,”)
我认为这样做的方法是:
- 将 input() 函数作为参数注入
- 尝试测试我们得到的结果(模式匹配?)
- 按照说明使用 NUnit here
老实说,我只是在努力寻找一个向我展示这一点的示例,以便学习 F# 中的语法和其他最佳实践。所以,如果你能告诉我那将是非常好的路径。
提前致谢。
首先,你的函数并不是真正的函数。这是一个价值。函数和值之间的区别是语法上的:如果你有任何参数,你就是一个函数;否则 - 你是一个价值。这种区别的结果在存在副作用时非常重要:值只在初始化期间计算一次,然后永远不会改变,而每次调用函数时都会执行它们。
对于您的具体示例,这意味着以下程序:
let main _ =
readMyType
readMyType
readMyType
0
只会要求用户输入一个,而不是三个。因为 readMyType
是一个值,它会在程序启动时被初始化一次,并且对它的任何后续引用都只会获得预先计算的值,而不会再次执行代码。
其次, - 是的,你是对的:为了测试这个函数,你需要注入 input
函数作为参数:
let readMyType (input: unit -> string) =
input().Split(';')
|> fun x -> {Name=x.[1]; Coordinate = {
Longitude = float(x.[4].Replace(",","."))
Latitude =float(x.[5].Replace(",","."))
}}
然后让测试提供不同的输入并检查不同的结果:
let [<Test>] ``Successfully parses correctly formatted string``() =
let input() = "foo;the_name;bar;baz;1,23;4,56"
let result = readMyType input
result |> should equal { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } }
let [<Test>] ``Fails when the string does not have enough parts``() =
let input() = "foo"
(fun () -> readMyType input) |> shouldFail
// etc.
将这些测试放在一个单独的项目中,添加对您的主项目的引用,然后将测试运行器添加到您的构建脚本中。
更新
从您的评论中,我得到的印象是您不仅在寻求按原样测试功能(从您最初的问题中得出),而且还在寻求有关改进功能本身的建议,以使其更安全和可用.
是的,最好在函数内检查错误情况,return 适当的结果。然而,与 C# 不同的是,通常最好避免异常作为控制流机制。 特殊 情况除外。对于您从未预料到的这种情况。这就是为什么他们是例外。但是由于你的函数的全部意义在于解析输入,因此无效输入是它的正常情况之一是理所当然的。
在 F# 中,您通常不会抛出异常,而是 return 一个指示操作是否成功的结果。对于您的函数,以下类型似乎是合适的:
type ErrorMessage = string
type ParseResult = Success of MyType | Error of ErrorMessage
然后相应地修改函数:
let parseMyType (input: string) =
let parts = input.Split [|';'|]
if parts.Length < 6
then
Error "Not enough parts"
else
Success
{ Name = parts.[0]
Coordinate = { Longitude = float(parts.[4].Replace(',','.')
Latitude = float(parts.[5].Replace(',','.') }
}
此函数将 return 我们 MyType
包装在 Success
中或错误消息包装在 Error
中,我们可以在测试中检查:
let [<Test>] ``Successfully parses correctly formatted string``() =
let input() = "foo;the_name;bar;baz;1,23;4,56"
let result = readMyType input
result |> should equal (Success { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } })
let [<Test>] ``Fails when the string does not have enough parts``() =
let input() = "foo"
let result = readMyType input
result |> should equal (Error "Not enough parts)
请注意,即使代码现在检查字符串中的足够部分,仍然存在其他可能的错误情况:例如,parts.[4]
可能不是有效数字。
我不打算进一步展开,因为那样会使答案太长。我只会停下来提两点:
- 与 C# 不同,验证所有错误条件 而不是 必须以 pyramid of doom 结束。验证可以以线性方式很好地组合(参见下面的示例)。
- F# 4.1 标准库已经提供了类似于上面
ParseResult
的类型,命名为Result<'t, 'e>
.
有关此方法的更多信息,请查看 this wonderful post(不要忘记浏览其中的所有链接,尤其是视频)。
在这里,我将给您留下一个示例,说明您的函数在对所有内容进行全面验证后会是什么样子(请记住,尽管这仍然不是 最干净的 版本):
let parseFloat (s: string) =
match System.Double.TryParse (s.Replace(',','.')) with
| true, x -> Ok x
| false, _ -> Error ("Not a number: " + s)
let split n (s:string) =
let parts = s.Split [|';'|]
if parts.Length < n then Error "Not enough parts"
else Ok parts
let parseMyType input =
input |> split 6 |> Result.bind (fun parts ->
parseFloat parts.[4] |> Result.bind (fun lgt ->
parseFloat parts.[5] |> Result.bind (fun lat ->
Ok { Name = parts.[1]; Coordinate = { Longitude = lgt; Latitude = lat } } )))
用法:
> parseMyType "foo;name;bar;baz;1,23;4,56"
val it : Result<MyType,string> = Ok {Name = "name";
Coordinate = {Longitude = 1.23;
Latitude = 4.56;};}
> parseMyType "foo"
val it : Result<MyType,string> = Error "Not enough parts"
> parseMyType "foo;name;bar;baz;badnumber;4,56"
val it : Result<MyType,string> = Error "Not a number: badnumber"
这是对@FyodorSoikin 的 试图探索建议
的一些跟进
keep in mind though that this is not the cleanest version still
使 ParseResult
通用
type ParseResult<'a> = Success of 'a | Error of ErrorMessage
type ResultType = ParseResult<Defibrillator> // see the Test Cases
我们可以定义一个构建器
type Builder() =
member x.Bind(r :ParseResult<'a>, func : ('a -> ParseResult<'b>)) =
match r with
| Success m -> func m
| Error w -> Error w
member x.Return(value) = Success value
let builder = Builder()
所以我们得到一个简洁的符号:
let parse input =
builder {
let! parts = input |> split 6
let! lgt = parts.[4] |> parseFloat
let! lat = parts.[5] |> parseFloat
return { Name = parts.[1]; Coordinate = { Longitude = lgt; Latitude = lat } }
}
测试用例
测试永远是基础
let [<Test>] ``3. Successfully parses correctly formatted string``() =
let input = "foo;the_name;bar;baz;1,23;4,56"
let result = parse input
result |> should equal (ResultType.Success { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } })
let [<Test>] ``3. Fails when the string does not have enough parts``() =
let input = "foo"
let result = parse input
result |> should equal (ResultType.Error "Not enough parts")
let [<Test>] ``3. Fails when the string does not contain a number``() =
let input = "foo;name;bar;baz;badnumber;4,56"
let result = parse input
result |> should equal (ResultType.Error "Not a number: badnumber")
注意通用ParseResult
与通用
的用法。
小注
Double.TryParse
以下
就够了
let parseFloat (s: string) =
match Double.TryParse s with
| true, x -> Success x
| false, _ -> Error ("Not a number: " + s)
我是刚开始学习F#的C#开发者,有几个关于单元测试的问题。假设我想要以下代码:
let input () = Console.In.ReadLine()
type MyType= {Name:string; Coordinate:Coordinate}
let readMyType =
input().Split(';')
|> fun x -> {Name=x.[1]; Coordinate = {
Longitude = float(x.[4].Replace(",","."))
Latitude =float(x.[5].Replace(",","."))
}}
如您所见,有几点需要考虑:
- readMyType 调用 input() 有副作用。
- readMyType 假设读取的字符串有很多内容(包含“;”至少 6 列,有些列是浮动的,带有“,”)
我认为这样做的方法是:
- 将 input() 函数作为参数注入
- 尝试测试我们得到的结果(模式匹配?)
- 按照说明使用 NUnit here
老实说,我只是在努力寻找一个向我展示这一点的示例,以便学习 F# 中的语法和其他最佳实践。所以,如果你能告诉我那将是非常好的路径。
提前致谢。
首先,你的函数并不是真正的函数。这是一个价值。函数和值之间的区别是语法上的:如果你有任何参数,你就是一个函数;否则 - 你是一个价值。这种区别的结果在存在副作用时非常重要:值只在初始化期间计算一次,然后永远不会改变,而每次调用函数时都会执行它们。
对于您的具体示例,这意味着以下程序:
let main _ =
readMyType
readMyType
readMyType
0
只会要求用户输入一个,而不是三个。因为 readMyType
是一个值,它会在程序启动时被初始化一次,并且对它的任何后续引用都只会获得预先计算的值,而不会再次执行代码。
其次, - 是的,你是对的:为了测试这个函数,你需要注入 input
函数作为参数:
let readMyType (input: unit -> string) =
input().Split(';')
|> fun x -> {Name=x.[1]; Coordinate = {
Longitude = float(x.[4].Replace(",","."))
Latitude =float(x.[5].Replace(",","."))
}}
然后让测试提供不同的输入并检查不同的结果:
let [<Test>] ``Successfully parses correctly formatted string``() =
let input() = "foo;the_name;bar;baz;1,23;4,56"
let result = readMyType input
result |> should equal { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } }
let [<Test>] ``Fails when the string does not have enough parts``() =
let input() = "foo"
(fun () -> readMyType input) |> shouldFail
// etc.
将这些测试放在一个单独的项目中,添加对您的主项目的引用,然后将测试运行器添加到您的构建脚本中。
更新
从您的评论中,我得到的印象是您不仅在寻求按原样测试功能(从您最初的问题中得出),而且还在寻求有关改进功能本身的建议,以使其更安全和可用.
是的,最好在函数内检查错误情况,return 适当的结果。然而,与 C# 不同的是,通常最好避免异常作为控制流机制。 特殊 情况除外。对于您从未预料到的这种情况。这就是为什么他们是例外。但是由于你的函数的全部意义在于解析输入,因此无效输入是它的正常情况之一是理所当然的。
在 F# 中,您通常不会抛出异常,而是 return 一个指示操作是否成功的结果。对于您的函数,以下类型似乎是合适的:
type ErrorMessage = string
type ParseResult = Success of MyType | Error of ErrorMessage
然后相应地修改函数:
let parseMyType (input: string) =
let parts = input.Split [|';'|]
if parts.Length < 6
then
Error "Not enough parts"
else
Success
{ Name = parts.[0]
Coordinate = { Longitude = float(parts.[4].Replace(',','.')
Latitude = float(parts.[5].Replace(',','.') }
}
此函数将 return 我们 MyType
包装在 Success
中或错误消息包装在 Error
中,我们可以在测试中检查:
let [<Test>] ``Successfully parses correctly formatted string``() =
let input() = "foo;the_name;bar;baz;1,23;4,56"
let result = readMyType input
result |> should equal (Success { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } })
let [<Test>] ``Fails when the string does not have enough parts``() =
let input() = "foo"
let result = readMyType input
result |> should equal (Error "Not enough parts)
请注意,即使代码现在检查字符串中的足够部分,仍然存在其他可能的错误情况:例如,parts.[4]
可能不是有效数字。
我不打算进一步展开,因为那样会使答案太长。我只会停下来提两点:
- 与 C# 不同,验证所有错误条件 而不是 必须以 pyramid of doom 结束。验证可以以线性方式很好地组合(参见下面的示例)。
- F# 4.1 标准库已经提供了类似于上面
ParseResult
的类型,命名为Result<'t, 'e>
.
有关此方法的更多信息,请查看 this wonderful post(不要忘记浏览其中的所有链接,尤其是视频)。
在这里,我将给您留下一个示例,说明您的函数在对所有内容进行全面验证后会是什么样子(请记住,尽管这仍然不是 最干净的 版本):
let parseFloat (s: string) =
match System.Double.TryParse (s.Replace(',','.')) with
| true, x -> Ok x
| false, _ -> Error ("Not a number: " + s)
let split n (s:string) =
let parts = s.Split [|';'|]
if parts.Length < n then Error "Not enough parts"
else Ok parts
let parseMyType input =
input |> split 6 |> Result.bind (fun parts ->
parseFloat parts.[4] |> Result.bind (fun lgt ->
parseFloat parts.[5] |> Result.bind (fun lat ->
Ok { Name = parts.[1]; Coordinate = { Longitude = lgt; Latitude = lat } } )))
用法:
> parseMyType "foo;name;bar;baz;1,23;4,56"
val it : Result<MyType,string> = Ok {Name = "name";
Coordinate = {Longitude = 1.23;
Latitude = 4.56;};}
> parseMyType "foo"
val it : Result<MyType,string> = Error "Not enough parts"
> parseMyType "foo;name;bar;baz;badnumber;4,56"
val it : Result<MyType,string> = Error "Not a number: badnumber"
这是对@FyodorSoikin 的
keep in mind though that this is not the cleanest version still
使 ParseResult
通用
type ParseResult<'a> = Success of 'a | Error of ErrorMessage
type ResultType = ParseResult<Defibrillator> // see the Test Cases
我们可以定义一个构建器
type Builder() =
member x.Bind(r :ParseResult<'a>, func : ('a -> ParseResult<'b>)) =
match r with
| Success m -> func m
| Error w -> Error w
member x.Return(value) = Success value
let builder = Builder()
所以我们得到一个简洁的符号:
let parse input =
builder {
let! parts = input |> split 6
let! lgt = parts.[4] |> parseFloat
let! lat = parts.[5] |> parseFloat
return { Name = parts.[1]; Coordinate = { Longitude = lgt; Latitude = lat } }
}
测试用例
测试永远是基础
let [<Test>] ``3. Successfully parses correctly formatted string``() =
let input = "foo;the_name;bar;baz;1,23;4,56"
let result = parse input
result |> should equal (ResultType.Success { Name = "the_name"; Coordinate = { Longitude = 1.23; Latitude = 4.56 } })
let [<Test>] ``3. Fails when the string does not have enough parts``() =
let input = "foo"
let result = parse input
result |> should equal (ResultType.Error "Not enough parts")
let [<Test>] ``3. Fails when the string does not contain a number``() =
let input = "foo;name;bar;baz;badnumber;4,56"
let result = parse input
result |> should equal (ResultType.Error "Not a number: badnumber")
注意通用ParseResult
与通用
小注
Double.TryParse
以下
let parseFloat (s: string) =
match Double.TryParse s with
| true, x -> Success x
| false, _ -> Error ("Not a number: " + s)