Один тест – одна проверка, а у вас сколько?

 

Камрады Автоматизаторы,

 

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

Вот один из многочисленных примеров на эту тему:

"One assert per test" http://maxheapsize.com/2011/06/14/one-assert-per-test-really/

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

 

А, сколько в ваших системных тестах (тех что на Selenium, WatiN, SilkTest, QTP и т.д. ), в среднем, проверок? (Assert’ов или чего другого)?

 

Тоже не раз дискутировал на эту тему. 

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

В целом интересная тема, но главное чтобы сам тест делился на 3 части:

- setUp

- test

- tearDown

А где ставить проверки это должен подсказывать здравый смысл ну или тестировщик, который написал тестовый сценарий.

Например:

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }public void setUp() { projectPage = loginPage.openAndLogin(); assertThat(projectPage.isOpened(), is(true)); } public void testAddProject() { projectPage.addProject(project.getName()); assertThat(projectPage.isProjectPresent(project.getName(), is(true)); }

public void tearDown() {
projectPage.loggedOut();
}{/syntaxhighlighter}{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }{/syntaxhighlighter}

Юнит тесты отличаются от функциональных, потому при автоматизации функциональных тестов можно и нужно переиспользовать некоторые паттерны, но не все подряд. Тут все зависит от того, как построен ручной тест, ведь автоматизация не происходит сама по себе. Есть ручной тест, он выполняется, потом нужно его выполнять быстро, потому создается автоматический тест. Если в ручном тесте 100 шагов и 20 проверок, то автоматичский тест будет зеркальным отображением ручного теста.

Например, 

{syntaxhighlighter brush: java;fontsize: 100; first-line: 1; } @Test public void viewAffiliate_existedAffiliate_detailsAccessible(){ //combined with success affiliate creation test case AffiliatesListPage affiliate = createAffiliateWithRandomData() assertThat("affiliate is created", affiliate.returnCreationMessage(), endsWith("created")) ViewAffiliatePage view = affiliate.openAndViewAffiliateById(affiliate.returnIdOfCreatedAffiliate()) assertThat("affiliate name is in and displayed", view.getPropertyValueByName("Name"), containsString("name")) assertThat("addiliate description is in and displayed", view.getPropertyValueByName("Description"), containsString("description")) }{/syntaxhighlighter}

Ручные тесты же тоже состоят из pre-condition, test steps и post condition. 

Может имеет смысл предыдущий пример разделить на 3 функции? Такой же пример - подход BDD.

Опишу свой взгляд на данную проблему как разработчика:

1. Модульные тесты действительно сильно отличаются от функциональных. Один assert в них рекомендуется для лучшей понимаемости и быстрого анализа результатов выполнения. А выполняются они ооочень часто. Если у вас физически несколько assert в одном тесте, то рекомендуется объединить их в один логический assert (это метод в большей части случаев, иногда более сложная конструкция). Этот подход заставляет вас писать тесты, которые тестируют ровно один аспект функциональности.

2. В функциональных тестах правила не так жестки, но я предпочитаю их придерживаться и в этом случае. Это упрощает понимание тестов и анализ их результатов.

3. Многие ошибочно (на мой взгляд) применяют assert для проверки промежуточных состояний (как в выше приведенных примерах). Это добавляет путаницы, потому что вы тестировали не эту проверку, а ту, которая расположена в конце теста. Вместо assert мне больше нравится применять проверки с пробрасыванием exception (благо в Java есть классы Validate и другие утилиты). Иногда проверку можно не делать вовсе, потому что следующая операция и так провалится. А exception укажет вам на номер строки, где это произошло.

4. Ну и в разрезе BDD - assert допустим только в секции //Then (из //Given, //When, //Then). :)

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

Где-то я согласен с Колей, по поводу assert. Но на некоторых проектах было требование, чтобы ошибки автотестов были человека читаемые.

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

Или может есть другие варианты решения проблемы?

Николай,

А что такое логический Assert? Это какой-то Assert.True или какой-нибудь хитрый и сложный метод/функция?

Можно пример кода?

Использую ваш же пример - насколько я понимаю, то в данном случае сообщение в случае ошибки трудно назвать человекочитаемым

public void setUp() {
    projectPage = loginPage.openAndLogin();
    assertThat(projectPage.isOpened(), is(true));
}


Как вариант можно сделать несколько специфических для тестируемого приложения assert-методов для проверки основных состояний (открыта нужная страница, есть соответствующий текст на странице) и использовать их для повышения читаемости.

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

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

Вообще для юнит-тестов правило "один тест - один assert" выглядит разумно, для случая функциональных тестов должно быть столько проверок, сколько требует тест-кейс и здравый смысл (не всегда эти два понятия совпадают :))

Вот вам пример. Физических проверок две, но логически только одна.

{syntaxhighlighter brush: java;fontsize: 100; first-line: 1; } @Test public void regularUserAutomaticallyLoggedInAfterRegistration() { RegistrationPage registrationPage = new LoginPage(driver).open().startRegistration();

    MainPage mainPage = registrationPage.registerUser("pavel", "123");
    
    assertUserLoggedIn(mainPage);
}

private void assertUserLoggedIn(MainPage mainPage) {
    assertTrue(mainPage.hasText("Welcome, pavel"));
    assertTrue(mainPage.isLinkPresent("Logout"));
}{/syntaxhighlighter}</p>

Тот пример не освещал написанный комментарий. 

Вот так будет выглядеть сообщение об ошибке

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }public void setUp() { projectPage = loginPage.openAndLogin(); assertThat(projectPage.isOpened(), is(true), "Project page was not opened"); }{/syntaxhighlighter}

 

И этот текст выведется в заголовке ошибки. Не просто "Exepected True but was False". А еще и это кастомное сообщение

Неудачный пример. В идеале страница должна бросать исключение с текстом ошибки если у нее не получилось открыться. Тогда будет единообразие - ведь если элемент не найден, то будет исключение, а не assert error. :) И дубликатов не будет по всем тестам. :)

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

Хотя, я люблю писать по меньше кода, и поэтому у меня в каждой страницы есть специальный метод Invoke(), который обязан запустить страницу.

Вот пример обычного теста:

 

{syntaxhighlighter brush: csharp;fontsize: 100; first-line: 1; }[TestMethod] public void AdminPageHasLinkToDefaultMessageScreen() { var adminPage = new AdministratorMenuPage(); adminPage.Invoke();

Assert.IsTrue(adminPage.lnkDefaultMessage.Exists);

}{/syntaxhighlighter}

И из Specflow/BDD Step Definition:

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } private AdministratorMenuPage adminPage = new AdministratorMenuPage();

    [When(@"I am on the \{Administrator} page")]
    [Given(@"I am on the \{Administrator} page")]
    [Then(@"after that I go back to the \{Administrator} page")]
    public void WhenIAmOnTheAdministratorPage()
    {
        adminPage.Invoke();
    }

{/syntaxhighlighter}

Плюс такого подхода в том, что я не использую лишние setup-методы и объем кода становится меньше. 
Минусы, это то, что внутри используется очень много магии :slight_smile:

Мисье, у вас же метод When - "WhenIAmOnTheAdministratorPage", по логике, точно такой же как setUp. Просто что обвернуто в BDD шаблон :)

Мсье, я так понимаю, что у вас в тест-классе не один единственный тест, и метод setUp автоматически дергается фреймворком (TestNG/JUnit) в начале каждого теста?
Если так, то в моем случае, метод WhenIAmOnTheAdministratorPage() вызывается в некоторых сценариях по нескольку раз.
В первом сценарии у меня это When – ну да, тот же setUp потому что выполняется вначале тесткейса.
Во втором случае, это Given – тот же сетап, но и одновременно And (в конце второго сценария) – это уже будет tearDown.

В третьем сценарии, который я не хочу приводить, ибо он очень специфичен к продукту, Given у меня отличается, и Админка запускается лишь в конце тесткейса для проверки на то, как мои действия вначале теста повлияли на админку.

 

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }Scenario: The Administrator page should have valid set of links and captions When I am on the {Administrator} page Then I can see the following set of references: | Web link description | Expected text | | CENSORED | CENSORED | | CENSORED | CENSORED | | CENSORED | CENSORED | | CENSORED | CENSORED | And I can see the {Administrator} page title is "Administrator Menu"

Scenario Outline: Open each page from the Admin Menu
Given I am on the {Administrator} page
When I click the {<Web link>}
Then I can see the page {<Web page class>} with title {<Web page title>}
And after that I go back to the {Administrator} page

Examples:
| Web link | Web page class | Web page title |
| CENSORED | CENSORED | CENSORED |
| CENSORED | CENSORED | CENSORED |
| CENSORED | CENSORED | CENSORED |
| CENSORED | CENSORED | CENSORED |
{/syntaxhighlighter}

 И да, Мсье, хочу подтвердить Вашу возможную мысль, Cucumber/Specflow/Duke4Nuke и прочие это еще то извращение :slight_smile:

И оффтопик (холивар):
Основная фишка Specflow в том, что этот фреймворк умеет компилировать BDD Features/Scenarios в тесты для MS Test, NUnit, xUnit.
Я могу запускать BDD-тесты и обычные юнит-тесты из одного проекта. Есть ли нечто подобное в мире JUnit/TestNG?

 

Все верно, только в testNG есть еще такие аннотации как @BeforeMethod @BeforeClass @BeforeTest @BeforeSuite и можно самому настроить как запускать свои методы SetUp. Что ничем не отличается от Given

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

Вот фреймворк http://code.google.com/p/hamcrest/wiki/Tutorial, который решает похожую задачу.

Есть ли нечто подобное в мире JUnit/TestNG?

JBehave. Это своего рода аналог SpecFlow для Java. Тесты можно запускать как обычные JUnit тесты.

 

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

 

1.       Можно мапить несколько фраз на один тест. Это удобней, и не надо писать что-то типа

Given /.*?[^\d].*?+/  как это иногда делают в кукумбере.

2.       В Specflow, я точно знаю, можно ограничить один шаг (Given/When/Then) для конкретной фичи, т.е. не делать его глобальным

 

В Cucumber можно переиспользовать сами Given\When\Then, то есть например внутри какого-то, то есть что-то типа такого:

 

{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }When "I open reports tab" do

Given "I'm on landing page"

And 'Click on the "Reports" link'

end{/syntaxhighlighter}

 

В SpecFlow я такого не видел. Там я еще наталкивался на ряд ограничений, которые меня несколько расстраивали, сейчас не вспомню. Но SpecFlow - это далеко не единственная подобная штуковина для C#. Есть еще NBehave, у которого для студии есть прикольный плагин, который позволяет делать автодополнение. То есть вы пишете какую-то фразу в feature-файле, а он вам варианты подбрасывает.