Хороший способ записать модульные тесты

Так, я ранее не был действительно в практике записи модульных тестов - теперь я отчасти, и я должен проверить, на правильном пути ли я.

Скажите, что у Вас есть класс, который имеет дело с математическими вычислениями.

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 способа, которыми я могу действительно думать, чтобы сделать модульный тест на Векторном классе:

1) Рука - решает некоторые проблемы, затем твердый код числа в модульный тест и передачу только если равный Вашему ручному и трудно кодированному результату

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

2) Перепишите код вычисления очень тщательно в модульном тесте.

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

Или есть ли другой способ сделать что-то вроде этого?

18
задан Donal Fellows 13 June 2010 в 17:24
поделиться

10 ответов

Способ №1 - это общепринятый способ проведения модульного тестирования. Переписывая свой код, вы можете переписать неисправный код в тест. В большинстве случаев для каждого метода, который вы тестируете, требуется только один реальный тест, поэтому это не СЛИШКОМ по времени.

9
ответ дан 30 November 2019 в 09:06
поделиться

Я считаю, что выписывание чисел (ваш второй подход) - правильный вариант. Это делает ваши намерения более очевидными для тех, кто читает тест.

Предположим, вы не перегружали оператор + , а вместо этого использовали функцию с ужасным именем 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?

Если бы вы делали деление, вы бы обязательно покрыли регистр деления на ноль. Постарайтесь иметь в виду такие крайние случаи.

1
ответ дан 30 November 2019 в 09:06
поделиться

Было бы совершенно бессмысленно использовать в тесте тот же расчет, что и в коде. Если вы собираетесь быть особенно осторожными, почему бы не проявить особую осторожность при написании кода? Использование примеров, рассчитанных вручную, - лучший способ сделать это, но еще лучше было бы написать тест до того, как вы напишете код, так вы не сможете лениться и написать тест, который, как вы знаете, пройдут, и избежать крайних случаев, которые вы не совсем уверен.

1
ответ дан 30 November 2019 в 09:06
поделиться

Это всегда зависит от варианта использования. Я бы всегда выбирал ту версию, которая делает проверяемую идею более очевидной. По этой причине я бы также не использовал метод isNear. Я бы проверил

expected.x == 7;
expected.y == 9;
expected.z == 11;

. Используя хорошую библиотеку xUnit, вы получите чистое сообщение об ошибке, какой компонент из ожидаемых был неправильным. В вашем примере вам придется искать реальный источник ошибки.

4
ответ дан 30 November 2019 в 09:06
поделиться

Способ 1 был бы лучшим вариантом. Главное, как вы выбрали магические данные, на которых будет тестироваться код.

Другой путь может заключаться в следующем. Иногда вместо жесткого кодирования значений в модульном тесте мы можем иметь набор входных данных (магические данные) и набор ожидаемых результатов, соответствующих входным данным. Таким образом, модульный тест будет считывать значение из входного набора, выполнять код и проверять ожидаемый результат.

0
ответ дан 30 November 2019 в 09:06
поделиться

Мой подход к этому довольно прост: никогда не копируйте производственный код, чтобы получить результат в тесте. Если ваш алгоритм ошибочен, то ваш модульный тест воспроизводит как ошибку , так и прохождение . Подумайте об этом на секунду! Ошибка в коде и прохождение теста с ошибками. Я не думаю, что может быть хуже. Представьте, что вы нашли ошибку в своем коде и изменили ее; теперь тест не пройден, но выглядит правильным. IMO не только делает это для подверженного ошибкам тестирования, но и заставляет задуматься о результате с точки зрения алгоритма. Для таких вещей, как математика, вам не нужно заботиться о том, каков алгоритм, просто то, что ответ правильный. Я бы даже сказал, что принципиально не доверяю тестам, которые подражают логике производственного кода.

Тесты должны быть как можно более декларативными, а это означает жесткое кодирование полностью рассчитанного результата. Для тестов по математике я обычно вычисляю результат на бумаге / калькуляторе, используя значения, которые максимально просты, но не проще. Например. если бы я хотел протестировать метод нормализации, я бы выбрал несколько хорошо известных значений. Большинство людей знают, что sin / cos 45 больше корня два, поэтому нормализация (1, -1, 0) даст легко узнаваемое значение. Есть множество других хорошо известных чисел / приемов, которые вы можете использовать. Вы также можете закодировать свои результаты, используя константы с хорошо названными именами, чтобы облегчить чтение.

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

2
ответ дан 30 November 2019 в 09:06
поделиться

При векторном сложении не имеет значения, какой метод вы выберете, потому что это довольно простая операция. Лучшей иллюстрацией может быть тестирование, скажем, метода нормализации:

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, то ему будет ясно, что ответ правильный.

0
ответ дан 30 November 2019 в 09:06
поделиться

Вы все равно должны выполнить номер 1, чтобы убедиться, что ваш код правильный - модульный тест должен был быть выполнен гипотетически как часть создания расчета. Используя эти знания, вы можете создать свой модульный тест, чтобы использовать уже созданный код (т. Е. Не дублировать его).

Модульный тест должен проверять известные случаи успеха, известные случаи сбоя, граничные случаи (верхний / нижний диапазоны, если применимо) и любые редкие случаи (редкие и дорогостоящие для отладки во время выполнения, но очень недорогие для тестирования во время сборки, если вы знайте, что они собой представляют :)

Вы обнаружите, что прямые вычисления легче всего поддаются модульному тестированию, поскольку поток логики (надеюсь) самодостаточен.

0
ответ дан 30 November 2019 в 09:06
поделиться

Дублирование этой логики мало чем поможет. Вы это понимаете, читая ваши комментарии к #2 :). Если только это не что-то невероятно сложное, я бы использовал метод #1.

Возможно, придется немного потрудиться, чтобы определить некоторые тестовые данные; но обычно это довольно легко определить.

1
ответ дан 30 November 2019 в 09:06
поделиться

Простые правила, которым нужно следовать:

  1. Всегда используйте Arrange, Act и Assert (AAA шаблон) - погуглите, чтобы найти больше об этом.
  2. Вы должны НИКОГДА не иметь блоки if/else в ваших модульных тестах
  3. вы должны НИКОГДА не иметь любые вычисления/логику в ваших модульных тестах
  4. Не тестируйте более одной вещи в в вашем модульном тесте. Например: Если я написал метод: 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 и далее) для параметризованных тестов - здесь вы просто пишете один метод и передаете различные параметры, указывая их в качестве атрибутов.

0
ответ дан 30 November 2019 в 09:06
поделиться
Другие вопросы по тегам:

Похожие вопросы: