a

Câu hỏi F # UnitTesting chức năng với tác dụng phụ


Tôi là C # dev đã bắt đầu học F # và tôi có một vài câu hỏi về kiểm thử đơn vị. Giả sử tôi muốn đoạn mã sau:

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(",","."))
   }}

Như bạn có thể nhận thấy, có một số điểm cần cân nhắc:

  • readMyType đang gọi input () với một hiệu ứng phụ.
  • readMyType giả sử nhiều điều trên chuỗi đã đọc (chứa ';' ít nhất 6 cột, một số cột được nổi bằng ',')

Tôi nghĩ cách làm này sẽ là:

  • inject input () func như tham số
  • cố gắng kiểm tra những gì chúng tôi đang nhận được (khớp mẫu?)
  • Sử dụng NUnit như đã giải thích đây

Thành thật mà nói, tôi chỉ đang cố gắng tìm một ví dụ cho tôi thấy điều này, để tìm hiểu cú pháp và các phương pháp hay nhất khác trong F #. Vì vậy, nếu bạn có thể chỉ cho tôi con đường sẽ rất tuyệt vời.

Cảm ơn trước.


7
2017-07-30 13:19


gốc




Các câu trả lời:


Đầu tiên, chức năng của bạn không thực sự là một chức năng. Đó là một giá trị. Sự khác biệt giữa các hàm và giá trị là cú pháp: nếu bạn có bất kỳ tham số nào, bạn là một hàm; nếu không - bạn là một giá trị. Hậu quả của sự phân biệt này là rất quan trọng trong sự hiện diện của các tác dụng phụ: các giá trị được tính chỉ một lần, trong quá trình khởi tạo, và sau đó không bao giờ thay đổi, trong khi các hàm được thực thi mỗi lần bạn gọi chúng.

Đối với ví dụ cụ thể của bạn, điều này có nghĩa là chương trình sau:

let main _ =
   readMyType
   readMyType
   readMyType
   0

sẽ chỉ yêu cầu người dùng một đầu vào, không phải ba. Bởi vì readMyType là một giá trị, nó được khởi tạo một lần, khi bắt đầu chương trình, và bất kỳ tham chiếu nào tiếp theo nó chỉ nhận giá trị được tính trước, nhưng không thực thi mã lại.

Thứ hai, - vâng, bạn nói đúng: để kiểm tra chức năng này, bạn cần tiêm input chức năng như một tham số:

let readMyType (input: unit -> string) = 
   input().Split(';') 
   |> fun x -> {Name=x.[1]; Coordinate = {
   Longitude = float(x.[4].Replace(",",".")) 
   Latitude =float(x.[5].Replace(",","."))
   }}

và sau đó có các bài kiểm tra cung cấp các đầu vào khác nhau và kiểm tra các kết quả khác nhau:

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.

Đặt các thử nghiệm này vào một dự án riêng biệt, thêm tham chiếu đến dự án chính của bạn, sau đó thêm nhân tố thử nghiệm vào tập lệnh xây dựng của bạn.


CẬP NHẬT
Từ nhận xét của bạn, tôi có ấn tượng rằng bạn đang tìm kiếm không chỉ để kiểm tra chức năng như nó (mà sau câu hỏi ban đầu của bạn), mà còn yêu cầu tư vấn về cải thiện chức năng, để làm cho nó an toàn hơn và có thể sử dụng được .

Có, tốt hơn hết là kiểm tra các điều kiện lỗi trong hàm và trả về kết quả phù hợp. Tuy nhiên, không giống như C #, tốt hơn hết là tránh các ngoại lệ như là cơ chế điều khiển luồng. Ngoại lệ dành cho đặc biệt tình huống. Đối với những tình huống như vậy mà bạn sẽ không bao giờ mong đợi. Đó là lý do tại sao họ là ngoại lệ. Nhưng kể từ khi toàn bộ điểm của hàm của bạn phân tích cú pháp đầu vào, nó là lý do khiến đầu vào không hợp lệ là một trong những điều kiện bình thường cho nó.

Trong F #, thay vì ném ngoại lệ, bạn thường sẽ trả lại kết quả cho biết hoạt động đã thành công hay chưa. Đối với chức năng của bạn, loại sau có vẻ thích hợp:

type ErrorMessage = string
type ParseResult = Success of MyType | Error of ErrorMessage

Và sau đó sửa đổi các chức năng cho phù hợp:

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(',','.') } 
         }

Hàm này sẽ trả về cho chúng ta MyType bọc trong Success hoặc một thông báo lỗi được gói trong Errorvà chúng tôi có thể kiểm tra điều này trong các bài kiểm tra:

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)

Lưu ý rằng, mặc dù mã hiện kiểm tra đủ các phần trong chuỗi, vẫn có các điều kiện lỗi khác có thể xảy ra: ví dụ: parts.[4] có thể không phải là số hợp lệ.

Tôi sẽ không mở rộng thêm về điều này, vì điều đó sẽ làm cho câu trả lời quá dài. Tôi sẽ chỉ dừng lại để đề cập đến hai điểm:

  1. Không giống như C #, xác minh tất cả các điều kiện lỗi không phải phải kết thúc như một kim tự tháp của doom. Việc xác thực có thể được kết hợp độc đáo theo cách tuyến tính (xem ví dụ bên dưới).
  2. Thư viện chuẩn F # 4.1 đã cung cấp một loại tương tự như ParseResult có tên nêu trên Result<'t, 'e>.

Để biết thêm về cách tiếp cận này, hãy xem bài đăng tuyệt vời này (và đừng quên khám phá tất cả các liên kết từ nó, đặc biệt là video).

Và ở đây, tôi sẽ để lại cho bạn một ví dụ về chức năng của bạn có thể trông như thế nào với việc xác thực đầy đủ mọi thứ (hãy nhớ rằng mặc dù đây không phải là sạch nhất phiên bản vẫn còn):

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 } } )))

Sử dụng:

> 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"

7
2017-07-30 13:45



Cảm ơn câu trả lời chi tiết này và giải thích về chức năng và giá trị, điều đó thực sự hữu ích. Bằng cách đọc câu trả lời của bạn Có vẻ như bạn không thêm bất kỳ kiểm tra nào trong mã, nhưng chỉ kiểm tra nó không thành công trong bài kiểm tra đơn vị, tại sao lại như vậy? (Tôi có nghĩa là trong C # tôi sẽ có thêm kiểm tra và ném một ngoại lệ sau đó kiểm tra nó trong một bài kiểm tra đơn vị. Có vẻ mã hơn nhưng tôi đang sử dụng để tiếp cận TDD) - Cedric Royer-Bertrand
Tôi đã cập nhật câu trả lời. - Fyodor Soikin
Cảm ơn bạn đã cập nhật, bài viết về lập trình theo định hướng đường sắt rất thú vị. - Cedric Royer-Bertrand


Đây là một chút theo dõi câu trả lời xuất sắc của @FyodorSoikin đang cố gắng khám phá đề xuất

hãy nhớ rằng đây không phải là phiên bản sạch nhất

Làm cho ParseResult chung

type ParseResult<'a> = Success of 'a | Error of ErrorMessage
type ResultType = ParseResult<Defibrillator> // see the Test Cases

chúng ta có thể định nghĩa một người xây dựng

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()

vì vậy chúng tôi có được ký hiệu ngắn gọn:

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 } }
    }

Trường hợp kiểm tra

Kiểm tra luôn luôn là cơ bản

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")

Chú ý việc sử dụng một ParseResult từ cái chung.

ghi chú nhỏ

Double.TryParse chỉ đủ trong những điều sau đây

let parseFloat (s: string) = 
    match Double.TryParse s with
    | true, x -> Success x
    | false, _ -> Error ("Not a number: " + s)

1
2017-07-30 22:21