Модульное тестирование агента

Я пытаюсь проверить MailboxProcessor в F#. Я хочу проверить, что функция f, которую я даю, фактически выполняется при публикации сообщения.

Оригинальный код использует Xunit, но я сделал для него fsx, который я могу выполнить с помощью fsharpi.

Пока я делаю это:

open System 
open FSharp
open System.Threading
open System.Threading.Tasks



module MyModule =

    type Agent<'a> = MailboxProcessor<'a>
    let waitingFor timeOut (v:'a)= 
        let cts = new CancellationTokenSource(timeOut|> int)
        let tcs = new TaskCompletionSource<'a>()
        cts.Token.Register(fun (_) ->  tcs.SetCanceled()) |> ignore
        tcs ,Async.AwaitTask tcs.Task

    type MyProcessor<'a>(f:'a->unit) =
        let agent = Agent<'a>.Start(fun inbox -> 
             let rec loop() = async {

                let! msg = inbox.Receive()
                // some more complex should be used here
                f msg
                return! loop() 
             }
             loop()
        )

        member this.Post(msg:'a) = 
            agent.Post msg


open MyModule

let myTest =
    async {

        let (tcs,waitingFor) = waitingFor 5000 0

        let doThatWhenMessagepostedWithinAgent msg =
            tcs.SetResult(msg)

        let p = new MyProcessor<int>(doThatWhenMessagepostedWithinAgent)

        p.Post 3

        let! result = waitingFor

        return result

    }

myTest 
|> Async.RunSynchronously
|> System.Console.WriteLine 

//display 3 as expected

Этот код работает, но он не выглядит хорошо для меня.

1) нормально ли используется TaskCompletionSource в F# или есть что-то, что позволяет мне ждать завершения?

2) Я использую второй аргумент в функции waitFor, чтобы ограничить его, я знаю, что мог бы использовать тип MyType<'a>(), чтобы сделать это, есть ли другой вариант? Я бы предпочел не использовать новый MyType, который я считаю громоздким.

3) Есть ли другой вариант для проверки моего агента, кроме как сделать это? единственный пост на эту тему, который я нашел на данный момент, - это пост блога за 2009 год http://www.markhneedham.com/blog/2009/05/30/f-testing-asynchronous-calls-to-mailboxprocessor/

1 ответ

Решение

Это сложный вопрос, я уже некоторое время пытаюсь заняться этим. Это то, что я нашел до сих пор, это слишком долго для комментария, но я бы не решался назвать его полным ответом либо...

От самого простого к самому сложному, на самом деле зависит, насколько тщательно вы хотите протестировать и насколько сложна логика агента.

Ваше решение может быть в порядке

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

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

Одно замечание, которое применимо и к другим частям ответа: я обычно запускаю агентов с токеном отмены, это облегчает жизненный цикл как производства, так и тестирования.

Использовать каналы ответа агента

добавлять AsyncReplyChannel<'reply> на тип сообщения и публиковать сообщения с помощью PostAndAsyncReplyвместо Post метод на агента. Это изменит вашего агента на что-то вроде этого:

type MyMessage<'a, 'b> = 'a * AsyncReplyChannel<'b>

type MyProcessor<'a, 'b>(f:'a->'b) =
    // Using the MyMessage type here to simplify the signature
    let agent = Agent<MyMessage<'a, 'b>>.Start(fun inbox -> 
         let rec loop() = async {
            let! msg, replyChannel = inbox.Receive()
            let! result = f msg
            // Sending the result back to the original poster
            replyChannel.Reply result
            return! loop()
         }
         loop()
    )

    // Notice the type change, may be handled differently, depends on you
    member this.Post(msg:'a): Async<'b> = 
        agent.PostAndAsyncReply(fun channel -> msg, channel)

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

Поскольку у вас есть отдельный вызов предоставленной функции и replyChannel.Reply ответ также может отражать состояние агента, а не только результат функции.

Тестирование на основе модели черного ящика

Это то, о чем я буду говорить подробнее, так как я думаю, что это наиболее общее.

В случае, если агент инкапсулирует более сложное поведение, я нашел удобным пропустить тестирование отдельных сообщений и использовать основанные на модели тесты для проверки целых последовательностей операций на модели ожидаемого внешнего поведения. Я использую FsCheck.Experimental API для этого:

В вашем случае это было бы выполнимо, но не имело бы большого смысла, так как не существует внутреннего состояния для моделирования. Чтобы дать вам пример того, как это выглядит в моем конкретном случае, рассмотрим агент, который поддерживает клиентские соединения WebSocket для отправки сообщений клиентам. Я не могу поделиться всем кодом, но интерфейс выглядит так

/// For simplicity, this adapts to the socket.Send method and makes it easy to mock
type MessageConsumer = ArraySegment<byte> -> Async<bool>

type Message =
    /// Send payload to client and expect a result of the operation
    | Send of ClientInfo * ArraySegment<byte> * AsyncReplyChannel<Result>
    /// Client connects, remember it for future Send operations
    | Subscribe of ClientInfo * MessageConsumer
    /// Client disconnects
    | Unsubscribe of ClientInfo

Внутренне агент поддерживает Map<ClientInfo, MessageConsumer>,

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

type ConsumerType =
    | SucceedingConsumer
    | FailingConsumer
    | ExceptionThrowingConsumer

type SubscriptionState =
    | Subscribed of ConsumerType
    | Unsubscribed

type AgentModel = Map<ClientInfo, SubscriptionState>

А затем используйте FsCheck.Experimental, чтобы определить операции добавления и удаления клиентов с по-разному успешными потребителями и попытки отправки данных им. Затем FsCheck генерирует случайные последовательности операций и проверяет реализацию агента по модели между каждым шагом.

Это требует некоторого дополнительного кода "только для тестирования" и вначале требует значительных интеллектуальных затрат, но позволяет тестировать относительно сложную логику с состоянием. Что мне особенно нравится в этом, так это то, что он помогает мне тестировать весь контракт, а не только отдельные функции / методы / сообщения, точно так же, как основанное на свойствах / генеративное тестирование помогает тестировать не только с одним значением.

Использовать актеров

Я еще не зашел так далеко, но то, что я слышал в качестве альтернативы, это использование, например, Akka.NET для полноценной поддержки модели актера, и использование его средств тестирования, которые позволяют запускать агенты в специальных тестовых контекстах, проверять ожидаемые сообщения и так далее. Как я уже сказал, у меня нет опыта из первых рук, но он кажется жизнеспособным вариантом для более сложной логики с сохранением состояния (даже на одной машине, а не в распределенной многоузловой системе акторов).

Другие вопросы по тегам