Разработка тестируемого функционального кода
Мне нравится идея написания чистых функций, но у меня возникают проблемы с пониманием способов их объединения, которые приводят к тестируемому коду. Я привык извлекать классы, а затем соответствующим образом заглушки и чувствую, что мне не хватает некоторого ключевого понимания функционального программирования.
Вот пример, который я уменьшил от проблемы, с которой я сейчас сталкиваюсь.
Я хочу взять список из списка дат и отфильтровать его по тем, которые соответствуют критериям "возможности".
В 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
, Ваши тесты будут сосредоточены вокруг того факта, что с калькулятором дважды будут проводиться консультации с правильными параметрами.