F#UnitTesting函数有副作用

Ced*_*and 7 f# unit-testing side-effects purely-functional

我是刚刚开始学习F#的C#dev,我对单元测试有一些疑问.假设我想要以下代码:

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(",","."))
   }}
Run Code Online (Sandbox Code Playgroud)

您可以注意到,有几点需要考虑:

  • readMyType调用input()并带有副作用.
  • readMyType假设字符串读取很多东西(包含';'至少6列,有些列浮动',')

我认为这样做的方法是:

  • 注入input()func作为参数
  • 试着测试我们得到的东西(模式匹配?)
  • 使用NUnit作为解释这里

说实话,我只是在努力找到一个向我展示这个例子的例子,以便学习F#中的语法和其他最佳实践.所以,如果你能告诉我一条非常棒的道路.

提前致谢.

Fyo*_*kin 7

首先,你的功能并不是真正的功能.这是一个价值.函数和值之间的区别是语法:如果你有任何参数,你就是一个函数; 否则 - 你是一个价值观.这种区别的结果在存在副作用时非常重要:在初始化期间,值只计算一次,然后永不改变,而每次调用时都会执行函数.

对于您的具体示例,这意味着以下程序:

let main _ =
   readMyType
   readMyType
   readMyType
   0
Run Code Online (Sandbox Code Playgroud)

会询问用户只有一个输入,而不是三个.因为它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(",","."))
   }}
Run Code Online (Sandbox Code Playgroud)

然后让测试提供不同的输入并检查不同的结果:

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.
Run Code Online (Sandbox Code Playgroud)

将这些测试放在一个单独的项目中,添加对主项目的引用,然后将test runner添加到构建脚本中.


UPDATE
从您的意见,我得到了你正在寻求不仅要测试的功能,因为它是(从原来的问题同系列),同时也寻求建议改善功能本身的印象,从而使之更安全可用.

是的,检查函数中的错误条件肯定更好,并返回适当的结果.然而,与C#不同,通常最好避免异常作为控制流机制.例外情况适用于特殊情况.对于你从未预料到的这种情况.这就是为什么他们是例外.但是,由于函数的整个点是解析输入,因此无效输入是其正常条件之一.

在F#中,您通常会返回指示操作是否成功的结果,而不是抛出异常.对于您的功能,以下类型似乎是合适的:

type ErrorMessage = string
type ParseResult = Success of MyType | Error of ErrorMessage
Run Code Online (Sandbox Code Playgroud)

然后相应地修改函数:

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(',','.') } 
         }
Run Code Online (Sandbox Code Playgroud)

这个函数将返回我们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)
Run Code Online (Sandbox Code Playgroud)

请注意,即使代码现在检查字符串中的足够部分,仍然存在其他可能的错误条件:例如,parts.[4]可能不是有效数字.

我不打算进一步扩展这一点,因为这将使答案太长.我只想提到两点:

  1. 不像C#,验证所有错误情况并没有就此终结了作为一个厄运的金字塔.可以以线性外观的方式很好地组合验证(参见下面的示例).
  2. F#4.1标准库已经提供了类似于ParseResult上面的类型,命名为Result<'t, 'e>.

有关此方法的更多信息,请查看此精彩帖子(并且不要忘记浏览其中的所有链接,尤其是视频).

在这里,我将为您提供一个示例,说明您的功能可以完全验证所有内容(请记住,尽管这不是最干净的版本):

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 } } )))
Run Code Online (Sandbox Code Playgroud)

用法:

> 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"
Run Code Online (Sandbox Code Playgroud)