company_banner

Тестирование в Яндексе. Матчеры: когда они полезны и как легко их использовать

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

    Сама по себе технология матчеров не новая — в текущем виде она была залита в репозиторий в июле 2012 года, а появилась и того раньше. Но, несмотря на это, многие о ней до сих пор не слышали или по каким-то причинам избегают. Мы хотим рассказать, как легко получать преимущества от её использования, и поделиться с вами нашей библиотекой матчеров.

    Предположим, у нас есть набор неких фруктов. Среди них чаще всего встречается круглый, оранжевый и сладкий.

    public class Fruit {
        
        ...
         
        public Color getColor() {...}
    
        public boolean isSweet() {...}
    
        public Shape getShape() {...}
    }
    


    Известно, что именно таким условиям удовлетворяет апельсин. Помимо фруктов, у нас есть и конвейер, через который можно пропустить такие фрукты. Есть и задача для конвейера — отсеять не апельсины, проведя серию тестов.

    И вот удача — у нас под рукой как раз оказался аппарат, который умеет определять сладкий ли фрукт и какого он цвета, сравнивать его форму с рядом известных и проводить еще множество проверок. Аппарат этот называется JUnit.

    Перед началом теста, на конвейер вываливается новый фрукт.

        @Before
        public void setUp() throws Exception {
            someFruit = getNextFruit();
        }
    


    Определим сперва, что фрукт круглый.

        @Test
        public void orangeIsRound() {
            assertEquals("Expected shape - " + Shape.ROUND + ", but was - " + someFruit.getShape(),
                    someFruit.getShape(), Shape.ROUND);
        }
    


    Затем, что фрукт сладкий.

        @Test
        public void orangeIsSweet() {
            assertTrue("Fruit should be sweet - expected TRUE", someFruit.isSweet());
        }
    


    И, наконец, посмотрим на его цвет.

        @Test
        public void orangeHasOrangeColor() {
            assertEquals("Orange has orange color, but was - " + someFruit.getColor(),
                    someFruit.getColor(), Color.ORANGE);
        }
    


    Какие минусы сразу очевидны? Во-первых, в каждой из проверок пришлось самостоятельно склеивать комментарий из ожидаемого значения, фактического значения, значения дополнительных уточнений и разных междометий. Даже описание этого списка утомительно.

    Во-вторых, представьте, что мы решили определять еще и сорт апельсинов, добавили для этого специального «дегустатора» и сказали ему: «Одобряй только сорт «Валенсия»». Но «дегустатор» не знает, что решил проверяющий автомат, если сам не спросит — автомат не болтлив. В итоге он будет пробовать все подряд. Чтобы не отравить «дегустатора» мячиком, который не успели проверить на сладость, нужно научить его игнорировать всё лишнее. Для этого ему нужно спросить автомат отдельно и самостоятельно, после чего всё забракованное отложить в сторону и больше не трогать.

    «Дегустатор» — это всего лишь еще один тест в нашем JUnit-аппарате, поэтому можно и нужно использовать встроенный рантайм-механизм игнорирования теста — assume. Тогда сценарий начала дегустации будет выглядеть так.

        @Test
        public void degusto() {
            assumeTrue("Expected shape - " + Shape.ROUND + ", but was - " + someFruit.getShape(),
                    someFruit.getShape().equals(Shape.ROUND));
            assumeTrue("Fruit should be sweet - expected TRUE", someFruit.isSweet());
            assumeTrue("Orange has orange color, but was - " + someFruit.getColor(),
                    someFruit.getColor().equals(Color.ORANGE));
    
            // Далее дегустатор полностью уверен что фрукт можно кусать.
        }
    


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

    Вдобавок ко всему, придется отказываться от assertEquals, assertNotEquals, assertNotNull, assertArrayEquals и т.д. В стандартной поставке JUnit эти assert* есть почти на любой тривиальный случай. А некоторых еще и несколько — на каждый тип аргументов. То есть логика проверки заключена в названии метода и жёстко привязана к его реализации. А теперь представьте, сколько нужно было бы кода дублировать и поддерживать, если на каждый assert* пришлось бы сделать аналогичный assume*.

    Значит, нужно разделить логику самой проверки, вывода описания и функцию принятия решения:

    • бракуем — assert,
    • игнорируем — assume,
    • а так же, фильтруем, ищем нужные, просеиваем, изменяем и т.д.

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

        @Test
        public void orangeIsRoundWithMatcher() {
            assertThat(someFruit, is(round()));
        }
    
        @Test
        public void orangeIsSweetWithMatcher() {
            assertThat(someFruit, is(sweet()));
        }
        
        @Test
        public void orangeHasColorWithMatcher() {
            assertThat(someFruit, hasColor(Color.ORANGE));
        }
    


    Для такой красоты, существует специальная библиотека Hamcrest. Она содержит в себе и интерфейс для реализации, и методы assertThat и assumeThat (последний, на самом деле, внутри JUnit, но использует интерфейс из Hamcrest). Они и спрашивают матчер об объекте, принимая решение.

    Начиная с версии 4.11, в зависимостях JUnit библиотека Hamcrest имеет версию не ниже 1.3. Именно она ввела интерфейс, в котором реализовано всё, что описано дальше. Поэтому, используя мавен, достаточно подключить JUnit 4.11, и минимально необходимый набор инструментов готов к использованию. А для полного набора всех доступных матчеров из поставки Hamcrest, понадобится артифакт hamcrest-all, который можно подключить отдельно.

    Так может выглядеть ваш pom.

    Как это работает?


    В библиотеке есть абстрактный класс TypeSafeMatcher, параметризуемый по типу проверяемого объекта. Класс предоставляет для переопределения три метода:

    • public boolean matchesSafely(Fruit fruit) — логика проверки,
    • public void describeTo(Description description) — описание ожидаемого значения,
    • protected void describeMismatchSafely(Fruit item, Description mismatchDescription) — описание полученного значения.


    Экземпляр класса, расширяющего этот, перед выполнением собственного кода выполнит родительский — рутинные проверки поступающего объекта на null и соответствие указанному классу.

    Например, матчер, проверяющий форму фрукта, выглядит так:

    public class ShapeMatcher extends TypeSafeMatcher<Fruit> {
        private Shape expected;
    
        public ShapeMatcher(Shape expected) {
            this.expected = expected;
        }
    
        @Override
        public boolean matchesSafely(Fruit fruit) {
            return expected.equals(fruit.getShape());
        }
    
        @Override
        protected void describeMismatchSafely(Fruit item, Description mismatchDescription) {
            mismatchDescription.appendText("fruit has shape - ").appendValue(item.getShape());
        }
    
        @Override
        public void describeTo(Description description) {
            description.appendText("shape - ").appendValue(expected);
        }
    
        @Factory
        public static ShapeMatcher round() {
            return new ShapeMatcher(Shape.ROUND);
        }
    }
    
    


    Количество кода сперва пугает. Но, если приглядеться, сразу заметно, что каждое логическое действие выделено в отдельный метод, а в тесте вызов умещается в одно слово — использовать очень просто!

    Но и это не все


    Частая ситуация, как, например, выше, вызвана необходимостью использовать для проверки только одно свойство объекта. Целый класс для этого — лишняя трата времени и сил. Здесь на помощь приходят анонимные классы Java и абстрактный класс FeatureMatcher<WhatWeGet, WhatWeWannaCheck>, параметризуемый двумя типами: какой объект поступит на вход и свойство какого типа нужно проверить.

    Конструктор у этого класса один и требует 3 атрибута:

    • матчер, который применим к WhatWeWannaCheck типу,
    • описание ожидания (оно добавится к описанию субматчера),
    • описание полученного значения (оно добавится к мисматч-описанию субматчера)

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

    Перепишем наш матчер для формы, а заодно и остальные, используя этот класс.

    public class Matchers {
    
        public static Matcher<Fruit> hasShape(final Shape shape) {
            return new FeatureMatcher<Fruit, Shape>(equalTo(shape), "fruit has shape - ", "shape -") {
                @Override
                protected Shape featureValueOf(Fruit fruit) {
                    return fruit.getShape();
                }
            };
        }
    
        public static Matcher<Fruit> round() {
            return hasShape(Shape.ROUND);
        }
    
    
        public static Matcher<Fruit> sweet() {
            return new FeatureMatcher<Fruit, Boolean>(is(true), "fruit should be sweet", "sweet -") {
                @Override
                protected Boolean featureValueOf(Fruit fruit) {
                    return fruit.isSweet();
                }
            };
        }
    
    
        public static Matcher<Fruit> hasColor(Color color) {
            return new FeatureMatcher<Fruit, Color>(equalTo(color), "fruit have color - ", "color -") {
                @Override
                protected String featureValueOf(Fruit fruit) {
                    return fruit.getColor();
                }
            };
        }
    
    }
    


    Feel the POWER OF MATCHERS


    Одно из главных преимуществ матчеров — возможность их объединения. Для этого в Hamcrest есть целый ряд специальных связующих: allOf, anyOf, both, either. Каждый из них заботливо соединит и описание ожидаемого значения, и описание проваленных матчеров из цепочки.

    Благодаря этому, сценарий предпроверки для нашего «дегустатора» сокращается еще сильнее:
        @Test
        public void orangeBothSweetRoundAndOrangeColorWithMatchers() throws Exception {
            assumeThat(someFruit, both(round()).and(sweet()).and(hasColor(Color.ORANGE)));
            // дальше дегустаторская магия
        }
    


    Исходники всех тестов.

    Еще одна из замечательных возможностей, которую даёт эта технология, — применение одного или ряда матчеров к коллекции. Предположим, вместо одного фрукта за раз стал поступать целый набор и все объекты в нём нужно проверить одновременно. Больше не нужно никаких циклов — все проще простого:
    
        @Test
        public void orangeBothSweetRoundAndOrangeColorWithMatchers() throws Exception {
            assertThat(someFruitList, everyItem(both(round()).and(sweet()).and(hasColor(Color.ORANGE))));
        }
    

    Как говорим, так и пишем: проверить каждый элемент нашим матчером. Возможны вариации — например, проверять что в пачке есть хотя бы один объект, удовлетворяющий условию hasItem().

    Примеры работы с коллекцией.

    Еще раз о велосипедах


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

    Наша библиотека матчеров находится по адресу github.com/yandex-qatools/matchers-java.
    Метки:
    Яндекс 664,07
    Как мы делаем Яндекс
    Поделиться публикацией
    Комментарии 40
    • +1
      Как можно догадаться по апельсину, что речь пойдет об автоматическом тестировании?
      • +7
        Никак. Это шутка такая :)
      • +1
        Сегодня только начал изучать unit-тесты. Так что статья очень вовремя! Буду писать сразу правильно.
        • 0
          Это прекрасно! Советую так же сразу ознакомиться с технологией рул (Rule) в JUnit. Тоже очень мощный и полезный инструмент.
          • +1
            Не стоит хвататься за такой мощный инструмент сразу. Начните с элементарных тестов:
            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());
            }
            
            • 0
              С вами полностью согласен.

              Тесты в какой-то мере должны служить документацией к коду. То что Вы написали в качестве примера элементарного кода, на самом деле выглядит очень просто и читабельно.

              • 0
                В какой мере тесты должны служить документацией можно понять из идеологии XP. Там довольно гармоничное использование Историй и Тестов. Истории первичны, кратки и ясные, а тесты «подпирают» все важные нюансы и частные случаи.
                Иногда мне приходилось встречать тесты, которые пытались подменить собой истории:
                @Test
                public void as3OrangeBuyerIMustFound3RoundedFruitsInMyBasket() {
                   ...
                }
                

                Увы, эта практика плохо работает. Из-за сильного формализма ЯП, теряется возможность править истории вслед за изменением видения системы заказчиком или потребителями (и со скоростью этих изменений). Так что не стоит «документировать» систему тестами. Документируйте историями, а тестами проверяйте корректность работы системы по мере ее развития.
                • 0
                  Прошу прощения, что такое идеология ХР?
                  • 0
                    Экстремальное программирование (Extreme Programming)
              • 0
                Отвечу по-порядку:
                1. Несмотря на мощность этого инструмента, он очень легок в освоении и использовании. Поддерживать тесты с его использованием становится гораздо проще, чем без него, когда количество тестов переваливает через некоторое количество. Ведь логика тестов отделена от логики получения конкретного значения от объектов — яйца в одной корзине, курицы в другой.

                2. Что делать, если понадобится проверить все оранжевые круглые фрукты на количество долек, если неизвестно — будет ли следующий фрукт делиться на дольки? Будет тест на круглость, будет тест на оранжевость. Предположим они провалились — как избежать лишнего выполнения третьего теста? Использовать assume механизм. Если его не использовать, а продублировать первые два теста в начале третьего — то получим сразу 2 проблемы:
                — дублирование кода — нужно ведь получать свойство и сообщать о нем так же как и в первых тестах.
                — шум от проваленного теста. Зачем нам 2 теста, которые провалятся с сообщением о том, что фрукт не оранжевый? Это будет ясно и из первого проваленного теста. А представьте, что таких тестов будет в 10 раз больше, или в 100. Просматривать результат в котором провалилось 100 тестов вместо 2х очень тяжело и непонятно куда бежать и что делать.

                3. Правильная мысль — думайте о том, кто будет поддерживать тесты: когда тест написан человеческим языком, в него гораздо проще въехать, а значит правильно распознать сценарий и изменить только необходимые части, не переписывая с 0 весь тест.

                4. Правильная мысль — делайте все последовательно. Только начинайте со сценария теста — пишите что вы хотите проверить, а дальше вводите в дело матчеры, в которые и выносите логику получения свойства из объектов. В дальнейшем не придется менять тесты в которых вы используете свойство этого объекта для проверки — придется немного подправить матчер.

                5. Абсолютно не согласен с проверкой списков константной индексацией. Я вижу три одинаковых строчки, с различием только в индексе. С матчером это будет одна строка, с полным соответствием смыслу проверки. Стоит только представить как при таком подходе будет расти копипаста, если таких тестов будет несколько, а список вдруг внезапно вырастет на пару элементов.
                С матчером это будет выглядеть так:
                assertThat(fruits, everyItem(hasShape(Shape.ROUND)));
                


                6. Многие ставят в пример то, как удобно формировать свое сообщение при использовании assertTrue. Но ведь никто не отменяет такой возможности и при assertThat! Просто в большинстве случаев этого делать уже не приходится — правильный матчер все сформирует сам
                • 0
                  Спасибо за обстоятельный ответ. Я пожалуй также по пунктам поддержу дискуссию.

                  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().
                  • 0
                    1. Такая оценка есть — легкость замены проверки и количество строк кода, которые нужно редактировать при изменении требований. Предположим, стало необходимым проверять что поля не эквивалентны, а одно должно содержать другое в своем значении. С assertEquals придется переписывать тест с 0. Как и все тесты, где используется поле, к которому изменились критерии оценки. А если еще и комментарий составлялся вручную, так и с ним придется тоже заморочиться. С матчером — либо просто подредактировать один класс текущего матчера, либо просто заменить матчер на другой.

                    2. Тесты призваны находить ошибки. В случае если вы в полной уверенности что создали апельсин, а вместо него создалось яблоко (например если вы тестируете фабрику фруктов), вам будет достаточно диагностики от 2х проваленных тестов, вам не нужен уже запуск остальных, которые зависят от этих свойств.

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

                    «Тест на округлость станет падать от того, что апельсины черные, несмотря на то, что они круглые.» — Ситуации когда логика говорит — все круглое должно быть оранжевым, но при этом все апельсины должны быть круглыми, а их цвет не важен. Как такое организовать только на assertEquals? Это возможно, но зачем пытаться извратиться, когда это легко помогают решить матчеры, вынеся логику в один отдельный объект и позволяя вращать и объединять эту логику как нравится и в любых количествах.
                    • 0
                      3.
                      // c) Второй формальный вариант
                      float minimumDiameter = shtangentcirkul.getMinimalDiameter(fruit);
                      float maximumDiameter = shtangentcirkul.getMaximumDiameter(fruit);
                      assertTrue(minimumDiameter / maximumDiameter > 0.95);
                      
                      
                      //С матчером самописным - чем не формально и при этом человекочитаемо?
                      assertThat(fruit, hasDiameterMinToMaxDivision(greaterThan(0.95)));
                      
                      public static Matcher<Fruit> hasDiameterMinToMaxDivision(Matcher<Double> matcher) {
                              return new FeatureMatcher<Fruit, Double>(matcher, "exp", "was") {
                                  @Override
                                  protected Double featureValueOf(Fruit actual) {
                                      Double minimumDiameter = shtangentcirkul.getMinimalDiameter(fruit);
                                      Double maximumDiameter = shtangentcirkul.getMaximumDiameter(fruit);
                                      return minimumDiameter / maximumDiameter;
                                  }
                              };
                          }
                      
                      
                      // Со стандартными матчерами hamcrest - greaterThan равноценно > - но читается проще. + Без лишних телодвижений заодно расскажет что ждали и что получили.
                      assertThat(minimumDiameter / maximumDiameter, greaterThan(0.95))
                      


                      4. Грамотно написанный матчер говорит все что делает своим названием, содержит четкую логику в одном определенном месте и никак не может запутать — матчер тоже не должен быть аля «проверить что все круто». Соответственно если меняется логика всей проверки матчера, то невозможна ситуация, когда в одном месте она должна быть старой, а в другом — новой. Тут либо изначально должно быть несколько матчеров, либо где то закралась ошибка в тесты.

                      5. Если список не должен быть пустым, то матчер должен выглядеть так:
                       assertThat(fruits, both(hasSize(greaterThan(0))).and(everyItem(hasShape(Shape.ROUND))));
                      

                      Это будет ошибкой формулировки требования к проверке, а не ошибкой матчера.

                      6.
                      assertThat(seller, hasDocument(withTelephone(not(nullValue()))));
                      


                      Надо проверить что телефон подходит под паттерн? Меняем только внутренний матчер:
                      assertThat(seller, hasDocument(withTelephone(matchesPattern("\+7[0-9]{10}"))));
                      


                      Реализацию несложно придумать на основе того же FeatureMatcher

                      Кстати учитывайте, что тесты на JUnit могут быть не только юнит-тестами, но и функциональными и интеграционными. В этом случае мы не владеем кодом, который тестируем и не можем его разобрать на объектики и довольствуемся только внешними интерфейсами.
                      • 0
                        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). Такие тесты не заставят вас проектировать более удобные интерфейсы между компонентами вашей системы.
                        • 0
                          Если при изменении требований тесты не меняются (но меняется матчеры), то значит ваши тесты не отражают требования к системе. То есть они перестают быть юнит-тестами.


                          Матчеры — это что. Тесты — это как. Это одно целое, разделенное во взаимосвязанные сущности. Код матчера можно обычно копипастить внутрь теста, и ничего от этого не изменится. Но когда такая проверка занимает больше того, что можно воспринять за пару секунд внутри теста, либо когда такая проверка встречается несколько раз — ее выносят для переиспользования и логического отделения. Изменение этого кода ничем не отличается от изменения теста.

                          2. Хорошо. Когда есть возможность ковырять один объект и локально же запускать для него тестики — в этом случае шум действительно не проблема. Но этот подход не работает для набора функциональных и интеграционных тестов, которые нужно запускать все разом и диагностировать точно первопричину, а не тот факт что все сломалось и паника паника паника. Особенно когда тесты запускаются автоматом после выкладки нового кода, а об их провале идут уведомления в смс, почту и куда то еще.

                          4. Матчер — еще более атомарная единица (сорри за тавтологию), чем тест — поэтому его чистоту отследить проще.

                          5. Мок объекты скрывают свою подлую пустую сущность, давайте не будем их использовать?
                          Характеристика линейности вполне осязаема — отсутствие ветвления — цикл — ветвление, условие — ветвление. Вызов инструмента, работающего по четкой спецификации — это не странные пассы руками, это его применение по назначению. Перебор элементов вручную ничем от такого вызова не отличается, кроме огромного количества лишней работы. По поводу пустоты списка повторюсь — нужно четко формулировать что нужно проверить.

                          6. seller что? hasDocument. Значит у продавца есть документ, ок. Документ какой? withTelephone. Телефон должен быть каким? Не пустым — withTelephone(not(nullValue()))

                          «У продавца в договоре должен быть записан телефон»

                          О том сколько вариантов проверок можно еще нагенерировать из этой фразы — это проблемы фразы, русский язык вполне позволяет указать более точные формулировки, и так же точно матчеры позволяют уточнять детали.
                          Проблема того что нужно заглянуть в реализацию матчера — не проблема, это же не структурное программирование.
                          • 0
                            1. Если вследствие изменения функционального требования меняется тест, то можно сказать, что он отражает это требование. Если же тест не меняется, то НЕ отражает. Юнит-тесты _должны_ отражать требования. Следовательно, то, что не отражает функциональные требования не может называться юнит тестом.
                            Если же делать по одному матчеру на каждый тест, и туда выносить логику проверки, то придет Оккам и бритвой попишет!

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

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

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

                            3. ~

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

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

                            6. Мое тестовое описание функциональности — слишком неконкретное, и может означать очень много чего. То же самое и предлагаемый вами код — такой же неформальный. А для понимания теста нужны конкретные детали — те три приведенные мной примера подходят и под ваш код, и под мое словесное описание, но ожидают различных реакций от системы.
              • +1
                Программирование путём построения примитивов, объединяющихся друг с другом комбинаторами в произвольные формы является одним из моих излюбленных приёмов с тех пор, как я увидел такой подход в SICP. Комбинаторы парсеров, функторы, futures и прочие монады — это прекрасно.
                • +2
                  Какую задачу пытались решить?

                  assertEquals(Shape.ROUND, fruit.getShape()) — вот эта строчка сформирует суперское сообщение об ошибке и кристально ясный стэк трейс.

                  assertThat(someFruit, is(round())) — выдаст запутанный стэк трейс.

                  Конечно assertTrue может потребовать сообщения:
                  assertTrue("The fruit was not sweet as it MUST be.", fruit.isSweat())

                  Но можно написать так: assertEquals(true, fruit.isSweat()), и сообщение станет "expected:<true> but was:<false>" — на практике такого сообщения обычно достаточно.

                  Я в недоумении.
                  • 0
                    Сравним на примере (можно позапускать тесты из примеров к статье на гитхабе)

                    используя assertEquals:
                    junit.framework.AssertionFailedError: Expected shape - ROUND, but was - SQUARE expected:<SQUARE> but was:<ROUND>
                    	at ru.yandex.qatools.examples.FruitDegustationTest.orangeIsRoundWithAssert(FruitDegustationTest.java:54)
                    


                    Используя матчер:
                    java.lang.AssertionError: 
                    Expected: fruit has shape -  <ROUND>
                         but: shape - was <SQUARE>
                    	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
                    	at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:8)
                    	at ru.yandex.qatools.examples.FruitDegustationTest.orangeIsRoundWithMatcher(FruitDegustationTest.java:61)
                    


                    Вам не понравились строчки стектрейса, которые относятся к
                    at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
                    
                    ?
                    Но их любая IDE даже подсвечивает другим цветом.

                    А что делать, если нам нужно игнорировать все тесты, которые не удовлетворяют условию? В случае с матчером, нам стоит просто поменять assertThat на assumeThat — с assertTrue такое прокатит, но там нужно везде думать о сообщении. А вот с assertEquals не сработает
                    • 0
                      У меня такое ощущение, что мы разговариваем о разных тестах. Что значит «игнорировать все тесты, которые не удовлетворяют условию»?

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

                      Может быть вы пытаетесь написать модульный тест, способный работать при любом состоянии испытуемой системы? Этого не требуется. Мало того, модульный тест должен падать, если система не в том состоянии, на которое он рассчитывал.
                      • 0
                        Да, возможно в разных видах тестов есть мелкие нюансы, которые могут влиять на детали применения инструментов. Но общая идеология неизменна — Должен быть тест на проверку состояния системы, который один и должен упасть. Остальные тесты ожидаемо упадут в этом случае. Зачем нагружать разработчика поиском и сортировкой упавших тестов, чтобы выявить первопричину, если это можно сделать программно при помощи стандартных средств JUnit+Hamcrest? И сэкономить в последующем во много раз больше времени, чем вставка лишнего assume и комбинации матчеров
                        • 0
                          JUnit + Hamcrest не помогут выявить первопричину при «обильном» падении тестов, а вот скрыть эту причину они очень даже могут. Например вы только что написали тест, и отлично его помните. Система как-то очень сильно сломалась, но ваш тест даже не запустился (как и множество других). И вам во-первых не видно, что произошел обильный обвал тестов, а во-вторых в качестве причины указывается малознакомый или вообще незнакомый аспект. Вы писали фабрику фруктов, а упал какой-нибудь «Can not bind AccessRughts to null».

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

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

                          Бывают «Хрупкие системы» — системы с чрезмерным внутренним связыванием. При малейшем изменении начинают валиться как карточный домик. Для таких систем особенно полезно проверять по одному аспекту в одном тесте.
                          • 0
                            Пожалуй, после
                            если все тесты упали — это красиво!

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

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

                                  Но все равно считаю, что падение всех тестов, лучше чем закулисный отказ от исполнения хотя бы одного теста. Вы согласны с этим?
                                  • 0
                                    Нет, я не могу понять чем сможет помочь в диагностике заведомо сломанный тест, упавший с НПЕ, ошибкой подключения к бд итд. Тест при прохождении assume выполнится — просто его непрохождение на шаге assume избавит от ложных ошибок.
                                    • 0
                                      Предположим у вас есть 7000 единичных аспектов функционирования системы. И на каждый из них есть по одному тесту. Но в тестах используются assume, которые в некоторых условиях «отключают» некоторые тесты. Таким образом у вас 6800 тестов «проходят», остальные игнорируются по assume. Проходят они или нет, вам не видно — они игнорируются.

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

                                      Выбирайте.
                                      • 0
                                        Таким образом у вас 6800 тестов «проходят», остальные игнорируются по assume. Проходят они или нет, вам не видно — они игнорируются.


                                        Не могут часть пройти — часть игнорироваться. Должен быть тест на проверку условия игнорирования. Он должен зафейлиться. Соответственно будет 6799 тестов прошедших, 1 проваленный, сообщающий точную ошибку и 200 проигнорированных по той же самой причине. Как итог — смотрим не на 200 одинаково упавших тестов, а на 1.
                                        Конечно, в этом случае должна быть установка, что любой проваленный тест это блокер к выкладке.
                                        • 0
                                          Ах, да. Ненаучная фантастика. Слышал об этом.

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

                                          Кроме того, зависимость юнит-тестов друг от друга — источник проблем.
                                          Даже в вашем примере, в случае какой-нибудь ошибки в тесте который должен был бы упасть у вас будет 6799 прошедших, 201 игнорируемых, и ни малейших намеков на источник проблемы.
                                          • 0
                                            У игнорируемых тестов аналогично упавшим пишется причина игнора. При желании ее всегда можно увидеть.
                                            Мне непонятно еще как возможно, что одна и та же проверка в одном случае пройдет, а в другом — не пройдет. Что это за юнит тесты такие, которые так колбасит?
                                            • 0
                                              У игнорируемых тестов аналогично упавшим пишется причина игнора

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

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

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

                                              Юнит-тест должен падать, если:
                                              1. В результате разработки были внесены непреднамеренные изменения функционирования
                                              2. Если система оказалась в такой среде, где ее корректное функционирование нарушилось
                                              3. Если система изменилась вслед за изменением требований, а тесты еще не отражают это изменение.
                                              • 0
                                                Так упадет же тест один. Чем помогут еще 200 упавших с той же самой ошибкой? Они просто программно пометятся проскипанными. Зачем заставлять разработчика самостоятельно глазами смотреть на одинаково упавшие тесты и для себя их помечать проигнорированными? Я не пойму, в чем понт делать ту работу, которую можно автоматизировать?
                                                • 0
                                                  1. Упавший тест == Некорректный работа одного аспекта.
                                                  Проскипанный тест == неизвестность

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

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

                                                  Нам стоит вернуться в началу.
                                                  Какую задачу нужно решить?
                                                  • 0
                                                    Окей, давайте остановимся на том, что в юнит тестировании это дело выбора каждого, тут матчеры могут здорово упростить жизнь во многих ситуациях, а могут просто не понадобиться. Тем кто не пробовал, стоит попробовать — ничего страшного в них нет, писать их недолго, поддерживать — не сложнее тестов. Во многих случаях они могут просто сыграть роль синтаксического сахара. Многие живут без матчеров и рады. Есть те, которые пишут почти стихи при помощи матчеров, но при этом проверки формальные и строгие.

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

                                                    В любом случае, все assertEquals(1, 2) стоит заменить на assertThat(2, equalTo(1)). Потому как вывод ошибки одинаков, но в случае изменения требований к тесту заменить equalTo() на greaterThanOrEqualTo() или lessThan() будет гораздо дешевле, чем переписывать логику получения значения из 2 и приводить к нужному виду в соответствие с 1. Это тоже считается использованием матчеров
                                                    • 0
                                                      Хотелось бы надеяться что матчеры принесут больше выгоды чем проблем.
                                                      Я внимательно к ним присматриваюсь, т.к. часто код с матчерами оказывается компактнее чем plain java. Но пока, мне кажется, что недостатки перевешивают достоинства.
                                                      Недостатки:
                                                      * Код с матчерами теряет «прозрачность»
                                                      * Утверждения относящиеся к функциональности перетекают из кода тестов в матчеры
                                                      * Увеличивается взаимное влияние тестов друг на друга через матчеры
                                                      * Матчеры не стимулируют разработку удобных интерфейсов между подсистемами

                                                      Из достоинств только то, что код, использующий матчеры, выглядит компактнее.
                                                      • +1
                                                        Вот сслыка на мой комент
                                                        habrahabr.ru/post/174781/#comment_6073323
                                                        Там критикуется код с матчером. Выглядит компактно, зато полный набор недостатков.
                                                        • 0
                                                          a.Verify(Reflect<DateViewModel>.GetProperty<int>(sut => sut.Year));
                                                          


                                                          Честно говоря это вообще бред какой-то. Гомнокод чистой воды. Так матчеры писать нельзя ни при каких обстоятельствах. Брр.
                                                          • 0
                                                            Да, но в то же время, автор этого кода старался изо всех сил — я уверен. А получилось так. :-(
                                                            Почему? Видимо очень трудно забить гвоздь микроскопом.
                  • 0
                    Интересно кто-нибудь исследовал «антипаттерны» использования Hamcrest в модульном тестировании?

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