如何在测试F#时模拟出丰富的依赖项

Car*_*ngo 6 f# unit-testing functional-programming

如何使我的F#应用程序可测试?该应用程序主要使用F#函数和记录编写.

我知道如何使用外部依赖关系来测试f#中的函数, 并且我知道各种博客帖子显示当你的界面只有一个方法时这是多么容易.

函数按模块分组,类似于我在C#类中对方法进行分组的方式.

我的问题是如何在运行测试时替换某些"抽象".我需要这样做,因为这些抽象读/写DB,通过网络与服务交谈等.这种抽象的一个例子是下面的存储和提取人和公司的存储库(及其评级).

如何在测试中替换此代码?函数调用是硬编码的,类似于C#中的静态方法调用.

我有一些可能性,但不确定我的想法是否太过我的C#背景.

  1. 我可以将我的模块实现为接口和类.虽然这仍然是F#,但我觉得这是一种错误的方法,因为我失去了很多好处.这也在http://fsharpforfunandprofit.com/posts/overview-of-types-in-fsharp/中论证.

  2. 调用例如的代码.我们PersonRepo可以作为参数函数指针指向函数的所有函数PersonRepo.然而,这很快就会积累到20个或更多指针.任何人都难以概述.它也使得代码库变得脆弱,对于每个新函数来说,PersonRepo我需要将函数指针"一直向上"添加到根组件中.

  3. 我可以创建一个包含我的所有函数的记录PersonRepo(以及我需要模拟的每个抽象的一个).但我不确定如果我再要创建例如用于使用记录一个明确的类型lookupPerson(Id;Status;Timestamp).

  4. 还有其他方法吗?我更喜欢保持应用程序的功能.

一个带有副作用的示例模块我需要在测试期间模拟出来:

namespace PeanutCorp.Repositories
module PersonRepo =
    let findPerson ssn =
        use db = DbSchema.GetDataContext(ConnectionString)
        query {
            for ratingId in db.Rating do
            where (Identifier.Identifier = ssn)
            select (Some { Id = Identifier.Id; Status = Local; Timestamp = Identifier.LastChecked; })
            headOrDefault
        }

    let savePerson id ssn timestamp status rating =
        use db = DbSchema.GetDataContext(ConnectionString)
        let entry = new DbSchema.Rating(Id = id,
                                       Id = ClientId.Value,
                                       Identifier = id,
                                       LastChecked = timestamp,
                                       Status = status,
                                       Rating = rating
        )
        db.Person.InsertOnSubmit(entry)
        ...

    let findCompany companyId = ...

    let saveCompany id companyId timestamp status rating = ...

    let findCachedPerson lookup identifier = ...
Run Code Online (Sandbox Code Playgroud)

Mar*_*ann 9

然而,这很快就会积累到20个或更多指针.

如果这是真的,那就是那些客户已经拥有的依赖数.反转控件(是:IoC)只会使显式而非隐式.

任何人都难以概述.

鉴于上述情况,还没有发生过这种情况吗?

还有其他方法吗?我更喜欢保持应用程序的功能.

你不能"保持"应用程序的功能,因为它不是.该PersonRepo模块包含不是 引用透明的功能.依赖于这种功能的任何其他功能也自动不是引用透明的.

如果大多数应用程序过渡依赖于这些PersonRepo功能,则意味着很少(如果有的话)它是引用透明的.这意味着它不是功能性的.由于这个原因,单元测试也很困难.(反过来也是如此:功能设计本质上是可测试的)

最终,功能设计还需要处理无法透明透明的功能.惯用的方法是将这些函数推送到系统的边缘,这样函数的核心就是纯粹的.这实际上与Hexagonal Architecture非常相似,但在例如Haskell中,它通过IO Monad正式化.最好的Haskell代码是纯粹的,但在边缘,函数在IO的上下文中工作.

为了使代码库可测试,您需要反转控制,就像IoC用于OOP测试一样.

F#为您提供了一个很好的工具,因为它的编译器强制您在定义它之前不能使用任何东西.因此,您需要做的"唯一的事情"是将所有不纯的功能放在最后.这确保了所有核心功能都不能使用不纯函数,因为它们在那时没有定义.

棘手的部分是弄清楚如何使用尚未定义的函数,但我在F#中的首选方法是将函数作为参数传递.

PersonRepo.savePerson该函数应该采用具有客户端函数所需签名的函数参数,而不是使用其他函数:

let myClientFunction savePerson foo bar baz =
    // Do something interesting first...
    savePerson (Guid.NewGuid ()) foo DateTimeOffset.Now bar baz
    // Then perhaps something else here...
Run Code Online (Sandbox Code Playgroud)

然后,当您撰写你的应用程序,您可以撰写myClientFunctionPersonRepo.savePerson:

let myClientFunction = myClientFunction PersonRepo.savePerson
Run Code Online (Sandbox Code Playgroud)

当你想进行单元测试时myClientFunction,你可以提供一个Test Double实现savePerson.您甚至不必使用动态模拟,因为唯一的要求是savePerson具有正确的类型.

  • @ CarloV.Dango应用了[ISP](https://en.wikipedia.org/wiki/Interface_segregation_principle)。将功能捆绑在一起时,便无法随后将它们分开。将事物组合在一起的次数越多,违反ISP的可能性就越大。这是我发现的面向对象原则,也适用于功能设计,但是我以相同的方式设计面向对象的代码:每个接口一个方法。因此,在C#中,如果您遵循[SOLID](http://bit.ly/YOB2I0),我也会说您有20多个依赖项。 (2认同)
  • @MarkSeemann我完全不同意ISP暗示每个接口有一种方法的建议.您链接的文章说:"(ISP)声明不应该强迫任何客户端依赖它不使用的方法".我确信有很多情况下,一个方法接口是合适的,但有时候它不适合.最好将适当隔离的界面视为定义一组具有高度凝聚力的离散行为.不必要的隔离接口使得定义作用于逻辑离散单元的函数变得复杂,冗长和不直观. (2认同)