Является ли использование юнит-тестов хорошей историей?

13

Итак, у меня есть модуль аутентификации, который я написал некоторое время назад. Теперь я вижу ошибки моего пути и пишу для него юнит-тесты. При написании юнит-тестов мне трудно придумывать хорошие имена и хорошие области для тестирования. Например, у меня есть такие вещи, как

  • RequiresLogin_should_redirect_when_not_logged_in
  • RequiresLogin_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, а не в качестве «модульных» тестов? Это всего лишь личный проект, поэтому я открыт для экспериментов, которые могут или не могут пойти хорошо.

Earlz
источник
4
Линии между блоком, интеграцией и функциональными тестами размыто, если бы я должен выбрать имя для тестовой заглушки, было бы функционально.
Яннис
Я думаю, что это вопрос вкуса. Лично я использую название того, что я тестирую с _testдобавлением, и использую комментарии, чтобы отметить, каких результатов я ожидаю. Если это личный проект, найдите какой-то стиль, который вам удобнее, и придерживайтесь его.
Мистер Листер
1
Я написал ответ с деталями о более традиционном способе написания модульных тестов с использованием шаблона Arrange / Act / Assert, но у друга был большой успех с использованием github.com/cucumber/cucumber/wiki/Gherkin , который используется для спецификации, и afaik может генерировать тесты на огурец.
StuperUser
Хотя я бы не использовал метод, который вы показали с помощью nunit или аналогичным, nspec поддерживает создание контекста и тестирование в более исторически
Майк
1
измените «Билл» на «Пользователь», и все готово
Стивен А. Лоу

Ответы:

15

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

Но нет, определенно не стоит писать ваши тесты таким образом, чтобы порядок выполнения тестов имел значение. Например, NUnit позволяет пользователю интерактивно выбирать, какой тест он хочет выполнить, так что это больше не будет работать так, как задумано.

Этого можно легко избежать, отделяя основную часть тестирования каждого теста (включая «утверждение») от частей, которые устанавливают вашу систему в правильное начальное состояние. Используя приведенный выше пример: напишите методы создания учетной записи, входа в систему и оставьте комментарий - без каких-либо утверждений. Затем используйте эти методы в разных тестах. Вам также необходимо добавить некоторый код в [Setup]метод ваших тестовых приборов, чтобы убедиться, что система находится в правильно определенном начальном состоянии (например, пока нет учетных записей в базе данных, нет подключенных пользователей и т. Д.).

РЕДАКТИРОВАТЬ: Конечно, это, кажется, противоречит "истории" характера ваших тестов, но если вы дадите своим вспомогательным методам осмысленные имена, вы найдете свои истории в каждом тесте.

Итак, это должно выглядеть так:

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

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

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

StuperUser
источник
1
Хотя я думаю, что это хороший пост, я не согласен с тем, что говорится в связанной статье о «гибридном» тесте. ИМХО очень полезно иметь «небольшие» интеграционные тесты (кроме того, конечно , не в качестве альтернативы чистым модульным тестам), даже если они не могут точно сказать, какой метод содержит неправильный код. Если эти тесты обслуживаемы, зависит от того, насколько чистый код написан для этих тестов, они не являются «грязными» как таковые. И я думаю, что цель этих тестов может быть очень ясной (как в примере с ОП).
Док Браун
3

То, что вы описываете, для меня больше похоже на Behavior Driven Design (BDD), чем на модульное тестирование. Взгляните на SpecFlow, который является технологией .NET BDD, основанной на DSL Gherkin .

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

Что касается соглашений для модульных тестов, ответ @ DocBrown кажется убедительным.

Дрю Марш
источник
Для информации, BDD - это то же самое, что и TDD, просто меняется стиль письма. Пример: TDD = assert(value === expected)BDD = value.should.equals(expected)+ вы описываете функции в слоях, которые решают проблему «независимости модульного теста». Это отличный стиль!
Offirmo