Здравствуйте, Кодт, Вы писали:
К>Нет, пока что я вижу, куда ты тянешь дискуссию: "поскольку юнит-тесты — это круто, давайте выкинем отладчик".
Нет, не так. Попытка заменять тесты отладчиком приводят именно что к фигак-фикаг продакшену.
К>Проект хромиума покрыт тестами сверху донизу, но их настолько недостаточно, что приходится иногда даже трахаться с gdb в командной строке.
Есть мнение, что без тестов он вообще бы не взлетел.
К>Э, ты тут спрыгиваешь с темы.
К>Дано: написал кривой код, который на редких условиях зримо глючит. Обкладывай его тестами для поиска бага.
Эээ, тесты не для поиска багов. А для написания верифицируемого в контролируемых условиях кода. Почувствуйте разницу (с).
К>Можешь даже отрефакторить наивный алгоритм, исходно обложенный тестами. Собственно, так оно и происходит: взяли наивную реализацию, начали переписывать на замороченную, затейливо накосячили (а старые тесты при этом выполнились).
Еще раз — когда в код вносят новые граничные условия, для этих условий пишут новые тесты. Это часть процесса разработки. Если ему не следовать, то, как говорится, щасливой атладки!
К>>>Если будешь рожать "с листа", то риск накосячить с адресной арифметикой достаточно велик.
L>>Ключевое слово "риск". Привыкший работать по TDD моментально распознает эти риски. И задает себе вопрос — как я напишу тест, который проверит, накосячил ли я в данном месте? Это не всегда можно, но чаще всего — как раз можно.
К>До какой степени детализации ты будешь обкладывать код тестами?
До приемлимой. Наша задача — написать код с предсказуемым поведением в определенных условиях и верифицировать это поведение в пределах этих самых условий.
К>К>void go_around_zero(int n) { for(int i = -n*10; i <= n*10; ++i) { do_smth(); } }
К>// TODO отрефакторить, чтобы покрыть тестами
К>
Эээ, тесты по определению тестируют наблюдаемое поведение. Что можно явно наблюсть у данного куска кода? Факт зацикливания? Это неявно. do_smth() явно не зависит ни от n, ни от i.
Можно нужно переписать цикл, убрав чертовщину с -n*10.
Но ладно, в принципе, пример годный. Допустим, что исходный код имеет некий пока непонятный мне смысл. Сделаем неявное явным
template <class F>
void go_around_zero_v2(int n, F func) { for(int i = -n*10; i <= n*10; ++i) { func(); } }
TEST(go_around_zero_v2, testNegativeN)
{
int n = 10;
unsigned int expected = n*10*2+1; // Ну или 100500 от балды, чтобы просто отловить бесконечный цикл
unsigned int actual = 0;
go_around_zero(-10, [&]() {
ASSERT_LT(expected, actual++) << "Possible infinite loop"; // Враг не пройдет
});
ASSERT_EQ(expected, actual); // Исходный код до этого момента не дойдет
}
Получаем провал теста.
получаем провал и начинаем думать — кто виноват и что делать. В смысле, должен ли go_around_zero выполнять проверку входных данных или по условию задачи они проверяются уровнем выше и тут их проверять смысла нет. Если последний вариант — добавляем огромный WARNING, нет, лучше ACHTUNG в описание функции. Если первый, то выясняем, что именно
нужно проверить — только на отрицательное значение n или же на переполнение и т.п. Или же в ходе дискусии выяснится, что этот go_around_zero — остаток говнокода из каменного века, и т.к. do_something от индекса не зависит, то код можно переписать с условием цикла здорового человека, убрав возможности для неявного переполнения.
На больших проектах, где задействовано много людей, бывает так, что концов не найти и все отмахиваются, но при этом настаивают на сохранении go_around_zero. Для очистки совести меняю оригинальный код на
template <class F>
void go_around_zero_v2(int n, F func) {
if (math::abs(n) > MAX_UINT/20 -1)
{
throw std::runtime_error("Loop will go bananas!");
}
for(int i = -n*10; i <= n*10; ++i) {
func();
}
}
void go_around_zero(int n)
{
go_around_zero_v2(n, do_smth);
}
Примерно все.
К>А в это время в Виллабаджо уже делают фигак-фигак-продакшен.
Через 3 месяца тестеры пишут баг-репорт "падает". Смотришь в лог, видишь bananas. Пинаешь того, кто вызывал твой код.