我知道How to test functions in f# with external dependencies并且我知道各种博客帖子显示当你的界面只有一个方法时这是多么容易.
我的问题是如何在运行测试时替换某些“抽象”.我需要这样做,因为这些抽象读/写DB,通过网络与服务交谈等.这种抽象的一个例子是下面的存储和提取人和公司的存储库(及其评级).
如何在测试中替换此代码?函数调用是硬编码的,类似于C#中的静态方法调用.
我有一些可能性,但不确定我的想法是否太过我的C#背景.
>我可以将我的模块实现为接口和类.虽然这仍然是F#,但我觉得这是一种错误的方法,因为我失去了很多好处.这也是在http://fsharpforfunandprofit.com/posts/overview-of-types-in-fsharp/争论的
>调用例如的代码.我们的PersonRepo可以作为PersonRepo所有函数的参数函数指针.然而,这很快就会积累到20个或更多指针.任何人都难以概述.它也使代码库变得脆弱,就像我们的PersonRepo中的每个新函数一样,我需要将“函数指针”一直添加到根组件中.
>我可以创建一个记录,包含我的PersonRepo的所有功能(以及我需要模拟的每个抽象的一个).但我不确定我是否应该创建一个明确的类型,例如对于lookupPerson中使用的记录(Id; Status; Timestamp).
>还有其他方法吗?我更喜欢保持应用程序的功能.
一个带有副作用的示例模块我需要在测试期间模拟出来:
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 = ...
This however,quickly accumulate to 20 or more pointers.
如果这是真的,那就是那些客户已经拥有的依赖数.反转控件(是:IoC)只会使显式而非隐式.
Hard for anyone to overview.
鉴于上述情况,还没有发生过这种情况吗?
Is there any other way? I prefer to keep the application functional.
你不能“保持”应用程序的功能,因为它不是. PersonRepo模块包含的函数不是referentially transparent.依赖于此类函数的任何其他函数也自动不是引用透明的.
如果大多数应用程序过渡依赖于此类PersonRepo函数,则意味着它几乎没有(如果有的话)是引用透明的.这意味着它不是功能性的.由于这个原因,单元测试也很困难. (反过来也是如此:Functional design is intrinsically testable,)
最终,功能设计还需要处理无法透明透明的功能.惯用的方法是将这些函数推送到系统的边缘,这样函数的核心就是纯粹的.这实际上与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...
然后,当您撰写应用程序时,可以使用.撰写myClientFunction
PersonRepo.savePerson:
let myClientFunction = myClientFunction PersonRepo.savePerson
如果要对myClientFunction进行单元测试,可以提供savePerson的Test Double实现.您甚至不必使用动态模拟,因为唯一的要求是savePerson具有正确的类型.