Pull to refresh

Comments 138

UFO just landed and posted this here

Почему же безосновательно? Чувак потрёт и тестируемый код тоже. С точки зрения код-ревью всё чисто и тесты зелёные. А остальное — (какбэ) вкусовщина.


Имхо, про эту "вкусовщину" и речь в статье. Много встречал тестов тестирующих не полезный функционал, а просто верифицирующих детали реализации. Самый жесткий пример, это тестить if передавая true или false и проверяя, смогёт ли он правильно разветвить программу.

Если тестируемый код стерт, и юнит тест стерт — все хорошо. Код покрыт на 100%.
Если помимо этого «Чувак» напишет новый код — ему придется написать и новый юнит тест — иначе коммит не пройдет, т.к. код покрыт не на 100%.
Покрывать тестами каждую строчку может быть нудно, но чисто в рамках заданной ситуации — покрытый на 100% код действительно что-то мне гарантирует, как минимум выполнение спецификации моих тестов в каждой строчке кода.

Это работает только когда тесты реально отражают спецификацию, а не реализацию.

Тут уже становится непонятно, как мерить процент покрытия, раз тесты покрывают не код реализации, а спецификацию.
с помощью мутационных тестов? :)
Имею мнение, что тесты надо писать на спецификацию. А достигать 100% стоит не методом написания новых тестов, а методом удаления непокрытого кода — какая разница, что там написано, если оно не используется?
Я был достаточно высокого мнения об этом разработчике, чтобы сказать без обиняков

I had enough trust with the developer to bluntly say,


Может, лучше было бы перевести как-то вроде «У меня были достаточно доверительные отношения с этим разработчиком, чтобы сказать без обиняков», потому что, честно говоря, «высокое мнение» сбило с толку при чтении.

Такой вариант перевода тоже рассматривали, но склонились «высокому мнению». Если это сбивает с толку, лучше исправим.

Почему вы склонились к высокому мнению, когда никакого высокого мнения там нет?

В целом, с посылом статьи я согласен, и мне кажется уже изначально некорректно требовать 100% покрытия кода именно от юнит-тестов, ведь не каждая строка имеет одинаковую ценность и смысл.

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

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

Но, даже если мы напишем более релевантные тесты для таких случаев, задействуя альтернативные инструменты, то может возникнуть другой вопрос — как совместить по разным инструментам задачу подсчета покрытия кода (и зачем)? Насчет первого — тут я даже не уверен, делает ли так кто либо… суммируют ли E2E+Integration+Unit coverage? Думаю, что да, но реже, чем стоило бы.

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

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

А это реальная ведь ситуация. За примерами далеко ходить не надо, если взять фронт-энд разработку — как насчет затерянного одинокого экзотического обработчика события, срабатывающего только для мобильного браузера Windows Phone 7? Можно понять человека, отказавшегося покрывать это тестами и проверившего обработчик пару раз вручную на живом телефоне.

Я думаю, что если бы написать такой экзотический тест было бы не дольше 20-30 минут и 10-20 строчек кода, то грешно было бы не написать. Но если же это грозит такими радужными перспективами как писать разные костыли, прикручивать друг к другу проекты номинально несовместимые или малоподдерживаемые, то ради чего?

В любом случае, мы здесь выиграем в том, что выявим недостатки в существующих инструментах тестирования и потенциально сможем добиться их улучшения (если это того стоит, ха-ха, ибо пример с WP7, пожалуй, слишком «суров»). А если мы не захотим или не сможем этого сделать, то как минимум у нас в проекте засчет интегрального такого покрытия будет всегда актуальная обновляемая карта «белых пятен на карте», перепись эдаких тихих омутов с чертями, что позволит держать их популяцию под хоть каким-то, но контролем.
Задача юнит тестов не заключается в том чтобы тестировать итоговую работу приложения. Юнит тесты и выполняют возложенную на них задачу — тестировать отдельно каждую строчку кода как API. Они дают уверенность, что если приложение работает неправильно, то хотя бы кирпичики из которых оно состоит — надежны, и ошибку следует искать на более высоком уровне абстракции.

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

не каждую строчку кода, а каждый объект. Строчка всегда и везде делает ровно то, что в ней написано. Но вот выдрать объект из контекста и проверить его работоспособность может быть очень полезным, чтобы переструктурировать код, уменьшить связанность, увеличить зацепление, проверить что объект может быть повторно использован без лишних плясок с бубнами. Поиск ошибок в юниттестах, имхо, всего лишь приятный побочный эффект.
UFO just landed and posted this here
«Это совершенно прозрачный код. В нём ничего такого нет: ни атрибутов conditional, ни циклов, no трансформаций. Просто обычный кусок связующего кода».

Странно это читать от человека, который чуть выше говорит о «tests first». Мы пишем тесты не зная реализации. И делаем это в том числе и для того, чтобы появление ошибочных «атрибутов conditional, циклов, трансформаций» было контролируемо.

Вот вы молодцы, вы инкапсуляцию не нарушаете. А мы тестим именно реализацию, потому что так сказал насяльника. Хорошие программисты как-то интуитивно держат баланс между гибкостью и формализмом, а неопытные просто добивают "покрытие" до 100% и идут спать. К ним статья, по-моему, и обращается.

А мы тестим именно реализацию, потому что так сказал насяльника.

Интересно, как он сказал это? Непокрытый код значит, что требование, которое он рализует непокрыто. Просто это сигнал к тестированию требований.

Очень просто, как всегда и делают, чтобы запороть проект: сказал нужно 100% покрытие, но БЫСТРО! То есть не вдумчивого тестирования требовал, где покрытие, это только вспомогательный сигнал, а просто числа процентов покрытия равного 100. И быстро :)

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

Совершенно согласен. Ладно бы поддерживать хорошие тесты, а они же именно что непонятные.

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


Стоп. Если известно, что в методе должно обязательно быть обращение, например, к базе данных, (например, подключение) то логично это и протестировать в тестах — было оно или нет (передав, к примеру, тем или иным образом (через параметр или инверсию — тут не расматриваем), мок объекта-подключения к базе, в этот тестируемый метод)!

Если это не тестировать, то как узнать — был ли в тестируемом методе вызван метод подключения к базе или нет?

Если и это нам не надо узнавать — то вопрос — что же мы должны в этом случае тестировать? — Тестировать метод как «чёрный ящик»? — но в методе происходит подключение к базе, к примеру — что же мы можем оттестировать тогда такое в этом тестируемом нами методе?

Если это не узнавать — то что именно тестировать в тестируемом методе, где происходит подключение к базе, к примеру?

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

Но и в этом случае, к примеру, нам известно, что при таких то значениях параметров метод не обращается во вне (к базе, инету, объекту ...), а при других значениях должен обязательно обратиться. — Что в этом случае тестировать, если мы не знаем реализации?

Тут вопрос такой — что есть такое «реализация» методом конкретного интерфейса?, к примеру — это «чёрный ящик» или реализация методом «спецификации».

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

Если он не прав — то скажите мне, что вы тогда можете оттестировать в таком вот тестируемом методе («не чистой функции», грубо выражаясь)?

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

Если метод загружает данные из БД, то у правильного теста входные данные — это то, что лежит в БД (или в ее моке), а выход — то, что возвращает метод. Это — тест спецификации.


У корявого теста входа нет, а выход — это последовательность обращений к БД, которую нельзя нарушать. Это — тест реализации.

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

А если в такт статье, то мой ответ: «...».
mayorovp
Если метод загружает данные из БД, то у правильного теста входные данные — это то, что лежит в БД (или в ее моке), а выход — то, что возвращает метод. Это — тест спецификации.

СТОП СТОП

Мы тестируем метод, в котором используется, к примеру, объект сервиса (база ли это, или иное — не важно).
Если этот сервис просто возвращает что-то и мы тестируем метод который возвращает что-то основанное на возврате этого сервиса - то тут вы правы.

Но часто у нас есть такие методы, например:

Начало Метода

  • Соединиться с сервисом и получить от него токен.
  • Послать сервису данные, используя этот токен.
  • Закрыть сервис.

Конец Метода.


Как вы собираетесь тестировать такой Метод без проверки что конкретно дёргали у мок-сервиса и в какой последовательности? — а был ли вызван метод у мок-сервиса о получении токена?, а был был ли вызван метод у мок-сервиса о передаче и что было передано?, а был ли вызван метод у мок-сервиса о закрытии сервиса?

Или далее, стандартный вывод в лог (в файл и т.п.):

Начало Метода (параметр)

  • вывести в лог параметр (loger.log(параметр) )

Конец Метода.


Как вы оттестируете этот Метод, без тестирования того, что в этом Методе в этом случае (при этом значении параметра, к примеру) должно быть логирование, то есть должен быть вызван метод вывода в лог (метод log(...) мок-логера)?

И реальных примеров методов где есть вывод инфы (в файл, в лог, в консоль, редирект и прочее) полно в коде встречается. — Как тестировать такие методы без проверки вызова методов мок-объекта того или иного вида сервиса вывода инфы.

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

Вы считаете это было бы лучше чем просто создать обычный мок-сервис по имеющемуся у нас файлу интерфейса этого сервиса и проверить при тестирование Метода (внутри которого используется этот сервис), а был ли вызван метод у этого мок-сервиса и с каким значение параметра?

P.S.
JPEG
Код, настолько неотделимый от базы данных приходится локализовывать и тестить вместе с базой.

Называйте это базой или сервисом, сервисом вывода в консоль, в файл, в лог, редирект и прочее — то есть сервисом, который внутри тестируемого Метода «выводит инфу» (или обращается за установкой соединения, к примеру) во вне!

Как оттестировать такой Метод? Как оттестировать такой Метод не запросив (в написанном вами коде теста) — а был ли вызван метод у мока этого сервиса?

Легко попасть в такие условия, когда уже ни вверх, ни вниз. Опытный скалолаз тем и отличается от мёртвого, что не загоняет себя в такие ситуации. Об этом, имхо, и статья.

Пожалуйста, используйте меньше жирного. Ну невозможно же нормально читать текст в пятнах...


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


Но при этом, к примеру, сам факт соединения с сервисом — это уже делать реализации! Тест не должен падать, к примеру, если не было сделано ни одного вызова к сервису (при условии выполнения остальных условий корректности).


Вывод в лог же у бизнес-методов тестировать не нужно совсем.

Передать целостную инмемори реализацию сервиса.


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

Начало Метода

Конец Метода.

Заметим еще что вы думаете тестированием реализации, попробуйте думать интерфейсом — какие требования к интерфейсу метода? И по ним построить тесты.

ApeCoder
Заметим еще что вы думаете тестированием реализации, попробуйте думать интерфейсом — какие требования к интерфейсу метода? И по ним построить тесты.


Пример 1:

Метод сменить_имя(сервис, номер_записи, новое_имя)

    сервис.сменить_имя_записи(номер_записи, новое_имя);

Конец Метода.


Мой вариант метода тестирования:

Метод тестировать_сменить_имя ()

    создать мок-сервис; 

    Правило проверки выполнения теста:
    Если был вызов мок-сервис.сменить_имя_записи() и параметры были
    равны: (номер_записи = 789, новое_имя = "Иванов" ), то ok
    иначе тест провален

    вызвать тестируемый метод:
          сменить_имя (мок-сервис, 789, "Иванов");

Конец Метода.


Ваш вариант этого теста?

P.S.
mayorovp
Если метод загружает данные из БД, то у правильного теста входные данные — это то, что лежит в БД (или в ее моке), а выход — то, что возвращает метод. Это — тест спецификации.

Если не тестировать вызов метода у мок-сервиса, то, к примеру

Пример 1:

Метод получить_имя (сервис, номер_записи)

    имя = сервис.найти_имя_записи (номер_записи);
    return имя;

Конец Метода.


Ваш, как я понял, вариант метода тестирования будет примерно таким:

Метод тестировать_получить_имя()

    создать мок-сервис;
    настроить мок-сервис:if вызван метод мок-сервис.найти_имя_записи(456) 
                         вернуть "Василий"

    Правило проверки выполнения теста:
    Если вызов получить_имя(мок-сервис, 456) возвращает "Василий", то ok
    иначе тест провален.

    вызвать тестируемый метод:
          получить_имя(мок-сервис, 456);

Конец Метода.


Всё как-бы нормально, вы не проверяете — был ли вызван у мок-сервис метод.

Но тогда и для такого тестируемого метода ваши тесты пройдут нормально:
Метод получить_имя (сервис, номер_записи)

    return "Василий";

Конец Метода.

Что конечно же будет неверным.

Такие методы нет необходимости не только тестировать — но даже писать. Это касается обоих примеров.


Что же касается трюка с return "Василий" — то код-ревью не просто так придумали.

[Test]
void changeName_ShouldSetNewName()
    var service = new InMemoryService();
    service.addRecord(recordId, OldName);
    subject.changeName(service, recordID, NewName);
    service.getName(recordID).should().be(NewName);
}

Заметьте, что здесь мы абстрагируемся от того, каким именно споcобом vмы работаем с сервисом. Может быть, в будущем появится какой-то еще метод для более удобной работы с именем и от этого ничего ровно не изменится — требование сформулировано в абстрактной форме.


Но, так как метод не вносит вообще никакой собственной ценности. Я бы просто сделал inline method :)

Но тогда и для такого тестируемого метода ваши тесты пройдут нормально:

По классическому TDD мы и должны сначала написать такой метод. А потом написать красный тест.

ApeCoder
Заметьте, что здесь мы абстрагируемся от того, каким именно споcобом vмы работаем с сервисом
Вы привели тест который ничего не тестирует вовсе. Ваш код просто создаёт мок-сервис и вызывает его методы

Выше я приводил примере методов которые надо оттестировать — в этих методах вызываются сервисы (по крайней мере один). Нам нужны тесты этих методов, а не тесты сервисов в них используемых.

Например в этом методе вызывается один раз метод одного сервиса:

Метод сменить_имя(сервис, номер_записи, новое_имя)

    сервис.сменить_имя_записи(номер_записи, новое_имя);

Конец Метода.



А вот пример метода в котором вызываются методы у двух сервисов (кэш и сервис):
Метод получить_имя (кэш, сервис, номер_записи)

    имя = кэш.найти_имя_записи (номер_записи);
    if (имя == null) {
       имя = сервис.найти_имя_записи (номер_записи);
    }
    return имя;

Конец Метода.


Я считаю, что в тестировании методов внутри которых вызываются методы сервисов вида:
  • get
  • put
  • post
  • update
  • delete
  • find

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

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

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



ApeCoder
По классическому TDD мы и должны сначала написать такой метод. А потом написать красный тест.

Ок.
Вначале пишем наш тестовый метод:
Метод тестировать_получить_имя()

    создать мок-сервис;
    настроить мок-сервис:if вызван метод мок-сервис.найти_имя_записи(456) 
                         вернуть "Василий"

    Правило проверки выполнения теста:
    Если вызов получить_имя(мок-сервис, 456) возвращает "Василий", то ok
    иначе тест провален.

    вызвать тестируемый метод:
          получить_имя(мок-сервис, 456);

Конец Метода.


Потом пишем наш метод:
Метод получить_имя (сервис, номер_записи)

    //TODO
    return "Иван";

Конец Метода.

Ок. Наш тестовый метод — «тестировать_получить_имя()» — теперь красный.

Далее пишем код нашего метода:

Метод получить_имя (сервис, номер_записи)

    имя = сервис.найти_имя_записи (номер_записи);
    return "Василий";

Конец Метода.

Всё — тест проходит — но это неправильно!


mayorovp
Такие методы нет необходимости не только тестировать — но даже писать. Это касается обоих примеров.

Наверное вы правы. Но, понимаете, в реальном мире не все методы есть «чистые функции».

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

mayorovp
Что же касается трюка с return «Василий» — то код-ревью не просто так придумали.

Вы, надеюсь, в этом месте пошутили.

А аргументы у вас будут?

Вы привели тест который ничего не тестирует вовсе. Ваш код просто создаёт мок-сервис и вызывает его методы

Нет. subject.changeName вызывает метод SUT


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

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

Можем. Надо думать требованиями а не реализацией.


Всё — тест проходит — но это неправильно!

  1. Если неправильно, надо надо повторить Red-Green-Refactor
  2. Надо писать простейшую реализацию сначала. Я бы сначала проверил поведение при отсутствии человека в базе

Прочитайте, пожалуйста книжку про юнит тесты. Например http://xunittestpatterns.com

ApeCoder
Нет. subject.changeName вызывает метод SUT
Точно, не заметил. Виноват.

Итак:

[Test]
void changeName_ShouldSetNewName()
    var service = new InMemoryService();
    service.addRecord(recordId, OldName);

    subject.changeName(service, recordID, NewName);

    service.getName(recordID).should().be(NewName);
}


Вы создаёте мок-сервис как полноценный класс (файл на диске) методы которого вы должны наверняка также тестировать — ибо ваш объект InMemoryService должен довольно таки хоть и «просто» но имитировать реальный сервис.

Тогда, что будет при изменении интерфейса сервиса?
Вы пишите:
Может быть, в будущем появится какой-то еще метод для более удобной работы с именем и от этого ничего ровно не изменится — требование сформулировано в абстрактной форме.
Рассмотрим моё поведение и ваше поведение в этом случае.

Моё поведение:
  • Произошло изменение интерфейса сервиса. Тесты упали. (Красные)
  • Смотрите упавшие тесты (их может быть много, ибо методов где используется сервис может быть много)
  • Правлю код в самом методе.
  • Правлю код в методе теста (ибо там происходит создание мок-сервиса по имеющемуся файлу интерфейса сервиса, сам же мок-сервис не имеет никакого файла имплементации (на диске) вовсе).
  • Тесты работают (Зелёные)


Ваше поведение:
  • Произошло изменение интерфейса сервиса. Тесты упали. (Красные)
  • Смотрите упавшие тесты (их может быть много, ибо методов где используется сервис может быть много)
  • Правите код в самом методе.
  • Правите код в вашем мок-сервисе.
  • Правите код в тестах для вашего мок-сервиса.
  • Тесты работают (Зелёные)


Да, благодаря "обвязки" вашего мок-сервиса — то есть имплементации в вашем мок-сервисе не только реального интерфейса сервиса, но и методов в сервисе интерфейса вовсе не присутствующих (service.addRecord(...), service.getName(...) и других необходимых вам только и только в ваших тестовых методах) вам возможно не потребуется менять код в ваших тестовых методах — но вам всё равно придётся туда (в тестовые методы) посмотреть когда они станут красные.

Да, мне придётся не только, как и вам, глянуть в тестовые методы когда они упадут, но и поправить код в них.

Но мне не надо будет никак (даже типа «просто»):
  • имплементировать сам сервис, создавая не такой уж простой файл мок-сервиса
  • и, возможно (это зависит от сложности сервиса и желаемой надёжности кода этого мок-сервиса), писать тесты для методов этого «простого» мок-сервиса.


Нет, я выбираю обычный путь:
  • Правлю код в самом методе.
  • Правлю код в методе теста
  • Не пишу вовсе код мок-сервиса и тесты для этого кода.


По мне — это более надёжнее чем ваш путь.



mayorovp
А аргументы у вас будут?

Да, аргумент простой — там где можно код-ревью заменить тестами, там надо использовать тесты.

Да, аргумент простой — там где можно код-ревью заменить тестами, там надо использовать тесты.

Как вы в таком случае предлагаете тестировать тот факт, что сотрудник-саботажник не удалил "красный" тест из репозитория с концами? :-)

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

Вот только введение усложнений в тестах против трюков вида return "Василий" не упрощает ревью, а только усложняет.

Это не трюк, это просто тест вам показал что покрытия недостаточно. Требует еще одного цикла Red-Green-Refactor. Правда автор начал со слишком сложного теста. Я бы сначала протестил самую простую ситуацию — такого просто нет в базе, а потом уже Василия.


В класических техниках TDD предлагается именно начинать с Василия.


Посмотрите например эту ката (RomainNumbers)

Я не против "начинать с Василия" в TDD. Я против писать проверку что метод сервиса был вызван только ради того чтобы недопустить появления Василия в будущем коде.

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

Вы создаёте мок-сервис как полноценный класс (файл на диске) методы которого вы должны наверняка также тестировать — ибо ваш объект InMemoryService должен довольно таки хоть и «просто» но имитировать реальный сервис.

Так у вас создается тоже — только ограниченная абстракция. То есть ваш вариант не проверяет требование "если мне передали сервис, я изменю имя" а требование "если мне передали сервис, я вызову changeName" что является более узким требованиями.


То есть например, вдруг ваш сервис получит какой-то другой метод. например changeEmployeeProperty и для оптимизации надо будет использовать его — у вас упадут все тесты, которые использовали changeName, у меня надо будет только добваить метод в класс.


но вам всё равно придётся туда (в тестовые методы) посмотреть когда они станут красные

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


имплементировать сам сервис, создавая не такой уж простой файл мок-сервиса

Будет то же самое только размазанное по rкоду тестов


и, возможно (это зависит от сложности сервиса и желаемой надёжности кода этого мок-сервиса), писать тесты для методов этого «простого» мок-сервиса.

У вас тоже самое только вы никак не тестируете код мока. Есть большая разница, находится ли он внутри класса или размазан по методам класса?


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


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

ApeCoder
То есть например, вдруг ваш сервис получит какой-то другой метод. например changeEmployeeProperty и для оптимизации надо будет использовать его — у вас упадут все тесты, которые использовали changeName, у меня надо будет только добваить метод в класс.


Нет, не так. Допустим обновили IService, добавив в него метод changeEmployeeProperty (метод changeName остался неизменным — его не меняли и не удаляли из IService ).

Это у вас упадут все тесты, ибо вам надо будет по крайней мере на первый раз реализовать в вашем файле мок-сервиса хоть заглушку под этот добавленный метод changeEmployeeProperty).

В моём случае, мок-сервис создаётся только и только в тестовом методе, типа:

IService mockService= createMockService(IService);


И тут не важно, что в интерфейс IService был добавлен новый метод changeEmployeeProperty — так как пока я его вообще не вызываю в своих тестах.

createMockService(IService) — создаёт мне Mock-объект на лету (и только в тестах), принимая любой интерфейс как параметр (сейчас и class, а не только interface, но это не имеет тут значения).

Мои тесты упадут тогда когда будет изменён(или удалён) метод changeName из интерфейса сервиса (IService) — но тогда упадут и ваши тесты аналогично.

У вас тоже самое только вы никак не тестируете код мока. Есть большая разница, находится ли он внутри класса или размазан по методам класса?


Есть. «Размазки» у меня нет, ибо и у вас есть методы «подготовки сервиса» и «проверки что же там (в объекте мок-сервиса) произошло после вызова тестового метода:

service.addRecord(recordId, OldName);
...
service.getName(recordID).should().be(NewName);


Аналогично и у меня:
настроить мок-сервис:if вызван метод мок-сервис.найти_имя_записи(456) 
                         вернуть "Василий"

если вызов получить_имя(мок-сервис, 456) возвращает "Василий", то ok
    иначе тест провален.



Так что никакой „размазки“ по тестам кода моего мок-сервиса (по сравнению с вами) нет.

Единственное отличии — я всегда явно указываю конкретный метод мок-сервиса, который должен быть вызван (или не вызван) в тестируемом методе.

Ваш тест фактически некорректен — в предусловии метода сказано „передайте мне IService“, а вы ему передаете нечто похожее, только реализующее ровно один метод.


Нет, не так — код, типа
IService mockService = createMockService(IService);

возвращает мне объект mockService в котором реализованы все методы интерфейса IService — а уж какие я проверяю на вызов — это я решаю сам в тестах.




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




mayorovp
Как вы в таком случае предлагаете тестировать тот факт, что сотрудник-саботажник не удалил „красный“ тест из репозитория с концами? :-)

ApeCoder
Василий может быть неумышленный.
Верно. Я не рассматриваю как противостоять саботажу и вредительству. Я о том, что „ошибка“ может быть неумышленная и если для её обнаружения можно (достаточно) написать тест, то лучше написать тест, а не надеяться в этом случае на коде-ревью.
Нет, не так. Допустим обновили IService, добавив в него метод changeEmployeeProperty (метод changeName остался неизменным — его не меняли и не удаляли из IService ).
Это у вас упадут все тесты,

Не упадут. На этапе добавления метода IDE скажет, что он не реалтзован, и предложит сгенерировать заглушку с NotImplemented exception.


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


Потому, что фактически у вас метод просит IService, а вы ему передаете более ограниченный вариант сервиса (условно INameChanger), но называете его IService. Фактически у вас наружение LSP — из-за этого у вас тест тестирует реализацию (какиме именно сетоды сервиса вызывать — это интимные подробности реализации).


Когда я вижу развесистое дерево вручную написанных файлов для реализации различных мок-объектов

Так у вас он тоже вручную написан, только
1) Не до конца
2) Средствами Reflection — то есть IDE не будет поддерживать контроль целостности и удобство работы в той же мере, что и с классами написанными без Reflection

http://blog.cleancoder.com/uncle-bob/2017/05/05/TestDefinitions.html


I, for example, seldom use a mocking tool. When I need a mock (or, rather, a Test Double) I write it myself. It’s not very hard to write test doubles. My IDE helps me a lot with that. What’s more, writing the Test Double myself encourages me not to write tests with Test Doubles, unless it is really necessary. Instead of using Test Doubles, I back away a bit from micro-testing, and write tests that are a bit closer to functional tests. This too helps me to decouple the tests from the internals of the production code.
ApeCoder >
Потому, что фактически у вас метод просит IService, а вы ему передаете более ограниченный вариант сервиса (условно INameChanger), но называете его IService. Фактически у вас наружение LSP — из-за этого у вас тест тестирует реализацию (какиме именно сетоды сервиса вызывать — это интимные подробности реализации).
На мой взгляд — создавая вручную файл мок-сервиса вы фактически и делаете реализацию IService. И вам её нужно будет и сопровождать и возможно и тесты писать для неё же, то есть тестировать и сами ваши моки!

Я никак в тестовых методах не выполняю имплементацию IService.
Я просто настраиваю мок IService — то есть указываю, что при обращении к такому-то методу IService верни то-то и то-то.

В данном случае я -, находясь в тестовом методе, не выходя из него никак, являюсь чисто «кукловодом» — дёргая за «ниточки» IService.

Я думаю это всё же лучше чем явное развесистое дерево исходных файлов реализации(имплементации) мок-сервисов в проекте.

I, for example, seldom use a mocking tool. When I need a mock (or, rather, a Test Double) I write it myself. It’s not very hard to write test doubles. My IDE helps me a lot with that. What’s more, writing the Test Double myself encourages me not to write tests with Test Doubles, unless it is really necessary. Instead of using Test Doubles, I back away a bit from micro-testing, and write tests that are a bit closer to functional tests. This too helps me to decouple the tests from the internals of the production code.
Он понимает и фактически признаётся что он не пишет тесты как таковые — он их называет «micro-testing» — то есть тесты для тестирования одного метода и только одного метода. Он фактически решил (почему я не знаю) писать сам что-то иное, которому он пока не подобрал слов и о чём он пишет как о «write tests that are a bit closer to functional tests».

Но «functional tests» — это всё же иное чем простой тест на тестирование одного и только одного метода.

Имхо, конечно, имхо. (С)
Я просто настраиваю мок IService — то есть указываю, что при обращении к такому-то методу IService верни то-то и то-то.

Я тоже настраиваю C# своим файликом. Или вы считаете что int x(return y); чем-то отличается от x=>y?

ApeCoder
Я тоже настраиваю C# своим файликом. Или вы считаете что int x(return y); чем-то отличается от x=>y?
Конечно. Вам надо писать файл. Перекладывать сообщения в нём. Писать методы получения параметров в нём. Возможно вам надоест писать однообразный код и вы начнёте наследоваться от базового файла ваших мок-текстов.

Кто-то начнёт «улучшать» ваши мок-файлы. Кто-то начнёт добавлять некую специфическую логику в эти уже общие мок-тесты. С благой целью, конечно.

Потом кто-то решит переиспользовать вами написанные мок-сервисы. Ну, подправит, возможно, для себя.

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

Всё это чревато. Имхо.

Остаётся вопрос — оно того стоит? Выращивание развесистого дерева мок-сервисов?

Я думаю… каждый решит сам. :-)

Я же решил, что тестируя метод, я пишу код, который зависит только и только от имплементации тестируемого метода и моего тестового кода в моём тестовом методе, который не зависит ни от какой имплементации мок-сервиса во вне. — Только я, тестируемый метод, строки теста и… mocking tool.
Конечно. Вам надо писать файл. Перекладывать сообщения в нём. Писать методы получения параметров в нём. Возможно вам надоест писать однообразный код и вы начнёте наследоваться от базового файла ваших мок-текстов.

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


Кто-то начнёт «улучшать» ваши мок-файлы. Кто-то начнёт добавлять некую специфическую логику в эти уже общие мок-тесты. С благой целью, конечно.

Не будет — там должна быть одна логика — полная InMemory реализация интерфейса


Кому-то придёт в голову разумная идея написать тесты и для вашей простой, но всё же, имплементации мок-сервисов.

Всё это чревато. Имхо.

Я уже приводил ссылку, что некоторые советуют так делать. Более того, писать абстрактный тест на интерфейс и два наскледника — для реальзной реализации и для тестовой.


Дерево файлов ваших мок-тестов начнёт расти.

Мок-реализаций. И это не дерево.


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

Use the front door first — не надо зависеть от реализации, тесты должны зависеть от требований. Почитайте Мезароса.


The types of interfaces we use has an influence on the robustness of our tests. The use of Back Door Manipulation (page X) to set up the fixture or verify the expected outcome or a test can result in Overcoupled Software (see Fragile Test on page X) that needs more frequent test maintenance. Overuse of Behavior Verification (page X) and Mock Objects (page X) can result in Overspecified Software (see Fragile Test) and tests that are more brittle and that may discourage developers from doing desirable refactorings.

ApeCoder
У них не может быть базового класса, потому, что все они разные — так как для разных интерфейсов.
… Мок-реализаций. И это не дерево.
Эка право — сервисы могут и есть очень разные, типа:

public interface IPageTypeService extends IGenericDictionaryService<Long, PageType, IPageTypeDAO>


extends — то есть пишем предка. Отсюда и дерево мок-сервисов растёт.

Use the front door first — не надо зависеть от реализации, тесты должны зависеть от требований.
С этим никто не спорит. Но выбор вместо mocking tool каких то странных самописных мок-сервисов с какой-то странной целью типа реализовать functional tests — хм, нет — мы обсудили всё выше — там нет ничего такого чтобы было хорошо. Там есть и чего плохого.

И главный вопрос — зачем это? — так и остался не отвеченным.
extends — то есть пишем предка. Отсюда и дерево мок-сервисов растёт

Принимается — интерфейсы представляют дерево => симуляторы представляют дерево.


зачем это?

Вы не используете front door — на front door написано "предоставьте мне IService и тогда я буду работать" а вы предоставляете подмножество IService.


Ваши тесты выражают не требования а реализацию — какие методы вызываются.


Дальше будут fragile overspecified tests.

UFO just landed and posted this here
Странно, в заголовок вынесено стопроцентное покрытие, а приведенные примеры — один про человека, который поставил себе целью вовсе не покрытие, а использование москиты, а второй, похоже, просто перся от того, что наваял огромную портянку кода. При чем тут стопроцентное покрытие кода тестами, когда банально два оболтуса занимались черте чем?

А что, по вашему написание теста для первого примера кода, без использования mockito, будет более полезным? Даже не так — оно вообще будет полезным? Попытка использовать негодные инструменты тут вторична, а первичной является идея тестировать то, что тестировать не нужно.


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


Если у вас есть убежденность, основанная на вашем знании устройства и анализе кода, что этот код правильный — это тоже может быть вполне достаточной причиной (может быть, но может и нет!), чтобы этот кусок кода не тестировать. Хотя бы потому, что ресурсы ваши всегда ограничены. Вы либо пишете тесты, либо делаете что-то другое, возможно более полезное. Либо обеспечиваете 100% покрытие, либо выпускаете новые функции, либо рефакторите код, делая его более простым и надежным by design.


А представьте, что дело обстоит так: вы выпустили релиз 1.0 своего продукта. Пользователи его применяют, прибыль растет, все довольны, багов мало или вовсе нет. А покрытие тестами скажем 10%. Вам все еще нужно 100% покрытие? А нафига, если потребитель доволен?

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

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


Некоторые вообще никогда не меняются, будучи написанными. Некоторые имеют сложную логику, а некоторые — рутинные и тривиальные. И подходить к их тестированию нужно соответственно- прилагая голову. И я этот пост понимаю именно так.

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

«Или какие-то фичи теряют актуальность — и код для них можно выбросить, вместе с тестами. „
Это некоторый необходимый оверхед от тестов

“Ещё бывает так, что для какой-то фичи написан плохой код, и надо бы его переписать, тут-то нам тесты и могли бы помочь.»
Да для рефакторинга очень подходит, я участвовал во многих проектах где был плохо написанный фунционал и его боялись править, с тестами такого бы не было.

«Если бы были написаны хорошо, но наивно рассчитывать, что разработчик написал плохой код, но хорошие тесты для него. Напрашивается вывод, что не очень-то и нужны такие тесты.»
Эта мысль не очень ясна,
тесты нужны не для того чтобы писать хороший код (есть побочный эффект тестирования, выраженный в том, что в коде появляются хорошие и расширяемые интерфейсы взаимодействия между различными частями программы, но это не значит что код будет красиво и эффективно написан). И если тесты есть, они уже полезны. А умение писать хорошие тесты приходит после нескольких переделываний кучи плохих тестов (на моей практике это тесты со сложной логикой и неправильное взаимодействие между частями функционала(много лишней инициализации)) после изменения функционала в одном и том же месте.

Случаи где я считаю что не нужно писать тесты — это там где нет требования к высокому качеству кода (прототипы, говносайты, обучение) и/или разработчик только один или несколько полностью изолированных. В остальных случаях тесты пригодятся и уменьшат многодневные разборы багов практически до нуля.
100% покрытие тестами я не пробовал, тестирую только сложные/вычислительные части кода или там где возникает много багов.
А умение писать хорошие тесты приходит после нескольких переделываний кучи плохих тестов


Умение писать хороший код приходит после того как достаточно поработаешь с плохим кодом. К некоторым разработчикам это умение приходит, а некоторые так и остаются говнокодерами. От плохого кода тесты не защищают, хороший код и так хороший. На кой вообще сдались эти юниты?
Ещё раз, тесты нужны чтобы гарантировать, что весь функционал внедрённый ранее работает по-старому это не связано с хорошим или плохим кодом.
Плюс к этому не надо верить в миф о том что «есть люди которые всегда пишут хороший код». Программист может не выспаться, напиться/накуриться, его может бросить жена и.т.д., то есть он может находиться в изменённом/нестабильном состоянии сознания абсолютно в любой момент времени. И в такие моменты он вряд ли будет писать хороший код, а учитывая оптимизм разработчиков, даже осознавая проблему изменённого сознания они скорее всего продолжат работу.

Также есть такой момент, что программисты одинаковых категорий («сеньоры», например) могут придерживаться разных взглядов на парадигмы программирования, в разной степени глубины знать инструменты и их особенности (кто-то программируя на JS знает что {} + [] == 0, а кто-то нет и для написания хорошего поддерживаемого кода это знать необязательно) и.т.п. И когда один из них написал кусок кода, а другой туда вносит изменения, то с отсутствием тестов ему придётся анализировать большое количество кода написанного ранее и нет никакого гарантированного способа (кроме тестов, пока что) что внеся изменения он не сломает предыдущий функционал. По сути своим опытом и временем потраченным на внесение изменений он только уменьшить шансы на поломку.

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

И ещё один случай опишу:
я писал через TDD универсальный автономный парсер RSS лент. Написал всё работает, все довольны. Через некоторое время выясняется что в некоторых случаях определяется неправильная кодировка и в базу пишутся кракозябры. После 5 минутного анализа выясняется что я брал данные о кодировке из заголовков HTTP (так было быстрее и удобнее), а на 30% RSS веб сервер отдавал одну кодировку, а по факту была другая, а она ещё внутри прописана (а может быть и не прописана, там всё паршиво с однообразием по-факту =) ). Ну и я радостный написал пару строк кода и исправил проблему, запустил тесты и часть тестов сломалось, тоже мелкая и понятная проблема, новая правка, часть других тестов сломалось и.т.д. Надёжно исправить я смог только с 6го раза + я написал несколько новых тест кейсов для ситуаций где кодировки заголовков и контента не совпадают.
А про плохие тесты:
они влияют на то что при простых изменениях в коде приходиться править много тестов. То есть увеличивает (иногда существенно) время внесения исправлений, потому тестирование и не «взлетает» в большинстве команд.
Вы исходите из ошибочного предположения что продукт написан и не изменяется. Если продукт развивается, то в нём появляются изменения которые как раз и могут ломать всё что угодно и тесты предназначены именно для этого, чтобы дать гарантию того что после внесения изменений весь предыдущий функционал продолжает работать как надо.

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

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


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


100% покрытие вряд ли практично (интересно было бы посмотреть на достаточно большой практичный пример).

Ничего я не постулирую. Я лиш описываю один из вариантов — заказной софт, под конкретного заказчика, который сидит в этой же компании. Разработчик вполне может (и знает реально), доволен ли потребитель.


100% покрытие вряд ли практично

Так а я о чем? Я в основном именно про то, что это непрактично в первую очередь. Даже если вы точно собираетесь рефакторить, и вам нужны тесты как показатель того, что вы не сломали — редко кто рефакторит весь код целиком.

редко кто рефакторит весь код целиком

Часто рефакторинг делается просто как часть начальной разработки — просто в процессе. Разработка фичи как серия рефакторингов в добавлением функциональности. И тут тесты могут помочь уже в процессе начальной разработки фичи.

Я не из мира джавы (явы), но уже давно пришел к личному выводу, что имеет смысл писать больше высокоуровневых тестов, а юнит-тесты писать только когда логика там совсем заковыристая и нужен пруф, что она работает или в случае, когда ручное тестирование занимает больше времени, чем написание тестов. Экономит приличное кол-во времени, позволяя сосредоточиться на чем-то более важном, чем мифическое 100% покрытие, которое само по себе ничего не дает — все равно когда-нибудь рухнет в том месте, где вообще не ожидаешь и тогда, когда вообще не ожидаешь. Но это имхо, конечно.
А что будет, когда жахнет едрёна бомба? Помогут ли юнит-тесты?

Дали доступ к коду разгильдяю-саботажнику, который удаляет тесты, потому что они не работают и виноваты тесты?

Это из серии, что инкапсуляция призвана защищать от злоумышленников.

Помешались все на этих unit-тестах. Половина unit-тестов заменяется assert-ом в нужном месте.

Ассерт неудобен тем, что выстреливает только в момент выполнения участка кода.
Помешались все на этих unit-тестах. Половина unit-тестов заменяется assert-ом в нужном месте.

А вторая половина зменяется code-style валидаторами
«Послушай, пусть даже этот воображаемый недотёпа или злодей действительно придёт и всё испортит — как ты думаешь, что он сделает, когда соответствующий юнит-тест провалится? Удалит, и все дела».
Нельзя так просто взять и удалить неработающий тест, если код остался.
Этот воображаемый злодей может с тем же успехом переписать существующую миграцию, а потом ресетнуть бд на проде, чтобы получилось её накатить заново.

100% покрытие кода тестами не гарантирует правильное поведение кода. Покрытие конкретной строки кода говорит лишь о том, что строка была исполнена, но ничего не говорит о правильности исполнения.
Правильное поведение кода гарантирует мутационное тестирование.

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

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

Тесты вообще ничего не гарантируют на 100%. Простой пример — в той же Java и других языках с исключениями, когда одна строка кода выполнена, то дальше управление перейдет либо к следующей строке (не учитываем пока if и пр.), либо неизвестно куда, потому что в теории при выполнении может быть выброшено произвольное (хотя и конечное) число разных исключений.


Полное покрытие — это значит, что вы вообще говоря, все эти случаи протестировали. Т.е. не просто каждая строка кода была выполнена, но и все пути переходов тоже.


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

UFO just landed and posted this here
Мой ответ: «…»

Вот и не жалко же людям свое время тратить на ерунду? Лучше бы функциональность новую разрабатывали.


P.S. Я все равно не верю, что все ошибки. Потому что статический анализ неполноценный в большинстве языков, а то что вы думаете на эту тему, может быть очень далеко от того, что есть на самом деле.

UFO just landed and posted this here

А вы в состоянии определить, сколько разных RuntimeException может выкинуть конкретная строка кода? А если там вызов чужой библиотеки? И даже если можете — вы будете их все тестировать? И зачем? А если нет — то о каком 100% покрытии вы говорите, в каком смысле?


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

UFO just landed and posted this here
А откуда берётся wishlists в первом примере, это глобальная переменная?

P.S. Не умею на джаве писать, в принципе код понятный, но откуда взялась эта магия непонятно.
UFO just landed and posted this here
Всегда ненавидел эти бессмысленные требования писать тесты для всего.
Нормальный программист может адекватно оценить когда тест писать выгодно(затраты на написание теста меньше чем экономия потом на поиске ошибок) а когда нет. Это часть архитектуры и программист сам должен решать где они нужны а где нет, а не следовать слепым правилам.
UFO just landed and posted this here
Тут проблема не в отсутствии теста а в безразличном отношении тех кто туда дописывает. Не думаю что обязаловка тестов тут чем-то поможет. Допишут код, тест как-нибудь залечат и ладно.

Я стараюсь писать тесты когда они простые, покрывают много потенциальных ошибок, не сломаются при ожидаемых доработках кода который тестируют и т.п. Но такие тесты придумать совсем не просто, и далеко не везде возможно.
А тестировать плоский тривиальный код, который с 99.999999% вероятностью не содержит ошибок, а если и содержит то они тут же выстрелят и элементарно будут найдены и исправлены — это пустая трата времени
На стадии эксплуатации это «элементарно» может стоить несколько недель. Ведь в таком случае баг еще надо найти, описать, засабмитать, приоритезировать, разобраться, починить, проревьювать, протестить, задеплоить, зарелизить, обновить.
Где-то в промежутке этот код перестанет проходить ревью и его распилят на тривиальные методы, которым снова не нужны тесты.

В целом, если в рамках одного метода появляется достаточно сложная логика для того, чтобы имело смысл её тестировать — это сигнал не к написанию ещё двух десяток строк бесполезного кода, а к тому, что этот метод пора рефакторить. Да, бывают случаи, когда отрефакторить действительно нельзя, но это исчезающие доли процента от общего количества кода.
Берем один сложный метод, делим его на 10 простых связанных методов… и кто нам после этого скажет, что оно работает так же как работало? Я уже даже не буду спрашивать, а работает ли оно так, как требуется изначально.
Написать качественный тест, гораздо сложнее, чем написать качественный код. А написать качественный код уже сложно.

Да, конечно, существуют абстракции, которые идеальны изолированы для тестирования, коллекции, алгоритмы… Но если это логическая конструкция, по типу UIListAdapter, который существует, чтобы отображать ui строчки и все, что делает это делегирует коллекцию? То зачем это? Или CollectionOfFavorites, которое на 90% состоит из прямых вызовов Collection, что мы тестируем, ArrayList?
Мне в JS помог переход на функциональное программирование внутри модулей. То есть снаружи всё выглядит как компонент, но внутри только функции (без this, но не чистые, конечно). Слава redux'у за популяризацию, теперь хотя бы у виска не крутят, когда про такое говорю :)
Ну нет так нет…

Плохой программист. Хороший не поленится добавить хотя бы тест, которые проверяет то, что он изменил. А при наличии времени — покрыть сразу весь метод.
UFO just landed and posted this here
А мне кажется, что в приведенных примерах не только тестов не надо, но и код, ими тестируемый — явно признак запущенной стадии ООП головного мозга.

Статья — полный бред.


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


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


Человек за 15 лет практики не понял, что TDD — это способ избежать появления такого кода и таких тестов.


я свято придерживался принципов TDD

Ну понятно, такой же ***, как и те, кого он приводит в качестве примера. Фу.


P.S. Выше в комментах уже приводили эти доводы, но мне хотелось дополнить их выводом, выделенным курсивом. И простите за жесткость. Заколебало.

Жесть. А ведь тестировать нужно только public-интерфейсы для связи с внешним миром. Все остальное остается в black-box и, в случае неисправности, проявит себя при тестировании public-интерфейсов.
Взять например популярный патерн «Command», с единственным «execute()» в интерфейсе, и кучей indirect inputs/outputs внутри, на которых код может валиться.
Это уже не юнит-тестирование. Почему-то все путают публичный API програмного компонента (библиотеки) и публичный интерфейс модуля. Последний может оставаться частью приватного API, но при этом протестированным юнит-тестами. А вот тестировать приватные методы модуля — действительно плохая практика, в этом случае лучше пересмотреть дизайн и вынести логику в отдельный тестируемый модуль.
Речь как раз идет о public-интерфейсах классов / модулей, а не о public API всего приложения / библиотеки.
Просто меня black-box несколько смутил. Что вы под этим термином понимаете? Являются ли зависимости частью публичного интерфейса?
«Black-box» применимо ко всему: классы / модули / public API приложения и т.п., зависит от контекста, что называть black-box.

Например:

Класс / модуль, который зависит от другого класса / модуля, является клиентом, а тот другой класс, является black-box (в данном контексте).

Всегда есть клиент и есть провайдер (black-box) с public-интерфейсами, к которым обращается клиент для получения от black-box какого-то полезного результата. Клиент самостоятельно не умеет выполнять нужную работу, но он знает, что вот тот другой класс умеет, и обращается к нему через public-интерфейс.

Блэк-боксом мы его называем потому что для клиента он по сути black-box: клиент не знает (и не должен знать), как именно провайдер будет выполнять ту работу, которую поручил ему клиент, т.е. все, что внутри – это мегасекретная тайна для клиента.

Чтобы это все работало, между клиентом и провайдером (black-box) должно быть заключено негласное соглашение («контракт»):
– клиент должен знать, что есть вот такой провайдер и он умеет делать такие-то вещи, должен знать каким образом обратиться к этому провайдеру, какие варианты ответов можно ожидать от провайдера, в каком формате эти ответы поступят, и т.д.
– а провайдер (а вернее разработчики этого провайдера), в случае изменения характеристик (версии) своих public-интерфейсов, должен уведомить об этих изменениях всех заинтересованных, чтобы они привели в соответствие свой код
Ну это для клиента он black-box, так как клиент ожидает только выполнение контракта (интерфейс, возможные исключения, нулабилити). А является ли модуль black-box для юнит-теста? В моем понимании — нет, не является, так как юнит тест должен в точности проверить детали реализации. Максимум, что можно отнести к black-box — это использование стандартных коллекций и строк, — то что нет смысла инжектить и мокать. Т.е., по сути, любой тест состояния можно отнести к тестированию black-box, но тест поведения (через моки) — это уже white-box. Последних в моей практике на порядок больше.
И для unit-тестов, объект, который подвергается тестированию, точно так же является black-box.

Вы зачем и что тестируете? Не тест же ради теста (бессмысленно)?

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

Выполняет класс / модуль нужный функционал? Отлично.
Не выполняет (или перестал выполнять после каких-то изменений)? Тогда нужно исправить код и снова довести весь модуль до 100% compliance с его задуманной спецификацией – unit-тесты как раз для этого и нужны.

Запланировали модули. Каждый из них по отдельности реализовали. Реализация отвечает запланированной, ваши unit-тесты это подтвердили. Отлично!

Теперь проведите integration tests, которые покажут, работают ли ваши отдельные модули в связке между собой? Если работают, то вы победитель. Если нет, то что-то не так в общей картине. Найдите ошибку(-и), исправьте и проведите все тесты снова.

В результате вы должны получить код, который достаточно надежно выполняет задуманный бизнес-функционал. А это уже продукт. Готовый, оттестированный, который можно где-то применить, интегрировать в какую-то другую систему и т.д. Кроме того, продукт этот вы можете развивать и быть уверенным, что если что-то где-то внутри поломается, то unit- и integration-тесты вам об этом сразу скажут.

UPD:
Обратите внимание, что тестируете вы бизнес-функционал, а доступен он через public-интерфейсы вашего класса / модуля. Т.е. ваш тест выступает в роли Клиента. А Клиент, как известно, взаимодействует с классом через public-интерфейсы.
Да, я и сам через это прошел.

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

Во всех книгах и статьях, которые я прочел, описана какая-то фактология. Никто методично ничего не объясняет.

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

Юнит-тест тестирует не только публичный интерфейс, но и взаимодействие с зависимостями (кроме модулей без зависимостей). Зависимости в чистом SOLID скрыты от клиента абстракцией (Java итерфейсом или абстрактным классом). Интерфейс не задает поведения, он лишь определяет контракт взаимодействия. Поведение задает конкретная реализация интерфейса. Тест эту реализацию проверяет с помощью моков зависимостей. Зависимости — часть внутреннего (скрытого от клиента) устройства модуля, поэтому юнит-тесты считаются тестами белого ящика.
Максимум, что можно отнести к black-box — это использование стандартных коллекций и строк, — то что нет смысла инжектить и мокать.
– вот это не понял и меня это смутило.

Т.е., по сути, любой тест состояния можно отнести к тестированию black-box, но тест поведения (через моки) — это уже white-box.
– зачем Вам тест поведения (т.е. внутренней реализации класса / модуля)?

Вероятно, я не верно Вас понял, и имелось в виду совсем другое?
Поведение задает конкретная реализация интерфейса. Тест эту реализацию проверяет с помощью моков зависимостей. Зависимости — часть внутреннего (скрытого от клиента) устройства модуля, поэтому юнит-тесты считаются тестами белого ящика.
– т.е. тот факт, что для проведения некоторых unit-тестов технически необходимы mocks (а это уже означает, что автор теста уже вынужден быть осведомлен о внутренней реализации класса / модуля), говорит о том, что это уже никак не black-box, а скорее… «white-box»? Если так, то, вероятно, вопрос и правда философский :-)

В таком случае, можно говорить, что white-боксом его делает «знание» unit-тестом о том, в какой среде (без «затыкания» внешних зависимостей класс / модуль не сможет сработать) должен проходить unit-тест.

Тогда можно сказать, что:
– black-box – это когда вы ничего не знаете об объекте тестирования, кроме деталей контракта
– white-box — это когда вы ничего не знаете об объекте тестирования, кроме деталей контракта и необходимой данному объекту среде, без наличия которой он не способен работать.

Не является ли необходимая объекту среда частью контракта?

Нет, white-box — это когда об объекте тестирования известно все. На пальцах: смотрим в код, видим if (x > 5) — и пишем тесты для x = 4, x = 5 и x=6.

Завтра бизнес логика изменится, и станет x > 5 && x < 10
Юнит-тест по прежнему зеленый, а ситуация с x >= 10 не оттестирована.
Я это не к определению white-box-а пишу, просто наблюдение.

а для этого придумали мутационное тестирование

Это if (x > 5) должно быть реализацией какого-то требования к тестируемому объекту. Соответственно если эти кейзы сформулированны в терминах требований, то не вижу проблемы в таком вайтбокс тестировании. Если же замокан модуль Math и Перегружена операция < и контроллирвется что она вызвана ровно один раз, то тест получится хрупким

Интерфейс в с какой-то степенью задает поведение. Скорее всего, если у вас есть IStack то в комментарии к методу push будет написано, что значение может быть дальнейшем извлечено при помощи pop. То есть чтобы корректно реалзовать интерфейс надо соблюсти какие-то требования к поведению.


Зависимости могут быть частью интерфейса и тогда "моки" (в общем смысле — фейковые объекты) вполне себе помогают тестировать его как черный ящик.


Если тестировать не требования а реализацию, то тест не сможет ответить на вопрос, соответствует ли реализация требованиям

Чето я не понял… Автор вроде пишет про TDD, а в примере его спрашивают про то как написать тест для уже существующего реализованного функционала.
TDD же про:
разработка через тестирование, или, как её раньше называли, подход test-first

Получается что проблема не в методологии, а в том что автор ей не следует.
Люди часто говорят «TDD», подразумевая использование юнит тестов, а не конкретную методологию.
Есть хорошая русская пословица — заставь дурака молиться он и лоб расшибёт. Умеренность хороша во всём. Особенно в программировании.
UFO just landed and posted this here
Метод не нужно тестировать, если он выполняет одну задачу и не имеет сайдэффектов (или если проксирует покрытый тестами сложный метод).
Внезапно — Универсального ответа не существует! А истина — в поисках идеального баланса между нулевым и 100% покрытием. Который конечно же на каждом проекте и в каждом случае свой. И вообще — это работа тест лида.
p.s. То же самое происходит с излишней оптимизацией и рефакторингом да и много с чем еще.
У меня это критерий когда я боюсь этого кода и хочу руками проверить что он работает.
Регулярные выражения — тест нужен. Преобразования данных — тест нужен. Поиск по каким-то данным — тест нужен.
Присваивание переменной — тест не нужен. Условный оператор — тест не нужен.
Очень везет, когда баги статические. Когда же приложение с кучей потоков, всё динамическое, куча событий, гигабайты логов — вот это настоящий вызов. Попробуй тут тест напиши.
Юнит-тесты знают о деталях реализации, тем самым нарушая инкапсуляцию и являясь антипаттерном.
Для чего он предназначен? Для фиксирования логики работы. Но логика работы проверяется при дебаге, и никто не будет менять эту логику без веских причин или случайно.
Если же код правится, то эти тесты падают, и их тоже приходится править. И опять они бесполезны.
Да и что они тестируют? Что мои глаза меня не обманывают? Что компилятор не сломался?

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

Плюс не надо забывать что юнит-тесты это тоже код, который надо поддерживать. При 100% тестировании количество кода увеличивается раза в 4. Соответственно увеличивается время изменений и цена продукта.
Опять же при 100% покрытии крупный рефакторинг практически невозможен. — нужно будет выкинуть все эти тесты
Для чего он предназначен? Для фиксирования логики работы. Но логика работы проверяется при дебаге, и никто не будет менять эту логику без веских причин или случайно

В первую очередь TDD предназначено для того, чтобы разрабатывать. Логика работы в таком случае проверяется не дебагом, а запуском тестов, в которых эту логику придётся описать в явном виде.


Да и что они тестируют? Что мои глаза меня не обманывают? Что компилятор не сломался?

Что код соответствует ожиданиям программиста. Обновили библиотеку и хотите понять, что это не сломало ваш код? Запустите юнит тесты.


Опять же при 100% покрытии крупный рефакторинг практически невозможен.

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

Логика работы в таком случае проверяется не дебагом, а запуском тестов, в которых эту логику придётся описать в явном виде

Неужели прямо в ТЗ описана вся реализация вплоть до присвоений переменных?
Нет, там описаны наборы входных и выходных данных. Именно это и надо тестировать — что мы подали на вход и что получили на выходе. Но не реализацию

Обновили библиотеку и хотите понять, что это не сломало ваш код?

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

Рефакторинг это такая штука, которая не меняет логику.

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

Нет, есть огромная куча кода, которая в ТЗ не описана вообще. И для неё естественно надо писать юнит тесты. Чтобы понимать, что она работает так, как ожидается.


Нет, там описаны наборы входных и выходных данных.

В ТЗ описано чего хочет заказчик. Иногда это наборы данных, иногда это вокрфлоу, иногда что-то ещё.


решается любым интеграционным smoke тестом.

Он не покажет что конкретно сломалось.


Тем более что все ломается обычно на стыках слоев и инициализации и конфигурации фреймворков, что юнит-тест не способен протестировать.

Бывает по разному. В моей практике много раз бывало, что ломался код, который опирался на сторонние библиотеки. Юнит тесты сразу показывали где.


Именно что меняет логику и реализацию

Рефакторинг логику не изменяет по определению. Если вы под рефакторингом имеете в виду любую доработку вообще, то это определение не канонично.

Чтобы понимать, что она работает так, как ожидается.

проверять что компилятор не сломался? Сам код уже не является этим описанием? А вы тесты на тесты не пробовали писать? В тестах тоже есть логика, которую надо тестировать

Он не покажет что конкретно сломалось.

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

Если вы под рефакторингом имеете в виду любую доработку вообще, то это определение не канонично.

где проходит граница между рефакторингом и доработкой? Для меня это изменяются ли выходные данные. Например перевод процедурного кода на ООП — внутренняя логика меняется, выходные данные не должны. Это рефакторинг
проверять что компилятор не сломался? Сам код уже не является этим описанием? А вы тесты на тесты не пробовали писать? В тестах тоже есть логика, которую надо тестировать

Нет, проверять, что ожидания соответствуют реальности. Ожидания описаны в юнит-тесте, реальность в коде. Кроме того, юнит-тест пишется раньше кода. Это облегчает и ускоряет разработку и это немаловажно.


юнит-тест, тем более на моках отсекает все сторонние библиотеки.

Если цель — тестировать что эта библиотека не сломана, то не отсекает.


где проходит граница между рефакторингом и доработкой?

Это всегда непросто сказать. Вот например переписывание программы на другом языке — точно не рефакторинг, хотя для пользователя ничего не меняется.


Например перевод процедурного кода на ООП — внутренняя логика меняется, выходные данные не должны.

Придётся выбросить большинство существующих юнит тестов и написать новые. Масштаб рефакторинга очень большой. Фактически это могло бы быть переписывание кода с C на C++. Или, с тем же успехом на Java :)

Юнит-тесты знают о деталях реализации, тем самым нарушая инкапсуляцию и являясь антипаттерном.

Юнит тесты знают только об итерфейсе юнита.


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

При изменении надо следить чтобы другие требования не нарушались.


Если же код правится, то эти тесты падают, и их тоже приходится править.

При правильно написанных тестах надо править только тесты, которые тестируют изменяемое требование.


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

https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html


Опять же при 100% покрытии крупный рефакторинг практически невозможен. — нужно будет выкинуть все эти тесты

Отрефакторить. Требования никуда не деваются — меняется только форма их представления

Юнит тесты знают только об итерфейсе юнита.

у адептов принято тестировать и private методы
При изменении надо следить чтобы другие требования не нарушались.

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

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

Кто такие адепты? Вот например известная книжка http://xunitpatterns.com/Principles%20of%20Test%20Automation.html читать с use the front door first. Вы не путаете покрытие приватных методов с тестированием реализации?


И их невозможно написать без дублирования требований.

Тесты должны рефакториться так же как и код.

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

Возможно кто-то в спешке нагородил процедурщину. А еще может быть, что класс пытается брать на себя больше одной ответственности (поэтому дополнительную фунциональность, которая не является ответственностью класса, завернули в приватный метод). В этом случае стоит попытаться вынести функциональность приватного метода в отдельный класс, где она будет основной, а следовательно публичной, и тестировать по обычной методике.
Бывают еще такие произведения искусства, как Oracle Commerce, где на тестирование 10 строк кода нужно написать 150 строк моков.
Может я чего-то не понимаю, но по-моему 100% покрытие не значит, что есть тест на каждую строку кода.
Просто этот код должен выполняться в каком-то тесте. Т.е. у меня может быть 1 тест, который проверяет метод
Foo.new(arg).call

и при этом у меня будет покрыт метод new и покрытие будет 100%
UFO just landed and posted this here
Sign up to leave a comment.