【问题标题】:Designing Testable Functional Code设计可测试的功能代码
【发布时间】:2013-06-12 16:56:27
【问题描述】:

我喜欢编写纯函数的想法,但我无法理解将它们组合在一起以生成可测试代码的方法。我习惯于提取类然后适当地存根,并且觉得我缺少对函数式编程的一些关键见解。

这是我从当前面临的问题中精简的示例。

我想获取一份日期列表,并筛选出符合“机会”标准的日期。

在 C# 中它看起来像:

    static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
    {
        return dates.Where(ds => HasOpportunity(ds)).ToList();
    }

    static bool HasOpportunity(List<DateTime> dates)
    {
        var threshold = 0.05D;

        var current = OpportunityProbability(dates, DateTime.Now);
        var previous = OpportunityProbability(dates, DateTime.Now.Subtract(TimeSpan.FromDays(30)));

        return previous >= threshold && current < threshold;
    }

    static double OpportunityProbability(List<DateTime> dates, DateTime endDate)
    {
        // does lots of math
        return 0.0D;
    }

所以在提示我们有OpportunityProbability 我知道如何测试。我遇到的麻烦是在HasOpportunity 和更进一步的链条 (Opportunities)。

我知道如何测试HasOpportunity 的唯一方法是删除OpportunityProbability,但我做不到。而且我不想创建假数据来满足OpportunityProbability 的设计,以便测试HasOpportunity。所以尽管这两个函数都是纯函数,但它是不可测试的,我觉得它的设计很糟糕。

因此我觉得我在设计糟糕的函数式代码:)

我关心HasOpportunity 主要是布尔测试。给定两个双精度数和一个阈值,进行比较并返回结果。为了获得这两个双打,它使用了一个需要日期列表和日期列表的函数。这导致HasOpportunity 也负责确定日期(DateTime.Now 和 30 天前)。也许我可以把它分开:

    static bool HasOpportunity(double probability1, double probability2)
    {
        var threshold = 0.05D;

        return probability2 >= threshold && probability1 < threshold;
    }

所以这显然是可测试的。我什至可以提高门槛:

    static bool HasOpportunity(double threshold, double probability1, double probability2)
    {
        return probability2 >= threshold && probability1 < threshold;
    }

所以这更通用。

我这样做时遇到的问题是我刚刚将内容移至Opportunities

    static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
    {
        return dates.Where(ds => {
            var current = OpportunityProbability(ds, DateTime.Now);
            var previous = OpportunityProbability(ds, DateTime.Now.Subtract(TimeSpan.FromDays(30)));
            return HasOpportunity(0.05D, current, previous);
        }).ToList();
    }

这是我不知道下一步要采取的地方。

功能霸主有什么想法吗?帮我用 C# 写 F#,在此先感谢!

更新

所以再往前走一步,我可以得到:

    static List<List<DateTime>> Opportunities(double threshold, DateTime currentDate, DateTime previousDate, List<List<DateTime>> dates)
    {
        return dates.Where(ds => {
            var current = OpportunityProbability(ds, currentDate);
            var previous = OpportunityProbability(ds, previousDate);
            return HasOpportunity(threshold, current, previous);
        }).ToList();
    }

所以我仍然不知道如何测试这个,但很好的是这个函数的参数最终定义了机会是什么:

  • 阈值
  • 第一次约会
  • 第二个日期

然后给出一个日期列表,它可以给你机会。

【问题讨论】:

  • 想评论为什么投反对票?
  • “功能霸主有什么想法吗?” 有什么想法?如何测试内置运算符? “帮我用 C# 写 F#,先谢谢了!” 什么 F#?
  • 我觉得这是非常通用的代码。它在 C# 中,因为我必须选择一个,但我可以很容易地用 F# 重写它。整个问题是基于如何以可测试的方式组合纯函数。我可以写得更抽象一些,但我认为它没有任何帮助。
  • 我认为你的最后一步很好——因为它是一个纯函数,你可以使用 NUnit 中的随机测试功能,或者 FsCheck(也可以用于 C# 代码)。跨度>
  • 如果回答能帮助您解决问题,请记得接受。

标签: c# f# functional-programming functional-testing c#-to-f#


【解决方案1】:

您是否考虑过使用高阶函数?将 OpportunityProbability 函数传入 HasOpportunity。

static List<List<DateTime>> Opportunities(List<List<DateTime>> dates)
{
    return dates.Where(ds => HasOpportunity(ds, OpportunityProbability, OpportunityProbability)).ToList();
}

static bool HasOpportunity(List<DateTime> dates, Func<List<DateTime>, DateTime, double> currentProb, Func<List<DateTime>, DateTime, double> prevProb)
{
    var threshold = 0.05D;

    var current = currentProb(dates, DateTime.Now);
    var previous = prevProb(dates, DateTime.Now.Subtract(TimeSpan.FromDays(30)));

    return previous >= threshold && current < threshold;
}

static double OpportunityProbability(List<DateTime> dates, DateTime endDate)
{
    // does lots of math
    return 0.0D;
}

现在您可以独立地测试 OpportunityProbability 和 HasOpportunity(在 HasOpportunity 的情况下,您“存根”第二个和最后一个参数。如果您想要更多的分离,您也可以将 OpportunityProbability 传递给 Opportunities。

【讨论】:

  • 我确实想过,但没有尝试。这些东西是我希望为 FP 建立的直觉。我实际上并不关心测试代码,我只是将它用作设计的代理。感谢您的评论。
  • 您可能会发现以下文章很有用,通常它是 F# 和函数式编程技术的绝佳网站:fsharpforfunandprofit.com/posts/recipe-part1
【解决方案2】:

我认为你应该加入一些古老的面向对象,并尊重单一职责模式。一种可能的方法是创建类:

  • OpportunityCalculator 使用方法double OpportunityProbability(List&lt;DateTime&gt; dates, DateTime endDate)

  • OpportunityFilter 使用方法bool HasOpportunity(double threshold, double probability1, double probability2)

这些类可以独立测试:

  • OpportunityCalculator 抽象出复杂的数学。
  • 在测试OpportunityFilter 时,您可以将OpportunityCalculator 存根。您的测试将围绕这样一个事实,即计算器将使用正确的参数进行两次查询。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-10-09
    • 2017-02-21
    • 1970-01-01
    • 1970-01-01
    • 2017-02-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多