Разработка тестируемого функционального кода

Мне нравится идея написания чистых функций, но у меня возникают проблемы с пониманием способов их объединения, которые приводят к тестируемому коду. Я привык извлекать классы, а затем соответствующим образом заглушки и чувствую, что мне не хватает некоторого ключевого понимания функционального программирования.

Вот пример, который я уменьшил от проблемы, с которой я сейчас сталкиваюсь.

Я хочу взять список из списка дат и отфильтровать его по тем, которые соответствуют критериям "возможности".

В 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 это в основном булевский тест. С учетом двух значений типа double и порога выполните сравнение и верните результат. Чтобы получить эти два числа, он использует функцию, которая требует список дат и дату. Это приводит 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();
    }

Это где я не знаю, следующий шаг, чтобы сделать.

Есть мысли о функциональных повелителях? Помогите мне написать F# в C#, заранее спасибо!

Обновить

Сделав еще один шаг, я могу получить:

    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();
    }

Так что я до сих пор не знаю, как это проверить, но приятно, что параметры этой функции в итоге определяют, что это за возможность:

  • порог
  • первое свидание
  • второе свидание

А затем, учитывая список дат, он может дать вам возможности.

2 ответа

Вы рассматривали возможность использования функций высшего порядка? Передайте функции 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.

Я думаю, что вы должны добавить немного старой доброй ориентации объекта и соблюдать Шаблон единой ответственности. Одним из возможных способов является создание классов:

  • OpportunityCalculator с методом double OpportunityProbability(List<DateTime> dates, DateTime endDate)

  • OpportunityFilter с методом bool HasOpportunity(double threshold, double probability1, double probability2)

Эти классы могут быть протестированы независимо:

  • OpportunityCalculator излагает сложную математику.
  • Во время тестирования OpportunityFilterВы можете заглушить OpportunityCalculator, Ваши тесты будут сосредоточены вокруг того факта, что с калькулятором дважды будут проводиться консультации с правильными параметрами.
Другие вопросы по тегам