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

时间:2015-11-01 20:18:57

标签: unit-testing f# functional-programming

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

我知道How to test functions in f# with external dependencies并且我知道各种博客文章显示当您的界面只有一种方法时,这样做是多么容易。

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

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

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

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

  1. 我可以将我的模块实现为接口和类。虽然这仍然是F#,但我觉得这是一种错误的方法,因为我失去了很多好处。这在[{3}}

  2. 中也有争议
  3. 调用例如的代码。我们的PersonRepo可以作为PersonRepo的所有函数的参数函数指针。然而,这很快就会积累到20个或更多指针。任何人都难以概述。它还使代码库变得脆弱,就像我们PersonRepo中的每个新函数一样,我需要将“函数指针”“添加到”根组件中。

  4. 我可以创建一个包含我PersonRepo的所有函数的记录(我需要模拟的每个抽象一个)。但我不确定我是否应该创建一个明确的类型,例如用于lookupPerson (Id;Status;Timestamp)

  5. 中使用的记录
  6. 还有其他方法吗?我更喜欢让应用程序正常运行。

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

    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 = ...
    

2 个答案:

答案 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背景时,要记住的关键是,功能代码会让你传递更复杂的行为作为参数。