如何使我的F#应用程序可测试?该应用程序主要使用F#函数和记录编写。
我知道How to test functions in f# with external dependencies并且我知道各种博客文章显示当您的界面只有一种方法时,这样做是多么容易。
函数按模块分组,类似于我在C#类中对方法进行分组的方式。
我的问题是如何在运行测试时替换某些“抽象”。我需要这样做,因为这些抽象读/写DB,通过网络与服务交谈等。这种抽象的一个例子是下面的存储和提取人和公司的存储库(及其评级)。
如何在测试中替换此代码?函数调用是硬编码的,类似于C#中的静态方法调用。
我有一些可能的想法,但不确定我的想法是否太过我的C#背景。
我可以将我的模块实现为接口和类。虽然这仍然是F#,但我觉得这是一种错误的方法,因为我失去了很多好处。这在[{3}}
调用例如的代码。我们的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 = ...
答案 0 :(得分:8)
然而,这很快会累积到20个或更多指针。
如果这是真的,则表示这些客户已有的依赖项数。反转控件(是:IoC)只会使显式而不是隐式。
任何人都难以概述。
鉴于上述情况,还没有发生过?
还有其他方法吗?我更喜欢让应用程序正常运行。
你无法保持'应用程序功能正常,因为它不是。 PersonRepo
模块包含不 referentially transparent的函数。依赖于这种功能的任何其他功能也自动不是引用透明的。
如果大多数应用程序过渡依赖于此类PersonRepo
函数,则意味着它很少(如果有的话)是引用透明的。这意味着它不具备功能性。由于这个原因,它也难以进行单元测试。 (反过来也是如此:Functional design is intrinsically testable,)
最终,功能设计还需要处理不能引用透明的功能。惯用的方法是将这些函数推送到系统的 edge ,这样函数的核心就是纯粹的。这实际上与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
的测试双重实施。您甚至不必使用动态模拟,因为唯一的要求是savePerson
具有正确的类型。
答案 1 :(得分:0)
我建议您删除所有DbSchema.GetDataContext(ConnectionString)资源查找,而是接收db作为参数(或者如果要将其包含在使用块中,则创建数据库的函数)。
示例:
let findPerson dbCreator ssn =
using (dbCreator()) (fun db ->
query {
for ratingId in db.Rating do
where (ratingId.Identifier = ssn)
select (Some { Id = ratingId .Id; Status = Local; Timestamp = ratingId.LastChecked; })
headOrDefault
})
其中dbCreator具有类型单位 - > ' a,在原始示例中使用的是db类型。
findPerson不再依赖于特定的资源,它只是接收一个它可以作用的东西的参数。现在,取代“db'与其他一些更适合用于测试的对象。
这可能与您在C#中可能使用的依赖注入样式方法不同。在这种情况下,上面的模块可能是一个带有构造函数的类,该构造函数接受包含依赖的数据库资源的参数。
savePerson函数稍微复杂一些,因为您有一个额外的副作用来写入数据库。您可以将此行为保留在savePerson函数中,或将其作为附加参数传递。当你来自OO背景时,要记住的关键是,功能代码会让你传递更复杂的行为作为参数。