Как функциональные программисты тестируют функции, которые возвращают единицу?

Как функциональные программисты тестируют функции, которые возвращают единицу?

В моем случае, я считаю, что мне нужно выполнить тестирование интерфейса для этой функции:

let logToFile (filePath:string) (formatf : 'data -> string) data =
    use file = new System.IO.StreamWriter(filePath)
    file.WriteLine(formatf data)
    data

Каков рекомендуемый подход, когда я тестирую функцию с помощью ввода-вывода?

В ООП я считаю, что тест-шпион можно использовать.

Обрабатывает ли шаблон Test Spy функциональное программирование?

Мой клиент выглядит примерно так:

[<Test>]
let ''log purchase''() =
    [OneDollarBill] |> select Pepsi
                    |> logToFile "myFile.txt" (sprintf "%A")
                    |> should equal ??? // IDK

Мой домен следующий:

module Machine

type Deposit =
    | Nickel
    | Dime
    | Quarter
    | OneDollarBill
    | FiveDollarBill

type Selection =
    | Pepsi
    | Coke
    | Sprite
    | MountainDew

type Attempt = {  
    Price:decimal
    Need:decimal
}

type Transaction = {
    Purchased:Selection  
    Price:decimal
    Deposited:Deposit list
}

type RequestResult =
    | Granted of Transaction
    | Denied of Attempt

(* Functions *)
open System

let insert coin balance = coin::balance
let refund coins = coins

let priceOf = function
    | Pepsi
    | Coke
    | Sprite
    | MountainDew  -> 1.00m

let valueOf = function
    | Nickel         -> 0.05m
    | Dime           -> 0.10m
    | Quarter        -> 0.25m
    | OneDollarBill  -> 1.00m
    | FiveDollarBill -> 5.00m

let totalValue coins =
    (0.00m, coins) ||> List.fold (fun acc coin -> acc + valueOf coin)

let logToFile (filePath:string) (formatf : 'data -> string) data =
    let message = formatf data
    use file = new System.IO.StreamWriter(filePath)
    file.WriteLine(message)
    data

let select item deposited =
    if totalValue deposited >= priceOf item

    then Granted { Purchased=item
                   Deposited=deposited
                   Price = priceOf item }

    else Denied { Price=priceOf item; 
                  Need=priceOf item - totalValue deposited }

Ответ 1

Не вижу это как авторитетный ответ, потому что я не специалист по тестированию, но мой ответ на этот вопрос будет то, что, в идеальном мире, вы не можете и не должны проверить unit -returning функцию.

В идеале вы бы структурировали свой код так, чтобы он состоял из некоторого ввода-вывода для чтения данных, преобразования, кодирующие всю логику и некоторые IO для сохранения данных:

read
|> someLogic
|> someMoreLogic
|> write

Идея состоит в том, что все ваши важные вещи находятся в someLogic и someMoreLogic и что read и write абсолютно тривиальны - они читают файл как строку или последовательность строк. Это достаточно тривиально, что вам не нужно его тестировать (теперь вы можете проверить фактическую запись файла, снова прочитав файл, но это, когда вы хотите протестировать файл IO, а не любую написанную вами логику).

Здесь вы должны использовать макет в OO, но поскольку у вас есть хорошая функциональная структура, вы теперь будете писать:

testData
|> someLogic
|> someMoreLogic
|> shouldEqual expectedResult

Теперь, на самом деле, мир не всегда так хорош, и что-то вроде spy операции оказывается полезным - возможно, потому, что вы взаимодействуете с миром, который не является чисто функциональным.

У Фила Трелфорда есть хороший и очень простой рекордер, который позволяет записывать вызовы функции и проверять, что она была вызвана с ожидаемыми входами - и это то, что я нашел полезным несколько раз (и достаточно просто, чтобы вы на самом деле не нужны рамки).

Ответ 2

Очевидно, вы могли бы использовать макет, как и в императивном коде, если единица кода принимает свои зависимости в качестве параметра.

Но, для другого подхода, я нашел этот разговор действительно интересным Mocks & stubs от Ken Scambler. Насколько я помню, общий аргумент заключался в том, что вам следует избегать использования mocks, сохраняя все функции как можно более чистыми, делая их Data-in-data-out. На самых краях вашей программы у вас будут очень простые функции, которые выполняют важные побочные эффекты. Они настолько просты, что даже не требуют тестирования.

Предоставляемая вами функция достаточно проста, чтобы попасть в эту категорию. Тестирование его с помощью макета или подобного было бы просто связано с тем, что вызываются определенные методы, а не что побочный эффект. Такой тест не имеет смысла и не добавляет никакой ценности над самим кодом, при этом все еще добавляет нагрузку на обслуживание. Лучше проверить часть побочного эффекта с помощью теста интеграции или сквозного теста, который фактически смотрит на файл, который был написан.

Еще один хороший разговор на эту тему - Границы Гэри Бернхардта, в котором обсуждается концепция функционального ядра, императивной оболочки.