Чи корисне використання одиничних тестів для розповіді історії?


13

Отже, у мене є модуль аутентифікації, про який я писав деякий час тому. Тепер я бачу помилки в моєму шляху і пишу для нього одиничні тести. Під час написання одиничних тестів мені важко придумати хороші імена та хороші області для тестування. Наприклад, у мене є такі речі

  • ПотрібенLogin_should_redirect_when_not_logged_in
  • ПотрібенLogin_should_pass_through_when_logged_in
  • Login_should_work_when_given_proper_credentials

Особисто я вважаю це трохи некрасиво, хоча це здається "належним". У мене також є проблеми з розмежуванням тестів, просто скануючи їх (я повинен прочитати назву методу хоча б двічі, щоб знати, що тільки не вдалося)

Отже, я подумав, що, можливо, замість того, щоб писати тести, які суто перевіряють функціональність, можливо, написати набір тестів, які охоплюють сценарії.

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

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

Це чути шаблон? Я бачив історії прийняття та таке, але це принципово інше. Велика різниця в тому, що я придумую сценарії, щоб "вимусити" тести. Замість того, щоб вручну намагатися придумати можливі взаємодії, які мені потрібно перевірити. Також я знаю, що це заохочує одиничні тести, які не перевіряють точно один метод та клас. Я думаю, це все в порядку. Також я знаю, що це спричинить проблеми принаймні в деяких тестових рамках, оскільки вони зазвичай припускають, що тести не залежать один від одного і порядок не має значення (де це буде в цьому випадку).

У будь-якому випадку, це взагалі доцільна модель? Або це буде ідеально підходить для інтеграційних тестів мого API, а не як "одиничні" тести? Це просто в особистому проекті, тому я відкритий для експериментів, які можуть чи не можуть пройти добре.


4
Рядки між блоком, інтеграцією та функціональними тестами розмиті, якби мені довелося вибрати назву для тестового заглушки, воно було б функціональним.
янніс

Я думаю, що це питання смаку. Особисто я використовую назву того, що я _testтестую, додаючи додані коментарі, і використовую коментарі, щоб відзначити, які результати я очікую. Якщо це особистий проект, знайдіть якийсь стиль, в якому ви відчуваєте себе комфортно, і дотримуйтесь цього.
Містер Лістер

1
Я написав відповідь з деталями щодо більш традиційного способу написання одиничних тестів за схемою Arrange / Act / Assert, але друг мав великий успіх, використовуючи github.com/cucumber/cucumber/wiki/Gherkin , який є використовується для специфікацій, а afaik може генерувати огіркові тести.
StuperUser

Хоча я б не використовував метод, який ви показали з nunit або подібним, nspec має підтримку для створення контексту та тестування в більш природному характері: nspec.org
Майк

1
змініть "Білл" на "Користувач", і ви закінчите
Стівен А. Лоу

Відповіді:


15

Так, це гарна ідея дати своїм тестам назви прикладних сценаріїв, які ви тестуєте. І використовуючи свій інструмент тестування одиниць для більш ніж просто одиничних тестів, можливо, теж добре, багато людей роблять це з успіхом (я теж).

Але ні, напевно, не годиться писати тести так, як має значення порядок виконання тестів. Наприклад, NUnit дозволяє користувачеві інтерактивно вибирати тест, який він / вона хоче виконати, тому це більше не буде працювати так, як передбачалося.

Уникнути цього можна легко, відокремивши основну тестову частину кожного тесту (включаючи "затвердження") від частин, які встановили систему в правильний початковий стан. Використовуючи приклад вище: пишіть способи створення облікового запису, увійдіть у систему та опублікуйте коментар - без жодних тверджень. Потім повторно використовуйте ці методи в різних тестах. Вам також доведеться додати якийсь код до [Setup]методу ваших тестових приладів, щоб переконатися, що система перебуває у належно визначеному початковому стані (наприклад, немає жодних облікових записів у базі даних, поки ніхто не підключений тощо).

EDIT: Звичайно, це, здається, суперечить "сюжетній" природі ваших тестів, але якщо ви надаєте своїм допоміжним методам значущі імена, ви знайдете свої історії в кожному тесті.

Отже, це має виглядати приблизно так:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}

Я б пішов далі і скажу, що добре використовувати інструмент xUnit для запуску функціональних тестів, якщо ви не плутаєте інструментарій з типом тесту, і ви тримаєте ці тести окремо від фактичних одиничних тестів, так що розробники можуть як і раніше швидко виконувати одиничні тести на час фіксації. Це, ймовірно, буде набагато повільніше, ніж одиничні тести.
bdsl

4

Проблема розповіді історії з одиничними тестами полягає в тому, що вона не дає чіткого визначення того, що одиничні тести повинні бути організовані та запускатися повністю незалежно один від одного.

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

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

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

Щоб зробити класи, методи та результати зрозумілими, велике тестування Art of Unit використовує конвенцію про іменування

Тестовий клас:

ClassUnderTestTests

Методи випробувань:

MethodUnderTest_Condition_ExpectedResult

Щоб скопіювати приклад @Doc Brown, а не використовувати [Setup], який працює перед кожним тестом, я пишу допоміжні методи для побудови ізольованих об'єктів для тестування.

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

Тож невдалі тести мають змістовну назву, яка дає вам деяку розповідь про те, який метод не вдався, про стан та очікуваний результат.

Ось так я завжди писав одиничні тести, але друг мав багато успіхів з Геркіним .


1
Хоча я вважаю, що це хороший пост, я не погоджуюся з тим, що йдеться у пов'язаній статті про "гібридний" тест. Проведення "невеликих" тестів на інтеграцію (крім того, не замість чистих одиничних тестів, звичайно), IMHO може бути дуже корисним, навіть якщо вони не можуть точно сказати, який метод містить неправильний код. Якщо ці тести є ретельними, залежить від того, наскільки чистий код написаний для цих тестів, вони самі по собі не є "брудними". І я думаю, мета цих тестів може бути дуже зрозумілою (як у прикладі ОП).
Док Браун

3

Те, що ви описуєте, для мене більше схоже на дизайн, керований поведінкою (BDD), ніж тестування. Погляньте на SpecFlow, який є .NET BDD-технологією, заснованою на DSL Gherkin .

Потужні речі, які будь-яка людина може читати / писати, не знаючи нічого про кодування. Наша тестова команда користується великим успіхом, використовуючи її для наших інтеграційних тестових наборів.

Щодо домовленостей про одиничні тести, відповідь @ DocBrown видається надійною.


Для інформації, BDD точно такий, як TDD, змінюється лише стиль написання. Приклад: TDD = assert(value === expected)BDD = value.should.equals(expected)+ ви описуєте особливості в шарах, які вирішують проблему "незалежність одиничного тесту". Це чудовий стиль!
Offirmo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.