All streams
Search
Write a publication
Pull to refresh
23
0
Семён Солдатенко @SamSol

User

Send message
Где-то в документации попадалась рекомендация использовать массивы примитивов (int[], long[], etc.) в случае, когда необходимо написать «числодробилку», и классы коллекций во всех остальных случаях.

Даже не могу себе представить когда может оказаться выгоднее использовать коллекции-похожие-на-sdk-шные вместо sdk-шных или вместо массивов примитивов.
Если для тестирования какого-то класса приходится изголяться, значит пора его переписать.
Да, но в то же время, автор этого кода старался изо всех сил — я уверен. А получилось так. :-(
Почему? Видимо очень трудно забить гвоздь микроскопом.
Вот сслыка на мой комент
habrahabr.ru/post/174781/#comment_6073323
Там критикуется код с матчером. Выглядит компактно, зато полный набор недостатков.
Хотелось бы надеяться что матчеры принесут больше выгоды чем проблем.
Я внимательно к ним присматриваюсь, т.к. часто код с матчерами оказывается компактнее чем plain java. Но пока, мне кажется, что недостатки перевешивают достоинства.
Недостатки:
* Код с матчерами теряет «прозрачность»
* Утверждения относящиеся к функциональности перетекают из кода тестов в матчеры
* Увеличивается взаимное влияние тестов друг на друга через матчеры
* Матчеры не стимулируют разработку удобных интерфейсов между подсистемами

Из достоинств только то, что код, использующий матчеры, выглядит компактнее.
1. Упавший тест == Некорректный работа одного аспекта.
Проскипанный тест == неизвестность

2. Проблемы «шума» НЕ существует:
Для разработчика нет проблемы шума, т.к. он обычно запускает один и тот же тест, пока не завершит свою часть.
Для инженера интеграции (если такой есть), этой проблемы тоже не существует, т.к. от него ждут что он прогонит каждый тест и отловит ситуации с неправильной конфигурацией.
Для тестирования перед передачей в QA также не существует проблемы шума, т.к. результаты будут смотреть все разработчики.
Кроме того, ситуации когда падает значительное число тестов обычно весьма тривиальные — нарушение конфигурирования. Зачем пытаться защититься от этой ситуации, когда любой junior сможет с ней разобраться?

3. Эту работу нельзя автоматизировать.
Юнит-тесты это бурлящий котел. Они постоянно добавляются, удаляются и изменяются. Добиться того, чтобы всегда падал ровно один тест, а остальные предсказанно-падучие игнорировались, неподъемная задача. Даже если в какой-то момент вы добьетесь положительного результата, кто-то допишет новый тест, в котором забудет assume и вот у вас уже два упавших теста, и куча проскипаных.

Нам стоит вернуться в началу.
Какую задачу нужно решить?
У игнорируемых тестов аналогично упавшим пишется причина игнора

то есть вы выбираете второй пукт:
Считать, что игнорируемые тесты все равно что упавшие, и заняться поиском причин

Хорошо. Я бы в такой ситуации тоже выбрал бы этот пункт.

Мне непонятно еще как возможно, что одна и та же проверка в одном случае пройдет, а в другом — не пройдет

Юнит-тест должен падать, если:
1. В результате разработки были внесены непреднамеренные изменения функционирования
2. Если система оказалась в такой среде, где ее корректное функционирование нарушилось
3. Если система изменилась вслед за изменением требований, а тесты еще не отражают это изменение.
Ах, да. Ненаучная фантастика. Слышал об этом.

Я, возможно, смог бы добиться такой согласованности тестов, на «замороженной» системе. Но в реальных условиях, при непрерывном изменении функциональности системы, это даже мне не под силу.

Кроме того, зависимость юнит-тестов друг от друга — источник проблем.
Даже в вашем примере, в случае какой-нибудь ошибки в тесте который должен был бы упасть у вас будет 6799 прошедших, 201 игнорируемых, и ни малейших намеков на источник проблемы.
Предположим у вас есть 7000 единичных аспектов функционирования системы. И на каждый из них есть по одному тесту. Но в тестах используются assume, которые в некоторых условиях «отключают» некоторые тесты. Таким образом у вас 6800 тестов «проходят», остальные игнорируются по assume. Проходят они или нет, вам не видно — они игнорируются.

Что делать в таком случае?
* Включить «дурака», сказать «у нас все тесты проходят», и отдать такую систему в QA
* Считать, что игнорируемые тесты все равно что упавшие, и заняться поиском причин

Выбирайте.
Специально почитал. «The default JUnit runner treats tests with failing assumptions as ignored». Да, я верно представляю себе как работает assume.

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

В этой части наши взгляды совпадают?
Интересно кто-нибудь исследовал «антипаттерны» использования Hamcrest в модульном тестировании?
1. Если вследствие изменения функционального требования меняется тест, то можно сказать, что он отражает это требование. Если же тест не меняется, то НЕ отражает. Юнит-тесты _должны_ отражать требования. Следовательно, то, что не отражает функциональные требования не может называться юнит тестом.
Если же делать по одному матчеру на каждый тест, и туда выносить логику проверки, то придет Оккам и бритвой попишет!

2.
Но этот подход не работает для набора функциональных и интеграционных тестов

Да, я говорил о юнит тестах. Потому как для всех остальных видов автоматического тестирования проблема шума вообще не может возникнуть.
Особенно когда тесты запускаются автоматом после выкладки нового кода

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

3. ~

4. Вы думаете проще отследить «чистоту» матчера который должен меняться вслед за изменением функциональных требований и архитектуры проверяемой системы? Или вы надеетесь, что матчер будет настолько обобщенным, что ему не придется меняться вслед за изменением требований и архитектуры?

5. четко формулировать что нужно проверить. Вот мы и вернулись, к требованию «простоты» и «прямолинейности» тестов. А самый прямолинейный подход — проверять списки явно. Насчет лишней работы не беспокойтесь — Ctrl+D — легко и непринужденно дублирует строки.

6. Мое тестовое описание функциональности — слишком неконкретное, и может означать очень много чего. То же самое и предлагаемый вами код — такой же неформальный. А для понимания теста нужны конкретные детали — те три приведенные мной примера подходят и под ваш код, и под мое словесное описание, но ожидают различных реакций от системы.
1. Если требования меняются — тесты должны меняться! Если при изменении требований тесты не меняются (но меняется матчеры), то значит ваши тесты не отражают требования к системе. То есть они перестают быть юнит-тестами. Переименуйте их в SistemValidationMatcherInvocator, чтобы я не путал их с юнит-тестами, и продолжим.

2. Вы снова преподносите какую-то гипотетическую возможность предсказать падение тестов и отказаться от их запуска. Зачем?
Я расскажу как я пишу код с тестами (думаю многие так делают):
— В IDEA я запускаю один класс с тестами, для той части с которой я работаю. Бывает, что даже запускаю всего один тест (из контекстного меню).
— Вношу исправления в код, в тесты, раз за разом запуская один этот тест, пока он не заработает.
— Запускаю Весь тестовый класс для той части, которую только что правил, и если все тесты для этой подсистемы прошли
— Запускаю все имеющиеся тесты

Ваш «другой пример» отпадает при моем подходе. Пока я ковыряю механизм получения одинаковых объектов, и тест на проверку этого механизма падает, я смотрю только на этот тест. Шуму неоткуда взяться.

«Все круглое оранжевое, а апельсины круглые, но неважно какого цвета» — это снова слишком абстрактные утверждения, мало полезные для модульных тестов. В тестах необходимо проверить каждый конкретный фрукт.

3. Вот тут вам удалось привести достойный пример:
assertThat(minimumDiameter / maximumDiameter, greaterThan(0.95))

Этот код достаточно прямолинейный, конкретный и наглядный.

4. Чтобы преодолеть сложность написания «Грамотно написанного юнит-теста» вы предлагаете написать «Грамотно написанный матчер»?

5. Акцентирую внимание, что assertThat(fruits, everyItem(hasShape(Shape.ROUND))) — вполне корректный фрагмент. Но он плохо подходит под требование «простоты» и «прямолинейности» тестовых методов. Также как цикл:
for(Fruit fruit : fruits) {
  assertEquals(Shape.ROUND, fruit.getShape());
}

Этот фрагмент точно также скрывает особенность своего ложно-отрицательного срабатывания в случае пустого списка. Выглядит как будто есть проверка, а на самом деле, в некоторых случаях ее нет! Поэтому я рекомендую стараться писать как можно более простые тесты, и «даже списки индексировать константами». Потому что фрагмент:
assertEquals(Shape.ROUND, fruits.get(0).getShape())

и выглядит как проверка, и является проверкой.

6. Предложенный вами вариант нельзя точно прочесть не открывая матчер. Он может быть
// 1
assertEquals("+7923xxxxxxx", seller.getPhone());
// 2
assertEquals("+7923xxxxxxx", document.getSellerPhone());
// 3
assertEquals(seller.getPhone(), document.getSellerPhone());


Согласен, что связка Mockito + Hamcrest + JUnit может сделать с «закрытой» системой гораздо больше, чем доступ через открытые интерфейсы. Однако, как ни печально, эта особенность является НЕДОСТАТКОМ для системы модульного тестирования (в случае Test Driven Design). Такие тесты не заставят вас проектировать более удобные интерфейсы между компонентами вашей системы.
JUnit + Hamcrest не помогут выявить первопричину при «обильном» падении тестов, а вот скрыть эту причину они очень даже могут. Например вы только что написали тест, и отлично его помните. Система как-то очень сильно сломалась, но ваш тест даже не запустился (как и множество других). И вам во-первых не видно, что произошел обильный обвал тестов, а во-вторых в качестве причины указывается малознакомый или вообще незнакомый аспект. Вы писали фабрику фруктов, а упал какой-нибудь «Can not bind AccessRughts to null».

Обильные падения тестов не проблема — если все тесты упали — это красиво!

А вот если ваши тесты показывают что сломалась какая-нибудь левая фигня, вместо того, чтобы показать, что сломалась вся фигня целиком, вот это проблема.

Бывают «Хрупкие системы» — системы с чрезмерным внутренним связыванием. При малейшем изменении начинают валиться как карточный домик. Для таких систем особенно полезно проверять по одному аспекту в одном тесте.
У меня такое ощущение, что мы разговариваем о разных тестах. Что значит «игнорировать все тесты, которые не удовлетворяют условию»?

Если есть модульный тест — запускай его! Если нет — напиши! Вот и всё.

Может быть вы пытаетесь написать модульный тест, способный работать при любом состоянии испытуемой системы? Этого не требуется. Мало того, модульный тест должен падать, если система не в том состоянии, на которое он рассчитывал.
Спасибо за обстоятельный ответ. Я пожалуй также по пунктам поддержу дискуссию.

1. Первый пункт полностью голословный. Я сомневаюсь, что можно найти объективную количественную оценку, чтобы сказать, сказать что assertThat лучше чем assertEquals.

2. «Что делать, если потребуется… если неизвестно будет ли...» — это выражения НЕ для тестов. В тестах проверяются только 100% известные и конкретные факты.
Добавляете в систему Orange, и считаете что он должен делиться на дольки — пишете тест, который проверяет, что Orange делится на 8 долек. И все! Не грейпфрут, не лимон — потому что их еще нет в системе.
Когда кто-то допишет систему до работы с Кокосами он допишет и тесты для проверки что кокос не делится на дольки (если это важно). А ваш тест, будет продолжать проверять чтобы Orange продолжал делиться на 8 долек. Никаких абстракций в тестах. Все должно быть конкретным.

— Что значит «лишнее выполнение теста»? Нет такого понятия! Тесты нужно выполнять _всегда_.

— Про дублирование зря беспокоитесь. Каждый тест должен проверять один аспект системы. Поэтому когда вы хотите проверить «округлость» апельсина — проверяйте только его «округлость», а не цвет, дольки и другую фигню. Не нужно ничего дублировать в начале каждого теста (но можно «поднимать» окружение в setUp() и tearDown()). Если вы перед проверкой на округлость станете проверять цвет, запах, и что-то еще, это станет проблемой. Тест на округлость станет падать от того, что апельсины черные, несмотря на то, что они круглые.

— «Шум от тестов» возникает гораздо реже, если каждый тест проверяет только один аспект (см. мои пояснения выше). Очень редко бывает так, чтобы слишком много аспектов системы вдруг начали сбоить (конечно если вы не заняты портированием сишного кода с одной архитектуры на другую). Но если все же такое случилось, и разом упали десятки или сотни тестов — приступайте к анализу того теста, который вы знаете лучше всего (из числа упавших, конечно). В этом случае вы просто увидите проблему. (Ах, триггеры не прописали! Ой, нет прав на запись в папку! Блин, либы не подложили."

3. Категорически не согласен. Ни один «человеческий» язык не достаточно формален, чтобы отражать конкретные детали. Сравните:
// a) Человеческий язык
checkThatOrangeIsRounded()

// b) Первый формальный вариант
assertEquals(Shape.ROUND, fruit.getShape());

// c) Второй формальный вариант
float minimumDiameter = shtangentcirkul.getMinimalDiameter(fruit);
float maximumDiameter = shtangentcirkul.getMaximumDiameter(fruit);
assertTrue(minimumDiameter / maximumDiameter > 0.95);

Видите? Проверка «округлости» может оказаться сравнением с заданным значением или вычислением меры «округлости».

4. «далее вводите матчеры»? Зачем? Чтобы запутать тех, кто попытается понять что делает ваш тест? И в дальнейшем, если тест упал, и нужно править матчер — это будет означать, что ваши исправления начнут влиять на все остальные тесты использующие этот матчер. Даже если они и не падали.

5. assertThat(fruits, everyItem(hasShape(Shape.ROUND))); не упадет, если список окажется пустым, а assertEquals(Shape.ROUND, fruit.get(0)) упадет.

6. Правильный матчер, слишком абстрактный чтобы написать например «У продавца в договоре должен быть записан телефон». Все сообщения которые можно сформировать на таком уровне абстракции уже и так формируются в методах assertEquals().
Экстремальное программирование (Extreme Programming)
В какой мере тесты должны служить документацией можно понять из идеологии XP. Там довольно гармоничное использование Историй и Тестов. Истории первичны, кратки и ясные, а тесты «подпирают» все важные нюансы и частные случаи.
Иногда мне приходилось встречать тесты, которые пытались подменить собой истории:
@Test
public void as3OrangeBuyerIMustFound3RoundedFruitsInMyBasket() {
   ...
}

Увы, эта практика плохо работает. Из-за сильного формализма ЯП, теряется возможность править истории вслед за изменением видения системы заказчиком или потребителями (и со скоростью этих изменений). Так что не стоит «документировать» систему тестами. Документируйте историями, а тестами проверяйте корректность работы системы по мере ее развития.
Не стоит хвататься за такой мощный инструмент сразу. Начните с элементарных тестов:
public void testOrangeIsRounded() {
    Fruit fruit = new Orange();
    assertEquals(Shape.ROUNDED, fruit.getShape());
}

public void testOrangeIsOrange() {
    Fruit fruit = new Orange();
    assertEquals(Color.ORANGE, fruit.getColor());
}

public void testOrangeIsSweat() {
    Fruit fruit = new Orange();
    assertTrue("The fruit was not sweet as it MUST be.", fruit.isSweat());
}

Когда пишете тесты думайте о том, кто их будет поддерживать. Делайте все прямолинейно императивно и последовательно. Даже списки проверяйте константной индексацией:
public void testAllFruitsInBasketIsRounded() {
    List<Fruit> fruits = basket.getFruits();
    assertEquals(3, fruits.size()); // In tests you always know how many
    assertEquals(Shape.ROUNDED, fruits.get(0).getShape());
    assertEquals(Shape.ROUNDED, fruits.get(1).getShape());
    assertEquals(Shape.ROUNDED, fruits.get(2).getShape());
}

Information

Rating
Does not participate
Location
Новосибирск, Новосибирская обл., Россия
Date of birth
Registered
Activity