NUnit - TDD - математическая формула
Если TDD, то вроде сначала в тестах описываем бизнес логику, а потом пишем код, чтобы тестам удовлетворяли? Если так, то бизнес-логика такая:
1) к целочисленным значениям должно применяться целое или равное 0 число процентов - т.е. из целого числа может получиться дробное;
2) при применении отрицательного числа процентов число должно уменьшаться, при этом -100% означает обнуление числа, и дальнейшее снижение процентов должно приводить всё к тому же обнулению;
3) результат должен округляться по математическим правилам до целого - т.е. при ,5 в бОльшую сторону, при -,5 - в меньшую - т.е. away from zero.
Вроде, это диктует правило, что в реализации логики нужно при применении процентов перейти к числам с плавающей запятой, а потом округлить по указанным правилам. Но я сейчас говорю о тесте. Как для такой задачи написать тест? У меня получилось только так - очень много условий (пытался проверить граничные значения процентов - как получившееся число с пятью десятыми или близко к этому будет округляться). Значения считал на калькуляторе, арифметические действия языка не использовал.
Вопрос - можно ли принципиально проще и короче (т.е. не просто несколько кейсов из теста выкинуть, а вообще как-то всё в несколько строк превратить), или и то, что я написал столько кейсов для подобных задач, нормально?
/// <summary> /// Here applies integer division with MidpointRounding.AwayFromZero. /// </summary> /// <param name="testBaseStat"></param> /// <param name="testStat"></param> /// <param name="expected"></param> [TestCase(0, 0, 0)] [TestCase(0, 1, 0)] [TestCase(0, -1, 0)] [TestCase(1, 0, 1)] [TestCase(1, 1, 1)] [TestCase(1, -1, 1)] [TestCase(1, 49, 1)] [TestCase(1, -49, 1)] [TestCase(1, 50, 2)] [TestCase(1, -50, 1)] [TestCase(1, 51, 2)] [TestCase(1, -51, 0)] [TestCase(1, 99, 2)] [TestCase(1, -99, 0)] [TestCase(1, 100, 2)] [TestCase(1, -100, 0)] [TestCase(1, 101, 2)] [TestCase(1, -101, 0)] [TestCase(1, 220, 3)] [TestCase(1, -220, 0)] [TestCase(2, 1, 2)] [TestCase(2, 49, 3)] [TestCase(2, 50, 3)] [TestCase(2, 51, 3)] [TestCase(2, 74, 3)] [TestCase(2, 75, 4)] [TestCase(2, 99, 4)] [TestCase(2, 100, 4)] [TestCase(2, 101, 4)] [TestCase(2, 149, 5)] [TestCase(2, 150, 5)] [TestCase(2, 151, 5)] [TestCase(11, 221, 35)] [TestCase(11, 239, 37)] [TestCase(int.MaxValue, int.MinValue, 0)] [TestCase(int.MinValue, int.MinValue, 0)] public void AddModifier_PercentValueModifier_ValueChangesCorrectly(int testBaseStat, int testStat, int expected) { var charStat = new CharacterStat(testBaseStat); charStat.AddModifier(new StatModifier(testStat, StatModifierType.Percent)); var result = charStat.Value; Assert.AreEqual(result, expected); }
Вот собственно имплементация, но это так, для дополнения (тесты все проходят)
void CalculateValue() { var finalValue = BaseValue; // testBaseStat in the test foreach (var mod in StatMidifiers) { var percentage = mod.Value < -100 ? -100 : mod.Value; var valueToRound = finalValue * (1 + percentage / 100.0); finalValue = (int)Math.Round(valueToRound, MidpointRounding.AwayFromZero); } Value = finalValue; }
Ладно, не привязываясь к точным определениям. Я написал требования, сделал тест. Это нормальный тест, или как надо?
Или по-другому. Я сначала написал логику по требованиям. На логику сделал тест. Нормальный тест, или как надо?
Меня больше интересует, нормальный ли тест. Привёл пример логики, которую он тестирует.
Если TDD, то вроде сначала в тестах описываем бизнес логику, а потом пишем код, чтобы тестам удовлетворяли?
TDD - итеративный процесс.
Грубо говоря, если ты делаешь калькулятор, то пишешь 1-й тест:
[TestCase(0, 0, 0)] public void AddModifier_PercentValueModifier_ValueChangesCorrectly(int summand1, int summand2, int expected) { var calc= new Calc(); int result calc.Add(summand1, summand2); Assert.AreEqual(expected, result); }
и код для будет таким:
public class Calc { public int Add(int summand1, int summand2) { return 0; } }
После этого добавляем новые тесты и делаем рефакторинг после каждого теста.
1) к целочисленным значениям должно применяться целое или равное 0 число процентов - т.е. из целого числа может получиться дробное;
Ну это совсем необязательно ;)
2) при применении отрицательного числа процентов число должно уменьшаться, при этом -100% означает обнуление числа, и дальнейшее снижение процентов должно приводить всё к тому же обнулению;
Странное требование, ну да ладно :)
3) результат должен округляться по математическим правилам до целого - т.е. при ,5 в бОльшую сторону, при -,5
Ну ОК.
В чем разница между этими тестами:
[TestCase(1, -1, 1)] [TestCase(1, -49, 1)] [TestCase(1, -50, 1)]
В чем разница между этими тестами:
[TestCase(1, -51, 0)] [TestCase(1, -99, 0)] [TestCase(1, -100, 0)] [TestCase(1, -101, 0)] [TestCase(1, -220, 0)]
В чем разница между этими тестами:
[TestCase(0, 0, 0)] [TestCase(0, 1, 0)] [TestCase(1, 0, 1)] [TestCase(1, 1, 1)] [TestCase(1, 49, 1)] [TestCase(1, 50, 2)] [TestCase(1, 51, 2)] [TestCase(1, 99, 2)] [TestCase(1, 100, 2)] [TestCase(1, 101, 2)] [TestCase(1, 220, 3)] [TestCase(2, 1, 2)] [TestCase(2, 49, 3)] [TestCase(2, 50, 3)] [TestCase(2, 51, 3)] [TestCase(2, 74, 3)] [TestCase(2, 75, 4)] [TestCase(2, 99, 4)] [TestCase(2, 100, 4)] [TestCase(2, 101, 4)] [TestCase(2, 149, 5)] [TestCase(2, 150, 5)] [TestCase(2, 151, 5)] [TestCase(11, 221, 35)] [TestCase(11, 239, 37)]
Вопрос - можно ли принципиально проще и короче (т.е. не просто несколько кейсов из теста выкинуть, а вообще как-то всё в несколько строк превратить), или и то, что я написал столько кейсов для подобных задач, нормально?
Можно. Делай так, как говорится в учебнике - добавляй по одному тест-кейсу. При этом как только ты добавил тест-кейс, он должен быть красным. После это этого ты этот тест-кейс фиксишь и после этого делаешь рефакторинг. После рефакторинга все тесты должны оставаться зелеными.
TDD - итеративный процесс.
Грубо говоря, если ты делаешь калькулятор, то пишешь 1-й тест:
и код для будет таким:
После этого добавляем новые тесты и делаем рефакторинг после каждого теста.
Но это же невероятно долго! Если писать сначала логику, а потом добавлять тесты, то я могу с первого раза написать то, что надо.
Опять же, насколько я могу понять, TDD работает хорошо только для заранее хорошо проработанных требований и описанной бизнес-логике. Если предполагается много раз менять её ещё на этапе написания программы, то TDD будет просто тормозить разработку? А если разрабатываешь вообще прототип, и ещё не знаешь, что в конце должно получиться, то и требований и описания бизнес-логики конкретной нет, а всё лишь в общих чертах, и TDD вообще трудно применить - непонятно, что конкретно тестировать. Правильно?
1) к целочисленным значениям должно применяться целое или равное 0 число процентов - т.е. из целого числа может получиться дробное;Ну это совсем необязательно ;)
Это не обязательно, но возможно. Причём очень возможно. К единице примените 40%, получится дробное по логике, но не по коду (в коде округлится согласно правилам округления языка программирования).
Я имею ввиду, что тестируемый код должен обрабатывать такие вещи и иметь логику округления, которая может отличаться от логики округления языка. Например, можно считать, что любое, даже мизерное превышение над целым должно округляться в сторону бОльшего целого (5,00001 = 6) - тогда это надо оговорить в описании задачи и это должно отражаться в тестах. В приведённых мной тестах другое правило - обычное математическое округление.
2) при применении отрицательного числа процентов число должно уменьшаться, при этом -100% означает обнуление числа, и дальнейшее снижение процентов должно приводить всё к тому же обнулению;Странное требование, ну да ладно :)
Я хотел описать, что такие проценты, как например -200%, должны означать то же, что и -100%. Просто в зависимости от реализации кода, который вычисляет проценты, если не вводить пороговые проверки, то можно получить неожиданные результаты при числе процентов меньше -100.
В чем разница между этими тестами:
Вот я и пытаюсь выяснить - мало, много или достаточно я тестовых кейсов применил.
[TestCase(1, -1, 1)] [TestCase(1, -49, 1)] [TestCase(1, -50, 1)]
Здесь проверка, правильно ли округляются пороговые значения около половины (условие, когда десятичная часть получается равной ",5" или близко к этому). Проверял на единице и брал значения результата чуть ниже порогового, равное и чуть выше.
[TestCase(1, -51, 0)] [TestCase(1, -99, 0)] [TestCase(1, -100, 0)] [TestCase(1, -101, 0)] [TestCase(1, -220, 0)]
То же самое, но проверка, а правильно ли округляются значения вблизи целого (т.е. когда есть десятичная часть типа ",01").
Последний тест кейс - отрицательное число процентов, которое меньше -100%, и которое должно работать как -100%.
[TestCase(0, 0, 0)] [TestCase(0, 1, 0)] [TestCase(1, 0, 1)] [TestCase(1, 1, 1)] [TestCase(1, 49, 1)] [TestCase(1, 50, 2)] [TestCase(1, 51, 2)] [TestCase(1, 99, 2)] [TestCase(1, 100, 2)] [TestCase(1, 101, 2)] [TestCase(1, 220, 3)] [TestCase(2, 1, 2)] [TestCase(2, 49, 3)] [TestCase(2, 50, 3)] [TestCase(2, 51, 3)] [TestCase(2, 74, 3)] [TestCase(2, 75, 4)] [TestCase(2, 99, 4)] [TestCase(2, 100, 4)] [TestCase(2, 101, 4)] [TestCase(2, 149, 5)] [TestCase(2, 150, 5)] [TestCase(2, 151, 5)] [TestCase(11, 221, 35)] [TestCase(11, 239, 37)]
Проверка, как код работает с нулём.
Как код работает с нулём процентов.
Опять пороговые значения, но в положительной области.
Проверка с чётными и нечётными числами (единица и двойка) - опять целые и пороговые дробные.
Для двойки пороговые проценты будут 50+-1, 75+-1, 100+-1.
Последние два теста - просто наугад взятые числа "побольше".
Но это же невероятно долго!
В начале да.
Если писать сначала логику, а потом добавлять тесты, то я могу с первого раза написать то, что надо.
Очевидно, что не можешь ;) Если бы мог, то этой темы бы не было.
Опять же, насколько я могу понять, TDD работает хорошо только для заранее хорошо проработанных требований и описанной бизнес-логике. Если предполагается много раз менять её ещё на этапе написания программы, то TDD будет просто тормозить разработку?
Нет. Я бы даже сказал, что в случаях, когда нет хорошо проработанных требований и описаний - TDD просто must have. Т.к. TDD проявит противоречивые требования и хотелки еще на стадии проектирования. И это в результате поможет избежать крупных переделок и/или костылей в будущем.
А если разрабатываешь вообще прототип, и ещё не знаешь, что в конце должно получиться, то и требований и описания бизнес-логики конкретной нет, а всё лишь в общих чертах, и TDD вообще трудно применить - непонятно, что конкретно тестировать. Правильно?
Неправильно. Если ты не знаешь что тестировать, то смысла в тестах конечно нет... впрочем, в коде, который ничего не делает смысла тоже нет :)
Вопрос - можно ли принципиально проще и короче (т.е. не просто несколько кейсов из теста выкинуть, а вообще как-то всё в несколько строк превратить), или и то, что я написал столько кейсов для подобных задач, нормально?Можно. Делай так, как говорится в учебнике - добавляй по одному тест-кейсу. При этом как только ты добавил тест-кейс, он должен быть красным. После это этого ты этот тест-кейс фиксишь и после этого делаешь рефакторинг. После рефакторинга все тесты должны оставаться зелеными.
Звучит правильно. Но верен ли такой подход для подобных задач, как у меня - математика? Ведь видно же, что тест кейсов просто дофига, и делать два-три десятка итераций - это писать одну простую функцию (процентное округление по кастомным правилам) чуть ли не весь день.
С другой стороны, я думаю, что именно этот случай как раз такой, когда можно сделать исключение и написать сначала код по требованиям бизнес-логики, который уже заранее удовлетворит большинству тест кейсов (если программист не дурак, он сможет это сделать), а потом уже написать тест кейсы и править баги в коде. Я сделал так и у меня получилось примерно 5-7 итераций - т.е. я написал требования, написал код, написал тесты с пороговыми значениями (не все, а что пока в голову пришли), возникли ошибки, поправил код, добавил тест кейсов, снова поправил. Если бы я делал как вы написали, то я бы сделал число итераций по числу тест кейсов... ну или не стал бы валять дурака, а почитал бы требования и понял хотя бы примерно, что требуется, и сразу написал бы код, который удовлетворял бы подавляющему числу тест кейсов.
Моё мнение - иногда проще СНАЧАЛА написать код не по тестам, а по требованиям. Потом догонять тестами и итерациями. Но, как я понимаю, TDD против этого?
Если писать сначала логику, а потом добавлять тесты, то я могу с первого раза написать то, что надо.Очевидно, что не можешь ;) Если бы мог, то этой темы бы не было.
Немного не так. Как я уже написал, я имею ввиду, что с первого раза можно написать БЛИЗКО к тому, что надо. А если опытный - ОЧЕНЬ БЛИЗКО. Вплоть до очевидности и ПРЯМО ТО, ЧТО НАДО. Поэтому в задачах, где предполагается много тест кейсов, и значительная часть которых явно (по опыту) покрывается первой же итерацией, лучше сначала написать код, а потом тест кейсы к нему. Нет?
Опять же, насколько я могу понять, TDD работает хорошо только для заранее хорошо проработанных требований и описанной бизнес-логике. Если предполагается много раз менять её ещё на этапе написания программы, то TDD будет просто тормозить разработку?Нет. Я бы даже сказал, что в случаях, когда нет хорошо проработанных требований и описаний - TDD просто must have. Т.к. TDD проявит противоречивые требования и хотелки еще на стадии проектирования. И это в результате поможет избежать крупных переделок и/или костылей в будущем.
Мне это трудно понять. Или я не знаю границы применимости эти высказываний. Я могу работать по такой методике на работе, если требуется, но в своих собственных проектах я предпочитаю сначала написать какой-то базовый код, а потом уже тестировать его и подгонять под требования.
Это не обязательно, но возможно. Причём очень возможно. К единице примените 40%, получится дробное по логике, но не по коду (в коде округлится согласно правилам округления языка программирования).
Начнем с того, что это нифига не требование :) Формулируй требования к черному ящику. Результат твоих вычислений - целочисленный. Так что когда ты говоришь о числах с плавающей точкой - ты вмешиваешься в работу черного ящика.
В твоем же случаевсе можно реализовать на интах - достаточно просто умножить на 100 ;) В конце вычисления соответственно разделить на 100. При этом для правильного округления надо просто домавить 50 ;) Т.е. (result + 50) / 100. И никаких чисел с плавающей точкой.
Я хотел описать, что такие проценты, как например -200%, должны означать то же, что и -100%.
Но ты же понимаешь, что это странное требование ;)
Ок, пример: 10 - 100% должно стать нулем. Нет проблем. -10 - 10% - должно стать -11, но при этом -10 - 100% почему-то должно стать нулем :) Тебя ничего не смущает в этом требовании? :)
Здесь проверка, правильно ли округляются пороговые значения около половины (условие, когда десятичная часть получается равной ",5" или близко к этому). Проверял на единице и брал значения результата чуть ниже порогового, равное и чуть выше.
Ты же понимаешь, что если x.5 округляется до x+1, то x.51 и x.99 тоже округляются до x+1 ;)
То же самое, но проверка, а правильно ли округляются значения вблизи целого (т.е. когда есть десятичная часть типа ",01").
Таже история - если x.49 округляется до х, то х.01 тоже округляется до х.
Последний тест кейс - отрицательное число процентов, которое меньше -100%, и которое должно работать как -100%.
-101 тоже меньше 100. Зачем нужен тест с -220?
Проверка, как код работает с нулём.
Зачем? Там какие-то требования к 0?
Как код работает с нулём процентов.
С 0 процентов какая-то особая работа?
Опять пороговые значения, но в положительной области.
Поврорение уже
существующих тестов?
Проверка с чётными и нечётными числами (единица и двойка) - опять целые и пороговые дробные.
Где-то есть требования, что с четными и нечетными числами надо по разному работать?
Для двойки пороговые проценты будут 50+-1, 75+-1, 100+-1.
Да пофиг, ты пороги уж проверил :)
Последние два теста - просто наугад взятые числа "побольше".
Что ты этими тестами тестируешь?
Ты понимаешь, что каждый тест должен тестировать уникальное поведение? Если 2 теста тестируют одно и тоже, то один из тестов можно смело выкинуть.
Но верен ли такой подход для подобных задач, как у меня - математика?
Да :)
Ведь видно же, что тест кейсов просто дофига
На самом деле, тест-кейсов там не дофига :) Больше половины твоих тест-кейсов можно выкинуть, т.к. они не тестируют ничего нового. Попробуй сделать описанную тобой задачу как по учебнику ;)
Но, как я понимаю, TDD против этого?
Да, TDD против :)
В твоем же случаевсе можно реализовать на интах - достаточно просто умножить на 100 ;) В конце вычисления соответственно разделить на 100. При этом для правильного округления надо просто домавить 50 ;) Т.е. (result + 50) / 100. И никаких чисел с плавающей точкой.
Но это кастомные "хаки" округления, и кроме того я теряю два порядка в длине целых чисел, что может быть существенным. Я хочу максимального отсутствия хаков и максимального использования стандартных библиотек, в том числе для округлений. И чтобы программист не заморачивался, что где-то длина интов режется на 2 порядка.
Я хотел описать, что такие проценты, как например -200%, должны означать то же, что и -100%.Но ты же понимаешь, что это странное требование ;)
Можно написать код, который будет без этого требования формально проходить тесты, генерируя дичь. Например, у меня получалось, что число просто уходило в отрицательные значения.
Например, 10 - 250% = -15.
Ок, пример: 10 - 100% должно стать нулем. Нет проблем. -10 - 10% - должно стать -11, но при этом -10 - 100% почему-то должно стать нулем :) Тебя ничего не смущает в этом требовании? :)
Пожалуй, нужно ещё ввести запрет обработки отрицательных чисел для такого округления. Или просто юзать беззнаковые целые. Но тогда код может уходить в другой конец - Int32.Max.
Видите, как сложно писать тесты? А до этого писать требования обычными словами? - За всем и не уследишь.
Ты же понимаешь, что если x.5 округляется до x+1, то x.51 и x.99 тоже округляются до x+1 ;)
Если пользоваться стандартными правилами округления. А если хакерить и кастомизить, то не факт. Короче, проверка от дурака. На всякий случай.
-101 тоже меньше 100. Зачем нужен тест с -220?
Просто любое число меньше -100.
А нужно ли комментировать каждый TestCase? Вы так делаете?
Проверка, как код работает с нулём.Зачем? Там какие-то требования к 0?
Требование простое: 0 при округлении должен быть всегда 0.
Код, удовлетворящий тестам, может содержать баги - например, неправильно обрабатывать 0. Кто знает, кто там как округляет. При применении стандартных библиотечных функций проблем с 0 быть не должно. Но тестер не знает, что будет применяться.
Вообще, насколько я знаю, что в математике, что по идее в программировании, должны проверяться разные пороговые (граничные), асимптотические и нулевые значения. Обычно там разные функции могут повести себя непредсказуемо или резко менять своё значение.
Опять пороговые значения, но в положительной области.Поврорение уже существующих тестов?
Да кто их знает, итих программёров, что они там напишут? Их же сейчас всех подряд берут, по объявлениям!
Проверка с чётными и нечётными числами (единица и двойка) - опять целые и пороговые дробные.Где-то есть требования, что с четными и нечетными числами надо по разному работать?
Работать надо одинаково. Но то же опасение - старнадртыне функции округления не требуют таких подробных проверок. Самописные костыли - возможно требуют.
Насколько я понял, я в тестах должен доверять самописным округлениям и вообще коду не из проверенных многими людьми до меня источников. У самописных фреймворков должны быть свои тесты - я не должен тестировать сторонние фреймворки, даже самописные?
Вобщем, если не быть таким подозрительным и не тестировать за других то, что я не должен тестировать, то можно сократить количество моих тестов раза в 2-3?
Не всегда очевидны очевидные вещи.
Но это кастомные "хаки" округления
Это не хаки округления :) Просто когда формулируешь требования к "черному ящику" не лезь не в свое дело - не предписывай ящику как ему работать. Ограничивайся требованиями к результатам.
кроме того я теряю два порядка в длине целых чисел
Должен заметить, что никаких ограничений у тебя нет. Если хочешь подумать о пограничных значениях, то попробуй сделать тест, который проверяет увеличение int.MaxValue на 5% :)
Я хочу максимального отсутствия хаков и максимального использования стандартных библиотек, в том числе для округлений.
Свои хотлки ты можешь реализовывать в своем коде. На этапе составления требований речь идет о "черном ящике" и ты не можешь предписывать черному ящику как ему работать.
И чтобы программист не заморачивался, что где-то длина интов режется на 2 порядка.
Составляй грамотно требования, а не придумывай оправдания не лету. Пример с увеличением int.MaxValue на 5% я уже привел. В твои требования этот пример укладывается, вот только получится хрень :)
Можно написать код, который будет без этого требования формально проходить тесты, генерируя дичь. Например, у меня получалось, что число просто уходило в отрицательные значения.Например, 10 - 250% = -15.
А почему это дичь? :)
Видите, как сложно писать тесты?
Ничего сложно :)
А до этого писать требования обычными словами? - За всем и не уследишь.
Именно поэтому требования обычно делаются одними людьми, а тесты пишутся другими :)
На всякий случай.
Ну так проверял бы тогда все варианты с шагом в 1% для всех чисел :)
Просто любое число меньше -100.
Так зачем нужен тест с -220, когда строчкой выше был для -101?
А нужно ли комментировать каждый TestCase? Вы так делаете?
Я стараюсь в названии теста указывать тестируемую фичу, входные условия и ожидаемый результат. Тесты не комментирую.
Требование простое: 0 при округлении должен быть всегда 0.
А 100 при округлении должно быть чем-то другим? Чем 0 отличается от 100? или от 123456?
Но тестер не знает, что будет применяться.
Тестер не пишет юнит-тесты и не работает по TDD. TDD - инструмент программиста. В задачу юнит-тестирования не входит поиск новых багов.
Вообще, насколько я знаю, что в математике, что по идее в программировании, должны проверяться разные пороговые (граничные), асимптотические и нулевые значения. Обычно там разные функции могут повести себя непредсказуемо или резко менять своё значение.
Да, но пограничные значения всегда разные. В выражении 1/(x+2) проверять надо поведение при х=-2, а 0 никого не интересует.
я не должен тестировать сторонние фреймворки, даже самописные?
Ты вообще не должен тестировать внешний код. Даже если ты используешь свой собственный фреймворк, в месте, где он используется этот фреймворк является внешним кодом.
При написании юнит теста ты должен исходить из того, что внешний код не содержит багов.
Можно написать код, который будет без этого требования формально проходить тесты, генерируя дичь. Например, у меня получалось, что число просто уходило в отрицательные значения.Например, 10 - 250% = -15.А почему это дичь? :)
Надо 0.
Например, на персонажа могут быть навешаны дебаффы, которые загонят какой-нибудь его показатель в -250%. Тогда показатель должен просто обнулиться, а не уйти в отрицательное значение.
Просто любое число меньше -100.Так зачем нужен тест с -220, когда строчкой выше был для -101?
Требование простое: 0 при округлении должен быть всегда 0.А 100 при округлении должно быть чем-то другим? Чем 0 отличается от 100? или от 123456?
Я думаю, это от недостатка опыта написания тестов и вообще опыта. Я хотел порогое значение вблизи 100% (или -100%), и просто большое число процентов, существенно больше порогового, но не равное другому пороговому. Объяснить не могу - мне кажется, что это не помешало бы.