Подтвердить что ты не робот

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

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

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

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

Есть два способа, которые я действительно могу сделать, чтобы сделать unit test в классе Vector:

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

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) Снова перепишите код вычисления внутри unit test.

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

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

4b9b3361

Ответ 1

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

Ответ 2

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

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

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

Ответ 3

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

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

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

Ответ 4

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

Предположим, вы не перегружали оператор +, но вместо этого имели ужасно названную функцию f, которая занимала два Vector3 s. Вы также не задокументировали это, поэтому я посмотрел ваши тесты, чтобы увидеть, что должен был делать f.

Если я вижу Vector3 expected( 7, 9, 11 ), мне нужно вернуться назад и спроектировать, как 7, 9 и 11 были "ожидаемыми" результатами. Но если я вижу Vector3 expected( 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?

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

Ответ 5

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

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

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

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

См? Это не ясно читателю, что либо правильно. Воспроизводимый расчет можно проверить, но он грязный и трудно читаемый. Также нецелесообразно переписывать каждое вычисление в unit test. Выбранный вручную расчет не дает никаких заверений читателю в том, что он правильный (читателю нужно было решить вручную и сравнить ответы).

Решение состоит в том, чтобы выбирать более простые входы. С помощью векторов вы можете тестировать все операции только на основе векторов (i, j, k). Поэтому в этом конкретном случае было бы более ясно сказать что-то вроде:

Vector3 i(1, 0, 0);
Vector3 result = i.normalize();
Vector3 expected(1, 0, 0);

Здесь он очищает то, что вы тестируете, и то, что вы ожидаете от результата. Если читатель знает, что должен делать normalize, тогда будет ясно, что ответ правильный.

Ответ 9

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

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

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

Ответ 10

Простые правила:

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