Чи слід чітко кодувати очікувані результати тесту?


29

Якщо очікувані результати одиничного тесту повинні бути жорстко кодованими, чи вони можуть залежати від ініціалізованих змінних? Чи збільшують жорсткі або обчислені результати ризик введення помилок в одиничному тесті? Чи є інші фактори, які я не враховував?

Наприклад, який із цих двох є більш надійним форматом?

[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.");
}

EDIT 1: У відповідь на відповідь DXM, чи є варіант 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.");
}

2
Зробіть і те, і інше. Серйозно. Тести можуть і повинні перекриватися. Також вивчіть якісь тести, керовані даними, якщо ви виявите, що маєте справу із твердо кодованими значеннями.
робота

Я погодився б, третім варіантом є те, що я люблю використовувати. Я не думаю, що варіант 1 не зашкодить, оскільки ви усунете маніпуляцію під час компіляції.
kwelch

Обидва варіанти використовують жорстке кодування, але вони порушаться, якщо тест не запускається на C: \\
Qwertie

Відповіді:


27

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

Сказавши це, у вашому конкретному прикладі я б не довіряв методу "Softcoded", оскільки він використовує вашу SUT (тестувану систему) в якості вхідних даних для ваших розрахунків. Якщо в MyClass є помилка, де поля не зберігаються належним чином, ваш тест насправді пройде, тому що для розрахунку очікуваного значення буде використана неправильна рядок, як target.GetPath ().

Моя пропозиція полягала б у тому, щоб розрахувати очікуване значення там, де це має сенс, але переконайтесь, що обчислення не залежить від будь-якого коду від самого SUT.

У відповідь на оновлення ОП на мою відповідь:

Так, виходячи з моїх знань, але дещо обмеженого досвіду роботи з TDD, я вибрав би варіант №3.


1
Влучне зауваження! У тесті не покладайтеся на неперевірений об'єкт.
Hand-E-Food

це не дублювання коду SUT?
Абікс

1
певним чином це є, але саме так ви перевіряєте, що SUT працює. Якби ми використовували той самий код, і він зірвався, ви ніколи не дізнаєтесь. Звичайно, якщо для того, щоб виконати обчислення, вам потрібно дублювати багато SUT, то, можливо, варіант №1 став би кращим, просто жорсткий код значення.
DXM

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 покладається на проходження інших моїх тестових випадків (таких як ConstructorShouldCorrectInitialiseFields). Опис, який ви описуєте, буде перехресним посиланням на інші тести блоку.
Hand-E-Food

@ Hand-E-Food, здається, ви пишете тести на окремі методи своїх об'єктів. Не варто. Ви повинні писати тести, які перевіряють правильність всього вашого об'єкта разом, а не окремими методами. Інакше ваші тести будуть крихкими щодо змін усередині об'єкта.
Вінстон Еверт

Я не впевнений, що я слідую за цим. Приклад, який я наводив, був суто гіпотетичним, легко зрозумілим сценарієм. Я пишу одиничні тести для тестування публічних членів класів та об’єктів. Це правильний спосіб їх використання?
Hand-E-Food

@ Hand-E-Food, якщо я вас правильно зрозумів, ваш тестовий ConstructShouldCorrectInitialiseFields закликає конструктор і потім стверджує, що поля встановлені правильно. Але ти не повинен цього робити. Вам не байдуже, що роблять внутрішні поля. Вам слід лише стверджувати, що зовнішня поведінка об'єкта правильна. Інакше може настати день, коли вам потрібно буде замінити внутрішню реалізацію. Якщо ви зробили твердження про внутрішній стан, всі ваші тести будуть зламані. Але якщо ви зробили лише твердження про зовнішню поведінку, все одно буде працювати.
Вінстон Еверт

@ Вінстон - я фактично перебуваю в оранці через книгу xUnit Test Patterns і до цього закінчив The Art of Unit Testing. Я не збираюся робити вигляд, що знаю, про що я говорю, але мені б хотілося подумати, що я взяв щось із цих книг. В обох книгах настійно рекомендується кожен тестовий метод перевірити абсолютний мінімум, і ви повинні мати багато тестових випадків, щоб перевірити весь ваш об'єкт. Таким чином, коли змінюються інтерфейси чи функціональні можливості, слід очікувати виправлення лише декількох методів тестування, а не більшості з них. А оскільки вони невеликі, зміни повинні бути легшими.
DXM

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. Hardcoding
ви повинні жорсткий код ? Знову відповідь - ні . тому що, як і будь-яке програмне забезпечення - жорстке кодування інформації, що стає важливою, стає важкою, коли все розвивається. Наприклад, коли ви хочете, щоб вищезазначений шлях був знову змінений, вам потрібно або написати додатковий блок, або продовжити модифікацію. Кращим методом є збереження дати введення та оцінки, отриманої з окремої конфігурації, яку можна легко адаптувати.

Наприклад, ось, як я мав би право тест-заглушки.

[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. Ви можете додати тести на різні входи та очікувані результати з одним рядком коду. Це спонукає вас писати більше тестів, зберігаючи тестовий код DRY та простий у обслуговуванні. Я вважаю, що оскільки так дешево додавати тести, мій розум відкритий для нових тестових випадків, я б не подумав, якби мені довелося написати для них зовсім новий метод. Наприклад, якої поведінки я б очікував, якби один із входів мав крапку в ньому? Зворотний нахил? Що робити, якщо один був порожнім? Або пробіл? Або почалося чи закінчилося пробілом?
  3. Рамка тестування розглядає кожний TestCase як власний тест, навіть додаючи надані входи та виходи у назву тесту. Якщо всі TestCases проходять, але один, дуже легко зрозуміти, який з них зламався та чим він відрізнявся від усіх інших.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.