Так, я ранее не был действительно в практике записи модульных тестов - теперь я отчасти, и я должен проверить, на правильном пути ли я.
Скажите, что у Вас есть класс, который имеет дело с математическими вычислениями.
class Vector3
{
public: // Yes, public.
float x,y,z ;
// ... ctors ...
} ;
Vector3 operator+( const Vector3& a, const Vector3 &b )
{
return Vector3( a.x + b.y /* oops!! hence the need for unit testing.. */,
a.y + b.y,
a.z + b.z ) ;
}
Существует 2 способа, которыми я могу действительно думать, чтобы сделать модульный тест на Векторном классе:
bool UnitTest_ClassVector3_operatorPlus()
{
Vector3 a( 2, 3, 4 ) ;
Vector3 b( 5, 6, 7 ) ;
Vector3 result = a + b ;
// "expected" is computed outside of computer, and
// hard coded here. For more complicated operations like
// arbitrary axis rotation this takes a bit of paperwork,
// but only the final result will ever be entered here.
Vector3 expected( 7, 9, 11 ) ;
if( result.isNear( expected ) )
return PASS ;
else
return FAIL ;
}
bool UnitTest_ClassVector3_operatorPlus()
{
Vector3 a( 2, 3, 4 ) ;
Vector3 b( 5, 6, 7 ) ;
Vector3 result = a + b ;
// "expected" is computed HERE. This
// means all you've done is coded the
// same thing twice, hopefully not having
// repeated the same mistake again
Vector3 expected( 2 + 5, 6 + 3, 4 + 7 ) ;
if( result.isNear( expected ) )
return PASS ;
else
return FAIL ;
}
Или есть ли другой способ сделать что-то вроде этого?
Способ №1 - это общепринятый способ проведения модульного тестирования. Переписывая свой код, вы можете переписать неисправный код в тест. В большинстве случаев для каждого метода, который вы тестируете, требуется только один реальный тест, поэтому это не СЛИШКОМ по времени.
Я считаю, что выписывание чисел (ваш второй подход) - правильный вариант. Это делает ваши намерения более очевидными для тех, кто читает тест.
Предположим, вы не перегружали оператор +
, а вместо этого использовали функцию с ужасным именем f
, которая занимала два Vector3
s. Вы тоже этого не задокументировали, поэтому я просмотрел ваши тесты, чтобы узнать, что должен делать f
.
Если я увижу Ожидаемый Vector3 (7, 9, 11)
, мне придется вернуться назад и перепроектировать , как 7, 9 и 11 были «ожидаемыми» результатами. . Но если я вижу Ожидаемый Vector3 (2 + 5, 6 + 3, 4 + 7)
, то мне ясно, что f
добавляет отдельные элементы аргументов в новый Vector3
.
Вы не задавали этого в своем вопросе, но я хотел бы сделать еще одно замечание. Что касается того, какие тесты писать, вы действительно хотите убедиться, что вы также покрываете крайние случаи. Что должно произойти для
Vector3 a(INT_MAX, INT_MAX, INT_MAX);
Vector3 b(INT_MAX, INT_MAX, INT_MAX);
Vector3 result = a + b;
// What is expected? Simple overflow? Exception? Default to invalid value?
Если бы вы делали деление, вы бы обязательно покрыли регистр деления на ноль. Постарайтесь иметь в виду такие крайние случаи.
Было бы совершенно бессмысленно использовать в тесте тот же расчет, что и в коде. Если вы собираетесь быть особенно осторожными, почему бы не проявить особую осторожность при написании кода? Использование примеров, рассчитанных вручную, - лучший способ сделать это, но еще лучше было бы написать тест до того, как вы напишете код, так вы не сможете лениться и написать тест, который, как вы знаете, пройдут, и избежать крайних случаев, которые вы не совсем уверен.
Это всегда зависит от варианта использования. Я бы всегда выбирал ту версию, которая делает проверяемую идею более очевидной. По этой причине я бы также не использовал метод isNear. Я бы проверил
expected.x == 7;
expected.y == 9;
expected.z == 11;
. Используя хорошую библиотеку xUnit, вы получите чистое сообщение об ошибке, какой компонент из ожидаемых был неправильным. В вашем примере вам придется искать реальный источник ошибки.
Способ 1 был бы лучшим вариантом. Главное, как вы выбрали магические данные, на которых будет тестироваться код.
Другой путь может заключаться в следующем. Иногда вместо жесткого кодирования значений в модульном тесте мы можем иметь набор входных данных (магические данные) и набор ожидаемых результатов, соответствующих входным данным. Таким образом, модульный тест будет считывать значение из входного набора, выполнять код и проверять ожидаемый результат.
Мой подход к этому довольно прост: никогда не копируйте производственный код, чтобы получить результат в тесте. Если ваш алгоритм ошибочен, то ваш модульный тест воспроизводит как ошибку , так и прохождение . Подумайте об этом на секунду! Ошибка в коде и прохождение теста с ошибками. Я не думаю, что может быть хуже. Представьте, что вы нашли ошибку в своем коде и изменили ее; теперь тест не пройден, но выглядит правильным. IMO не только делает это для подверженного ошибкам тестирования, но и заставляет задуматься о результате с точки зрения алгоритма. Для таких вещей, как математика, вам не нужно заботиться о том, каков алгоритм, просто то, что ответ правильный. Я бы даже сказал, что принципиально не доверяю тестам, которые подражают логике производственного кода.
Тесты должны быть как можно более декларативными, а это означает жесткое кодирование полностью рассчитанного результата. Для тестов по математике я обычно вычисляю результат на бумаге / калькуляторе, используя значения, которые максимально просты, но не проще. Например. если бы я хотел протестировать метод нормализации, я бы выбрал несколько хорошо известных значений. Большинство людей знают, что sin / cos 45 больше корня два, поэтому нормализация (1, -1, 0) даст легко узнаваемое значение. Есть множество других хорошо известных чисел / приемов, которые вы можете использовать. Вы также можете закодировать свои результаты, используя константы с хорошо названными именами, чтобы облегчить чтение.
Я также рекомендую использовать тестирование на основе данных для математических типов, так как вы можете быстро добавлять новые тестовые примеры.
При векторном сложении не имеет значения, какой метод вы выберете, потому что это довольно простая операция. Лучшей иллюстрацией может быть тестирование, скажем, метода нормализации:
Vector3 a(7, 9, 11);
Vector3 result = a.normalize();
Vector3 hand_solved(0.4418, 0.5680, 0.6943);
Vector3 reproduced(7/sqrt(7*7+9*9+11*11), 9/sqrt(7*7+9*9+11*11),
11/sqrt(7*7+9*9+11*11));
Видите? Читателю не ясно, что ни один из этих методов не является правильным. Воспроизведенный расчет можно проверить, но он грязный и трудночитаемый. Также непрактично переписывать каждый расчет в модульном тесте. Расчет, решенный вручную, не дает читателю никаких гарантий, что он верен (читателю придется решать вручную и сравнивать ответы).
Решение заключается в выборе более простых входных данных. С векторами можно проверить все операции только на базисных векторах (i, j, k). Поэтому в данном конкретном случае яснее было бы сказать что-то вроде:
Vector3 i(1, 0, 0);
Vector3 result = i.normalize();
Vector3 expected(1, 0, 0);
Здесь ясно, что вы тестируете и какой результат ожидаете получить. Если читатель знает, что должна делать normalize
, то ему будет ясно, что ответ правильный.
Вы все равно должны выполнить номер 1, чтобы убедиться, что ваш код правильный - модульный тест должен был быть выполнен гипотетически как часть создания расчета. Используя эти знания, вы можете создать свой модульный тест, чтобы использовать уже созданный код (т. Е. Не дублировать его).
Модульный тест должен проверять известные случаи успеха, известные случаи сбоя, граничные случаи (верхний / нижний диапазоны, если применимо) и любые редкие случаи (редкие и дорогостоящие для отладки во время выполнения, но очень недорогие для тестирования во время сборки, если вы знайте, что они собой представляют :)
Вы обнаружите, что прямые вычисления легче всего поддаются модульному тестированию, поскольку поток логики (надеюсь) самодостаточен.
Дублирование этой логики мало чем поможет. Вы это понимаете, читая ваши комментарии к #2 :). Если только это не что-то невероятно сложное, я бы использовал метод #1.
Возможно, придется немного потрудиться, чтобы определить некоторые тестовые данные; но обычно это довольно легко определить.
Простые правила, которым нужно следовать:
Не тестируйте более одной вещи в
в вашем модульном тесте. Например: Если я написал метод: public int Sum(int number1, int number2)
, то у меня будет 4-5 юнит-тестов, которые будут выглядеть следующим образом
Test_Sum_Number1IsOneNumer2IsTwo_ReturnsThree
Test_Sum_Number1IsZeroNumer2IsZero_Returns0
Test_Sum_Number1IsNegativeOneNumer2IsNegativeThree_ReturnsNegativeFour .... и так далее
Или, вместо того, чтобы писать четыре разных метода, вы можете использовать атрибут RowTest
в MBUnit или TestCase
в NUnit (2.5.5 и далее) для параметризованных тестов - здесь вы просто пишете один метод и передаете различные параметры, указывая их в качестве атрибутов.