Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

Как правильно выбрать название для юнит-теста?


Это был обычный будний день, я начал писать очередной обычный тест, но при написании названия теста что-то пошло не так:

testCustomerRedirectUrlAfterSelectLastTransactionProcessShouldContainsUrlToConfirmRecurringProfilePageWhenTransactionTypeIsCreateRecurringProfile


Примерно на середине я начал осознавать что происходит что-то не то, но я не остановилс
и дописал название теста до конца. Коллеги не оценили такое длиннющее название теста, но мне оно нравится. Сейчас используется что-то вроде testCorrectChangeCustomerRedirectUrl, а детали того что тест тестирует скрыты внутри теста.

Я не могу понять это лвлап, либо я устал под вечер.

Вопрос: Каковы критерии выбора хорошего названия теста?

UPD: Пожалуйста, приведите названия тестов из ваших проектов.
    


Ответы

Ответ 1



Название для тестов не надо выбирать красивое. Название нужно выбирать функционально и по возможности короткое -- чтобы по минимальному количеству символов было понятно, что и под какими условиями он тестирует, а также чтобы можно было отличить конкретный тест от тысяч других. Распространенные шаблоны наименования такие: TestClass_TestMethod_ConditionAndExpectedResult (для юнит-тестов) TestMethod_Condition_ExpectedResult (для юнит-тестов) ConditionAndExpectedResult (для интеграционных тестов, которые тестируют целые блоки функциональности через некоторую входную точку) При этом если у вас в частях Condition/ExpectedResult слишком много разных условий/результатов объединенных по "И", то вам нужно пересмотреть либо главную цель теста (что он тестирует), либо главное условие, которое отличает тест от других, и разбить его на несколько отдельных тестов. Я ратую прежде всего за то, что название должно быть информативным, но при этом настольк коротким, насколько возможно. Тут точно так же как и со всем остальным кодом -- че меньше кода, тем меньше времени нужно чтобы его понять, тем проще разбираться с ним Судя по вашему названию, это не простой маленький юнит-тест, а что-то побольше, поэтому совсем коротким названием тут не обойтись. В вашем случае, если тест класс у вам уже для Customer, я бы ограничился названием SelectLastTransactionProcess_CreateRecurringProfile_RedirectUrlShouldContainConfirmation. В вашем названии много повторений слов: transaction, url, recurring profile и т.д. Они избыточны. Между длиной и информативностью названия безусловно должен быть баланс. Достаточно чтоб названия были понятны разработчикам внутри проекта, которые уже обладают контексто и способны понять, о чем этот тест. И если для этого нужно длину названия сделать н 30 символов, а 40, сделайте. Но не нужно стремиться к тому, чтоб названия были понятн каждому встречному, попутно рассказывая обо всей системе, поскольку это приводит к таким вот километровым названиям, в которых сложно разобраться. Посмотрите на свой обычный код, посмотрите на названия методов. Вы ведь не описываете в названии метода каждую его деталь, каждую строчку? Вы описываете нечто главное, а остальное спрятано внутри. Точно так же и с тестовыми методами. Кто-нибудь может ещё рассказать что делать с названиями интеграционных тестов > где 100500 различных условий? К примеру биллинг, принимает на вход 5-10 видов прайс-листов и в зависимости от их комбинаций и комбинаций их параметров (тоже примерно 5-10) должен выдавать различные суммы на выход. Контейнер отправляемый в биллинг занимает ~30 строчек. Если в стиле поста называть тесты то это будет testPayoutDebtorToGateOntopGateTransactionAmountGateToCreditorFlatPayinDebtorTo‌​GateOntopTotalTransactionAmountGateToCreditorOntopTransactionAmount, причем не с полным контекстом. Нужно рассчитать итоговую сумму исходя из того что прайс-листы могли быть настроены следующим образом: Тип комиссии прайс-листа типа основной сделки - inside/total, комиссия 0.1% + 0.5руб cверху, расчет вести от оборота. Второй комиссионный прайс-лист ontop/gate.. ещё в таком же стиле раз 10 и пара неявных зависимостей прайс-листов друг от друга. Я бы сказал, что вам нужен параметрический тест. Это такой тест, который одним кодо тестирует разные наборы данных. Например, в терминах тестового фреймворка xUnit параметрический тест метода Calculator.Add() может выглядеть так: [Theory] // это параметрический тест [InlineDate(1, 2, 3)] // наборы данных, т.е. параметры теста [InlineDate(0, 1, 1)] [InlineDate(-1, 1, 0)] public void Calculator_Add_ShouldReturnCorrectResult(int a, int b, int expectedResult) { var result = Calculator.Add(a, b); Assert.AreEqual(result, expectedResult); } С параметрическими тестами вам не нужно перечислять все условия в названиях -- названи содержит только суть теста, что именно тестируется, конкретный кейс. В вашем случа это что-то вроде РасчетИтоговойСтоимости_ПоОсновнойИДополнительнойСделке (на английский сами переведете, вам область понятнее). А выглядеть ваш тестовый метод может так (при этом в xUnit тестовые данные можно подставлять из свойства и даже из отдельного объекта): [Theory] [PropertyData("PriceData")] // тестовые данные находятся в отдельном свойстве public void РасчетИтоговойСтоимости_ПоОсновнойИДополнительнойСделке( ComissionType mainComissionType, Comission mainComission, CalculationType mainCalculationType, ComissionType auxComissionType, Comission auxComission, CalculationType auxCalculationType, double expectedPrice) { // код теста } public static IEnumerable PriceData { get { // данные можете захардкодить, а можете прочитать из файла return new[] { new object[] { // основная сделка ComissionType.InsideTotal, new Comission(0.1, 0.5), CalculationType.ОтОборота, // дополнительная сделка ComissionType.OnTopGate, new Comission(0.42, 0.99), CalculationType.ОтБалды, // результат 100500 }, // и так далее для других пар }; } } Я думаю во многих тестовых фреймворка есть возможность создания параметрических тестов Способ задания параметров может отличаться, но суть останется та же: данные выносятся в секцию с данным, в названии теста остается только название кейса. Дополнительную информацию о параметрических тестах можной найти в блоге Сергея Теплякова.

Ответ 2



Перед самым первым "модульным тестом" Нужно договориться с самим собою или командой о том, что считать "модульным тестом"? К примеру в книге Роя(см. в раздел "рекомендуемая литература") дается ясное определение "unit" и достаточно удобное для программиста. Без четкого и уверенного понимания, что такое "unit" писать модульные тесты настоятельн не рекомендую. Даже если Ваше понимание, что такое модульный тест отличаются от того что говорит Рой все равно оно должно быть унифицированным и стабильным для ВАС или Вашей команды. Что требует некоторой договоренности. Понимание "Модульный тест" от Роя: Рой, на мой взгляд, предлагает самую удобную и практичную схему именования: {unit-of-work} _ {scenario} _ {expected-results-or-behaviour} Где: unit-of-work - это как раз то что Вы понимаете под 'unit' или о чем договорилис с командой. scenario - это конкретное действие над тестируемым объектом expected-results-or-behaviour - это Ваши ожидания относительно того, что должно произойти Примеры: jsonParse_InvalidFileExtension_ThrowInvalidLogFileException Сразу видно, что проверяется парсер json-документа на файле с недопустимым файловым расширением и поэтому должно быть брошено InvalidLogFileException. opimizeByteCode_UnknownInstruction_ReturnMinusOne Тут видно, что при оптимизации байт-кода будет возвращаться -1 в случае если встретится неизвестная инструкция Написание модульных тестов со сложными условиями Это совершенно другой вопрос Если кратко, то в одном модульном тесте ОДНА проверка. Если условие сложно, значи там несколько под условий и значит надо Взять и разбить на несколько проверок - модульных тестов В любом тесте не должно быть условной логики. Если в production-коде есть две ветки, одна if-часть, другая else-часть. Значит пишется ДВА модульных теста. и т.д. и т.п. Но все это тема другого обсуждения Рекомендации по названиям тестов автора вопроса testCustomerRedirectUrlAfterSelectLastTransactionProcessShouldContainsUrlToConfirmRecurringProfilePageWhenTransactionTypeIsCreateRecurringProfile Что неправильно, на мой взгляд, в вашем тесте: Префикс 'test'. Если смотрю тестовый проект с набором модульных тестов и когда вижу что-то, то что это если не тест? В C# есть [Test], в python-е есть py.test. Этого достаточно! Имя не разделено на логические части. Где действие? Где условие? Где ожидаемый результат А где то что мы тестируем? Все смешалось в одну кучу! Любой программер либо поленится прочитать такое имя, либо начнет разделять на части и не факт, что он разделит его так, как делил на части автор теста! Слово 'when' оно лишнее! Если у Вас есть принятая схема именования, то Вы или Ваш коллега итак знает, где искать условие или действие! Рекомендуемая литература: Настоятельно рекомендую скачать, а лучше купить книгу Art Of Unit Testing. Даже если Вдруг не пишите на C#. Знания полезны! Ответ SO-участника @Timofey Bondarev на мой вопрос Как заменить префикс 'test_' на 'should_' в модульных тестах с примением unittest?

Ответ 3



1. Имена тестов должны быть строками Начать надо с того, что имя теста - это имя функции. В отличие от функций, мы н вызываем тесты из кода, имя теста нужно только для того чтобы один раз описать что он тестирует, чтобы это описание потом записалось в лог с результатами тестирования. По этому в некоторых тестовых фреймворках имена тестов - это обычные строки. Например в Catch (C++) TEST_CASE("что-то когда такие-то условия") { auto testee = Testee(1, 2); REQUIRE(testee.foo() == 1); } Или Mocha (JS и прочие *script) describe('что-то', function () { it('когда такие-то условия', function () { var testee = Testee(1, 2); assert.equal(testee.foo(), 1); }); }); Интересным исключением являются языки где идентификаторы могут иметь любые символы, как например в F# [] let ``twice удваивает числа`` () = twice(1) |> should equal 2 2. Не надо повторяться (принцип DRY) Если тестовый фреймворк позволяет группировать тесты, то это надо использовать. Вмест тестов Чтото_КогдаЭто_ВотЭто, Чтото_КогдаДругое_ВотЭто и Чтото_КогдаДругое_ЕщеИЭто тесты можно сгруппировать. Пример на Mocha/JS: describe('что-то', function () { var testee = Testee(); describe('когда так-то', function () { it('вот это', function () { testee.setup(1); assert.equal(testee.f(), 1); }); }); describe('когда по другому', function () { testee.setup(2); it('вот это', function () { assert.equal(testee.f(), 20); }); it('и еще это', function () { assert.equal(testee.g(), 21); }); }); }); Или для Catch/C++: TEST_CASE("что-то") { auto testee = Testee(); WHEN("когда так-то то вот это") { testee.setup(1); REQUIRE(testee.f() == 1); } WHEN("когда по другому") { testee.setup(2); THEN("вот это") { REQUIRE(testee.f() == 20); } THEN("и еще это") { REQUIRE(testee.g() == 21); } } } Также зачастую нет смысла повторять код теста в названии теста. Надо описывать чт проверяет тест, а не как он это делает. По этому вместо StartButton_WhenPressed_BecomesDisabled можно написать TEST_F(ButtonsPanelTest, StartButtonPress) { EXPECT_CALL(delegate_mock, Start()); start_button.Press(); ASSERT_FALSE(start_button.IsEnabled()); } Как правило тест включает в себя начальные условия (GIVEN/WHEN часть) и результат (THEN часть). Если для каких-то начальных условий тест один, то не зачем писать в названии то что проверяется в THEN части. Это видно из кода, и если тест провалится, то текст проваленной проверки будет написан в логе. Примеры имен тестов Имена тестов зависят от тестового фреймворка и от уровня теста (юнит/интеграционный/регрессионный). Регрессионные тесты на Mocha/JS describe("File IO", function() { it("can read text", function() {...}) it("invalid file name error", function() {...}) it("read after EOF error", function() {...}) }) Юнит-тесты на Catch/С++, когда в Catch использовались имена тестов разделенные / TEST_CASE_METHOD(ParserFixture, "Notifications/Empty") { SECTION("no 'type' field") {...} SECTION("empty updates") {...} } TEST_CASE_METHOD(ParserFixture, "Notifications/Status update") {...} TEST_CASE_METHOD(ParserFixture, "Notifications/Profile update") {...} Более новые тесты Catch/C++, с тегами TEST_CASE("request line", "[http][parser]") {...} TEST_CASE_METHOD(FrameReceiverFixture, "is frame complete", "[websocket]") {...} Тесты на gtest/C++ (из кода Chromium) TEST_F(DownloadItemTest, CompleteDelegate_SetDanger) {...} TEST_F(DownloadItemTest, NotificationAfterOnDownloadTargetDetermined) {...} Тесты на Golang func TestSomeClassMethod_CanDoThis(t *testing.T) {...} Если имя теста нельзя задать строкой, то одновременное использование CamelCase нижнего подчеркивания дает довольно неплохо повышает читабельность. Например вместо TEST_CASE("some unit") { WHEN("this state") { THEN("those results") { ... можно написать SomeUnit_WhenThisState_ThoseResults.

Ответ 4



Очень длинное название теста можно считать диагностическим сигналом. Такие сигнал можно рассматривать с разных точек зрения, но в любом случае, как правило они свидетельствуют о наличии или возможном возникновении проблемы. Минимально возможные варианты: В тестируемом коде реализуется неправильная архитектура Это не модульный, а возможно интеграционный тест Неправильная реализация собственно тестового модуля Разработчик не выспался и(или) употреблял какие-либо трансформаторы сознания в избыточных количествах Раз уж вы привели пример, то давайте на него и посмотрим. Для начала я попробую извлечь из английского названия вашего теста русский смысл: "Удостовериться в том, что URL, на который перенаправляется клиент после обработк последней транзакции, должен содержать URL соответствующий профилю возврата на страницу, если тип транзакции реализует профиль возврата." Давайте предположим как может выглядеть собственно тело такого теста: [Test] public void redirect_to_profile_url_test() { Transaction trn = new TransactionCreateRecurringProfile(); this.executionContext.Add(trn); this.executionContext.ExecuteTransaction(); Assert.AreEqual(this.recurringProfileUrl, this.executionContext.RedirectUrl); } В первом приближении все вроде кратко, симпатично и читабельно. Давайте предположим как может выглядеть тест, который удостоверяется в обратном: [Test] public void do_not_redirect_to_profile_url_test() { Transaction trn = new TransactionDoesNotCreateRecurringProfile(); this.executionContext.Add(trn); this.executionContext.ExecuteTransaction(); Assert.AreNotEqual(this.recurringProfileUrl, this.executionContext.RedirectUrl); } Естественно мы предполагаем что вся подготовительная перед тестом работа вынесена из собственно тестов: [TestFixture] public class RedirectAfterTransactionTest { private ExecutionContext executionContext; private String recurringProfileUrl; [SetUp] public void setUp() { this.executionContext = new RealExecutionContext(); this.recurringProfileUrl = "http://localhost/bla-bla-bla"; } } Обновление Как я именую тесты? Примерно так (из рабочего проекта): public class InMemoryRepositoryTest { @Test public void insert_test() {} @Test public void find_test() {} @Test public void delete_test() {} @Test public void update_data_with_same_id_test() {} @Test public void cant_update_if_item_with_id_not_exists_test() {} @Test public void do_not_insert_other_type_data_test() {} @Test public void insert_with_merge_test() {} @Test public void update_with_merge_test() {} } Имя тестового класса ссылается на наименование тестируемого класса в коде. А имен тестовых методов обозначают имена тестируемых методов в тестовом классе и пытаются кратко сообщить об условиях тестирования каждого конкретного метода. Я предпочитаю сильно не увлекаться с подробностями в наименованиях тестовых методов потому что по мере рефакторинга кода, наименование метода может рассинхронизироваться с содержанием. Это особенно справедливо для случаев, когда проект верстается под сильным временным прессингом со стороны заказчика. Правила именования - это в некотором роде Дао или Кунг-Фу... Необходимо соблюдат баланс. Если имя тестового метода длиннее, чем сам код, содержащийся в тестовом методе то разработчику, который рефакторит метод, может быть проще и быстрее прочитать и понять код, чем продираться сначала через литературные перипетии названия на английском языке.

Ответ 5



Думаю, тестовые методы тут не стоит выделять отдельно - в целом они подчиняются те же правилам именования, что и обычные методы. В числе этих правил - не давать метод слишком длинное и неудобочитаемое название. Краткость - сестра таланта, как писал один русский писатель. Для всеобъемлющего описания того, что именно тестирует метод, человечество изобрело комментарии, в которых можно растекаться мыслью по древу (хотя злоупотреблять не стоит и там). Ваш же вариант именования сильно похож на применявшуюся несколько столетий наза витиеватую манеру давать названия книгам, когда в названии описывается едва ли не весь сюжет книги. Например, полное название Робинзона Крузо звучит устрашающе-длинно: "Жизнь, необыкновенные и удивительные приключения Робинзона Крузо, моряка из Йорка прожившего 28 лет в полном одиночестве на необитаемом острове у берегов Америки бли устьев реки Ориноко, куда он был выброшен кораблекрушением, во время которого весь экипаж корабля кроме него погиб, с изложением его неожиданного освобождения пиратами; написанные им самим"

Ответ 6



Я считаю что название модульного теста должно быть ёмким. Особенно если тест пишетс в BDD стиле. Особенно если ваш фреймворк для тестирования умеет это раскладывать в читабельные отчёты. Нет никакой разницы, насколько длинное имя метода с точки зрения читающего тест программиста Есть только один важный аспект, где длина метода будет пропорциональна пониманию, что он тестирует - это отчёт, который будет читать другой человек, не программист. На мой взгляд ёмкие названия (пример): testShouldAlwaysPreparePathsAndConvertThemInToArray testShouldRegisterNamespacesForMultipleDirectories testShouldThrowExceptionIfDbIsMissingOrInvalid testShouldSetOptionsWhenConfigPassedToTheConstructor В любом приличном фреймворке для тестирования такие имена методов трансформируются в что-то подобное (пример): Test should always prepare paths and convert them in to array [OK] Test should register namespaces for multiple directories [OK] Test should throw exception if db is missing or invalid [OK] Test should set option whet config passed to the constructor [FAIL] Согласитесь, такое гораздо приятнее читать. Это можно показать кому угодно, ПМ наприме или фронтенд разработчику : ) и ему совсем не обязательно знать фреймворк, который вы используете или язык. Не останавливайтесь на длине метода. Делайте его на столько длинным, насколько этог требует сценарий. Да, конечно, в пределах разумного. Несколько не с руки будет читать такой тест, если он из-за ширины экрана, в лучшем случае разобъётся на несколько строк, а то и вовсе обрежется.

Ответ 7



Меня учили что юнит тест всегда имеет шаблон названия: когдаТогда whenThen @Test whenStringArgInThenArgAddInArrayList() { Item item = new Item(); item.getList.add("a"); assertThat(item.getList[0].get, is("a")); }

Комментариев нет:

Отправить комментарий