Следует ли жестко кодировать ожидаемые результаты модульного теста?

30

Должны ли ожидаемые результаты модульного теста быть жестко закодированы или они могут зависеть от инициализированных переменных? Увеличивают ли жестко закодированные или рассчитанные результаты риск внесения ошибок в модульный тест? Есть ли другие факторы, которые я не учел?

Например, какой из этих двух форматов является более надежным?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

РЕДАКТИРОВАТЬ 1: В ответ на ответ ДХМ, является ли вариант 3 предпочтительным решением?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Hand-E-Food
источник
2
Делай оба. Шутки в сторону. Тесты могут и должны совпадать. Также посмотрите на какие-то тесты на основе данных, если вы столкнетесь с жестко закодированными значениями.
Работа
Я бы согласился, что третий вариант - это то, что мне нравится использовать. Я не думаю, что вариант 1 повредит, поскольку вы устраняете манипуляции при компиляции.
Квелч
Оба ваших варианта используют
жесткое кодирование и прервутся,

Ответы:

27

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

Сказав это, в вашем конкретном примере я бы НЕ доверял методу "мягкого кода", потому что он использует вашу SUT (тестируемую систему) в качестве входных данных для ваших расчетов. Если в MyClass есть ошибка, из-за которой поля не сохраняются должным образом, ваш тест на самом деле пройдет, потому что при расчете ожидаемого значения будет использоваться неправильная строка, как target.GetPath ().

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

В ответ на обновление ОП на мой ответ:

Да, исходя из моих знаний, но немного ограниченного опыта в выполнении TDD, я бы выбрал вариант № 3.

DXM
источник
1
Хорошая точка зрения! Не полагайтесь на непроверенный объект в тесте.
Hand-E-Food
Разве это не дублирование кода SUT?
Abyx
1
в некотором смысле это так, но именно так вы проверяете, работает ли SUT. Если бы мы использовали один и тот же код, и он обанкротился, вы бы никогда не узнали. Конечно, если для выполнения расчета вам нужно дублировать много SUT, то, возможно, вариант № 1 станет лучше, просто жестко закодируйте значение.
ДХМ
16

Что делать, если код был следующим:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Ваш второй пример не поймает ошибку, но первый пример будет.

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

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

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

Но есть и исключения. Что если ваш код должен работать в Windows и Linux? Мало того, что путь должен быть другим, он должен использовать разные разделители пути! Вычисление пути с использованием функций, которые абстрагируют разницу, может иметь смысл в этом контексте.

Уинстон Эверт
источник
Я слышу, что вы говорите, и это дает мне возможность подумать. Softcoding полагается на прохождение других моих тестов (таких как ConstructorShouldCorrectlyInitialiseFields). Ошибка, которую вы описываете, будет иметь перекрестные ссылки в случае неудачи других модульных тестов.
Hand-E-Food
@ Hand-E-Food, похоже, вы пишете тесты для отдельных методов ваших объектов. Не. Вы должны написать тесты, которые проверяют правильность всего вашего объекта, а не отдельных методов. В противном случае ваши тесты будут хрупкими в отношении изменений внутри объекта.
Уинстон Эверт
Я не уверен, что следую. Пример, который я привел, был чисто гипотетическим, простым для понимания сценарием. Я пишу модульные тесты для проверки открытых членов классов и объектов. Это правильный способ их использования?
Hand-E-Food
@ Hand-E-Food, если я правильно вас понял, ваш тест ConstructShouldCorrectlyInitialiseFields вызовет конструктор, а затем подтвердит, что поля установлены правильно. Но ты не должен этого делать. Вас не должно волновать, что делают внутренние поля. Вы должны только утверждать, что внешнее поведение объекта является правильным. В противном случае может наступить день, когда вам нужно заменить внутреннюю реализацию. Если вы сделали утверждения о внутреннем состоянии, все ваши тесты будут сломаны. Но если вы только сделали утверждения о внешнем поведении, все будет работать.
Уинстон Эверт
@ Уинстон - я нахожусь в процессе прохождения книги xUnit Test Patterns, а перед этим закончил «Искусство модульного тестирования». Я не собираюсь притворяться, что знаю, о чем говорю, но мне хотелось бы думать, что я кое-что понял из этих книг. Обе книги настоятельно рекомендуют, чтобы каждый метод тестирования проверял абсолютный минимум, и у вас должно быть много тестов для проверки всего объекта. Таким образом, при изменении интерфейсов или функциональности следует ожидать исправления только нескольких методов тестирования, а не большинства из них. А поскольку они маленькие, изменения должны быть проще.
ДХМ
4

На мой взгляд, оба ваших предложения не идеальны. Идеальный способ сделать это это:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Другими словами, тест должен работать исключительно на основе входных и выходных данных объекта, а не на основе внутреннего состояния объекта. Объект должен рассматриваться как черный ящик. (Я игнорирую другие проблемы, такие как неуместность использования string.Join вместо Path.Combine, потому что это всего лишь пример.)

Майк Накис
источник
1
Не все методы являются функциональными - многие правильно имеют побочные эффекты, которые изменяют состояние какого-либо объекта или объектов. Модульный тест для метода с побочными эффектами, вероятно, должен был бы оценить состояние объекта (ов), затронутых методом.
Мэтью Флинн
Тогда это состояние будет рассматриваться как результат метода. Целью этого примера теста является проверка метода GetPath (), а не конструктор MyClass. Прочитайте ответ @ DXM, он дает очень хорошую причину для подхода черного ящика.
Майк Накис
@ MatthewFlynn, тогда вы должны проверить методы, затронутые этим состоянием. Точное внутреннее состояние - это деталь реализации, а не бизнес теста.
Уинстон Эверт
@MatthewFlynn, просто чтобы уточнить, это связано с показанным примером, или что-то еще, чтобы рассмотреть для других модульных тестов? Я мог видеть, что это имеет значение для чего-то вроде target.Dispose(); Assert.IsTrue(target.IsDisposed);(очень простой пример.)
Hand-E-Food
Даже в этом случае свойство IsDisposed является (или должно быть) неотъемлемой частью открытого интерфейса класса, а не деталями реализации. (Интерфейс IDispose не предоставляет такого свойства, но это прискорбно.)
Майк Накис,
2

В обсуждении есть два аспекта:

1. Использование самой цели в тестовом примере
. Первый вопрос: следует ли / можно ли использовать сам класс, чтобы полагаться и получать часть работы, выполненной в тестовой заглушке? - Ответ НЕТ, поскольку, как правило, вы никогда не должны делать предположения о коде, который вы тестируете. Если это не сделано должным образом, со временем ошибки становятся невосприимчивыми к некоторым юнит-тестам

2. Жесткое кодирование -
это жесткий код ? Снова ответ - нет . потому что, как и любое программное обеспечение, жесткое кодирование информации становится сложным, когда вещи развиваются. Например, если вы хотите, чтобы вышеуказанный путь был изменен снова, вам нужно либо написать дополнительный модуль, либо продолжить модификацию. Лучшим способом является сохранение входных данных и даты оценки на основе отдельной конфигурации, которую можно легко адаптировать.

например, вот как я бы исправил тестовую заглушку.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Дипан Мехта
источник
0

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

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

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

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

Люк Франкен
источник
0

Современные тестовые среды позволяют вам предоставлять параметры для вашего метода. Я бы использовал эти:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

На мой взгляд, есть несколько преимуществ:

  1. Разработчики часто испытывают искушение скопировать кажущиеся простыми части кода из своего SUT в свои модульные тесты. Как указывает Уинстон , в них все еще могут быть скрыты хитрые ошибки. «Жесткое кодирование» ожидаемого результата помогает избежать ситуаций, когда ваш тестовый код неверен по той же причине, по которой исходный код неверен. Но если изменение требований заставит вас отслеживать жестко запрограммированные строки, встроенные в десятки методов тестирования, это может раздражать. Хранение всех жестко закодированных значений в одном месте, вне вашей логики тестирования, дает вам лучшее из обоих миров.
  2. Вы можете добавить тесты для разных входов и ожидаемых выходов с помощью одной строки кода. Это побуждает вас писать больше тестов, сохраняя при этом свой тестовый код СУХИМЫМ и простым в обслуживании. Я считаю, что, поскольку добавлять тесты так дешево, мой разум открыт для новых тестовых случаев, о которых я бы даже не подумал, если бы мне пришлось написать для них совершенно новый метод. Например, какое поведение я бы ожидал, если бы на одном из входов была точка? Обратная косая черта? Что делать, если кто-то был пуст? Или пробел? Или начался или закончился пробелом?
  3. Среда тестирования будет рассматривать каждый TestCase как свой собственный тест, даже помещая предоставленные входы и выходы в название теста. Если все тестовые случаи проходят, кроме одного, очень легко увидеть, какой из них сломался и чем он отличался от всех остальных.
StriplingWarrior
источник