F#或C#|如何测试扩展方法是否被调用?

时间:2019-02-18 23:45:26

标签: f# moq

当我具有类型 MyType和以下代码时:

// models.fs

type internal Ticker(price:decimal, time:System.DateTime) =
    member this.Price = price
    member this.Time = time

// cache.fs

module internal cache

type TickerCache = {date:DateTime; ticker:Ticker}

let cache_duration = TimeSpan.FromSeconds 60.0
let cache_tickers = Dictionary<string, TickerCache>()

let isCached currency_pair = 
    cache_tickers.ContainsKey currency_pair && (DateTime.UtcNow - cache_tickers.Item(currency_pair).date) < cache_duration


let getTicker currency_pair = 
    let found, item = cache_tickers.TryGetValue currency_pair        
    if found && (DateTime.UtcNow - item.date) < cache_duration then Some item.ticker
    else None

// Client.fs

namespace main

open Flurl.Http

type MyType =       

    let getTicker main, base_ = 

        let pair = sprintf"%s%s" main base_
        let cached_ticker = cache.getTicker(pair)
        if cached_ticker.IsSome then Task.FromResult(cached_ticker.Value)
        else         
            let url = sprintf"%s/ticker/%s" apiBaseUrl pair
            url.GetJsonAsync<Ticker>()

如何测试 GetJsonAsync 是否被调用(取决于 cache.getTicker 的结果)?
我通常使用Moq来检查是否调用了特定的 Setup ,但在这种情况下, Setup 是String的扩展方法,而不是我可以模拟的接口。

有关如何对此进行测试的建议?

(均匀的C#解决方案还可以,我会尝试转换为F#或使用C#编写测试)

[编辑] 为什么MyType是类型?
此代码旨在生成将由C#项目以这种方式使用的客户端:
(MyType是客户端

var configuration = new Configuration(tickerCacheTTL, publicKey, secretKey);
IClient client = new Client(configuration);
var ticker = client.getTicker("A", "B");

type 会生成一个 class ,它更加“ .Net友好”。 此外,我不想每次都将配置传递给getTicker函数。
在真实的F#代码中,我使用:interface IClient with公开公共方法。

[编辑2]
我可以在models.fs(Ticker,TickerCache)和cache.fs中使用 InternalsVisibleToAttribute

[<assembly:System.Runtime.CompilerServices.InternalsVisibleTo("MyProject.UnitTests")>]
do()

(并将TickerCache类型也从专用更改为内部) 但如果没有在客户端(MyType)中进行适当的注入,我仍然无法模拟cache.getTicker。

1 个答案:

答案 0 :(得分:3)

您可以将一个函数注入到类型中,从而允许您通过关闭可以检查的可变值来进行测试。我猜那将需要对您所做的进行最小的更改以使其可测试。

我在fsx脚本文件中整理了一个示例,希望可以证明这一点。

#r "packages/System.Net.Http/lib/net46/System.Net.Http.dll"
#r "packages/Flurl/lib/net40/Flurl.dll"
#r "packages/Flurl.Http/lib/net46/Flurl.Http.dll"

open System
open System.Net.Http
open Flurl
open Flurl.Http
open System.Threading.Tasks

type Ticker = {
    At: DateTime
    Value: float
}

module Ticker =

    type Data(f:string->Task<Ticker>) =

        let getTicker (m, b) =
            let url = sprintf "%s/ticker" b
            f(url)

        member this.GetTicker = getTicker


// in test
let mutable isCalled = false
let spy s = 
    isCalled <- true
    Task.FromResult({ At = DateTime.UtcNow; Value = 1.0 })

let t = Ticker.Data(spy)

let testR = t.GetTicker("whatever", "http://localhost:5000/")

// prod code
let fetchTicker (url:string) = url.GetJsonAsync<Ticker>()

let p = Ticker.Data(fetchTicker)

let result = t.GetTicker("whatever", "https://some.url/")

使用类型扩展名(https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/type-extensions)可能还有其他选择

可能值得退后一步,考虑是否可以采用其他方法。当F#提供一种更简单,功能更强大的方法时,您可能会像在C#中一样进行处理。具体来说,我无法确切说明您为什么选择使用myType

请参阅此处了解一些想法:https://en.wikibooks.org/wiki/F_Sharp_Programming/Caching