Pull to refresh

Comments 162

Шутки-шутками, а вот добиваться 100% покрытия кода на куче DT Объектов — та ещё головная боль, которая никакой практической пользы не несет.
Не сарказма ради, но, может, стоило убрать требование по 100% покрытию, если сами разработчики не видят в нем пользы? (Если оно, конечно, не продиктовано заказчиком, который за это платит.)
Давайте лучше подумаем над более адекватной метрикой, чем «доля строк кода, в которые заходит исполнение тестов».
как насчет «здравый смысл»?

Мало у кого взят этот перк.

А за чем вам 100% покрытие? чем 100% лучше чем 90% больше не всегда ведь лучше?
Никто ведь не пытается поднять температуру человека с 36.6 до 37 градусов.
А статья супер :)
чем 100% лучше чем 90% больше не всегда ведь лучше?

И тут я завис при парсинге

запятая после "90%" вернёт вашему парсеру душевное спокойствие.

Там больше подходит знак вопроса:
Чем 100% лучше, чем 90%? Больше не всегда ведь лучше?
Просто у меня там ассерт, который никогда не вызывается… И вот, кривой семантический парсер ушел в продакшн.
PS: хорошо хоть «за чем» нормально научился отрабатывать — в логи уходит сообщение «за шкафом».
Несколько раз встречал как отдельных людей, так и целые компании, ставившие 100% покрытие как обязательную метрику кода. Где-то на хх даже в вакансиях такое встречалось.
Особо это бесит, когда в коде есть а) куча сгенеренного бойлерплейта типа toString() и прочей мути, которая в тестах обычно никого не интересует и б) куча catch'ей для CheckedExceptions (боль для Java), которые никогда не будут выброшены, но компилятор заставляет добавлять для них catch-блоки и писать в них что-то там (а пустые catch-блоки могут быть запрещены гайдлайнами), что в итоге тоже попадает в статистику и портит coverage.
А ещё бывают «общие» для нескольких проектов библиотеки, которые содержат только набор различных объектов и в лучшем случае ещё пару конвертеров, но по требованиям начальства даже такие библиотеки должны быть протестированы на 100 (90, 80, 70) процентов, иначе никак.
Както попал я в такой проэкт, 100% покрытие кода тестами. Одно изменение которое отнимало 5 мин требовало 2-3 часов исправлений тестов. Тогда убедился что это безумие. Гораздо лучше и практичней использовать интеграционные тесты, если что-то не так они тоже упадут, но зато ясно что система/подсистема работает.
По описанию это выглядит как проблема с неправильно написанными тестами, а не конкретно 100% покрытием. Впрочем первое может вызываться вторым, когда от тебя требуют покрыть то, что не имеет смысла покрывать, то и рождаются странные и хрупкие тесты.
Григорий Остер «Вредные советы» )
Мутационное тестирование в проекты! :) И уже делать псевдо покрытие кода становится чуть сложнее. Хотя если человек не хочет писать тесты, то всегда найдет способ как их не писать :)

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

Почему тесты на java выглядят так уродливо?


Вот пример среднего теста на pytest:


def test_foo():
    assert foo()=='bar'

@pytest.mark.parametrize("input, output", [
    ('/var/log', True),
    ('/run/bin', False)
])
def test_foo_path(input, output):
   assert output in foo(input)

Вы же шутите, да? Вы же взрослый человек и понимаете, что это дело привычки, а семантика одна и та же выражается. Это всё равно что говорить, мол русский язык уродливо выглядит, все эти "Я тебя люблю", то ли дело английское "I love you".

Нет уж. Не "Я тебя люблю", а abstract_factory(я, ты).builder(verb).builder(любить).make_possesive().to_sting(). Вместо I.love(you).

Ого, да вы всерьёз! Вы, наверно, думаете, что и винда в синий экран каждый день падает?
Не знаю. Последнее, что я слышал, что она при апгрейде стирала данные и кирпичила рабочие станции HP, но как-то не приходится сталкиваться.
Выглядит как код примеров для Yargy. Вполне себе питоновая библиотека.
В котлине вы можете сделать I.love(you), но выглядит так, как будто love какое-то действие произведёт над you, Но нет. Поэтому красивве будет me.inLove = you

А холивара ради можно вот что обсудить. Я утверждаю, что чтобы протестировать API на динамически типизированном языке, вам потребуется вдвое больше кода. Потому что надо проверить, что каждый метод возвращает результат нужного типа и кидает исключение (или иным способом сигнализирует об ошибке) на параметры некорректных типов. То что в языках вроде Java проверяется компилятором и требует ноль тестов, в Питоне выродится в ужасный бойлерплейт. Как вы решаете эту проблему?

А на null вы все проверяете в каждом тесте?

В текущем проекте мы используем аннотации вроде @NotNull и annotation processor, который превращает их в рантайм-проверки. Это эффективно решает проблему и избавляет от необходимости проверять на нуллы. Но вы ткнули в больное место, конечно, за это плюсик!

Я бы не сказал, что проверки на null — это больное место. Просто писать код и ожидать, что так просто методы null не возвращают — нормально. В любом динамическом языке можно везде писать @returnString аннотацию или вроде того, но никто не пишет.

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

Ну так нулл один, и действительно разумно ожидать, что его не возвращают. А типов много, и тогда разумно ожидать что? Методов, возвращающих строки, должно быть очень мало. Вот метод getName() вернёт строку или объект Name? Неочевидно, чего ожидать.

Кажется валидный java код в последних версиях, тот тоже не ясно
var name = person.getName();
Так же не ясно что тут происходит
person.getName().toLowerCase();
Это метод строки или объекта Name

Это статически известно на этапе компиляции, можно всегда посмотреть в IDE или javadoc. Можно контролировать типы, например, выносом интерфейсов API в отдельный модуль с особыми правилами внесения изменений. Изменить тип можно только изменением декларации, никакое изменение тела метода тип не поменяет. Да, возможны редкие случаи, когда изменение типа метода не сломает весь остальной код, но это вполне нормально. В динамическом языке изменение тела одного метода может повлиять на результат других (которые делегируют к этому). Если делегация условная, другой метод может после такого изменения неожиданно возвращать разные типы в разных ситуациях. Я не понимаю, как можно уверенно рефакторить проект на динамическом языке, не обложившись тестами на типы аргументов и результатов методов.

С рефакторингом согласен, но даже со статической типизацией без тестов я не буду рефакторить, потому что типы не гарантируют корректную логику (толку мне от того, что getName вернет строку, если она всегда пустая). Поэтому все тоже самое — тестировать бизнес логику, а не аргументы и тип результата. Если брать JS, то тест
expect(person.getName()).toBe('Alex');
Вполне успешно тестирует и тип и поведение. Я пишу на js/js+статические типы/ruby/go/c++. В проектах крайне мал процент ошибок по типам… много ошибок логики, чуть меньше с null типами.

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

У меня тут вывод покрался… «если у вас проект на 10к+ строк кода, то лучше бы использовать язык со статической типизацией, потому что банально IDE очень сильно будет выручать». А для небольших проектов плюшки динамики очень сильно помогают, а минусы сильно не болят.
У меня тут вывод покрался… «если у вас проект на 10к+ строк кода, то лучше бы использовать язык со статической типизацией, потому что банально IDE очень сильно будет выручать». А для небольших проектов плюшки динамики очень сильно помогают, а минусы сильно не болят.

Мой 6-летний опыт поддержки, развития, и многократных глубоких рефакторингов JavaScript фреймворка на 1.5 млн строк утверждает: и на крупных проектах процент ошибок по типам так же мал, и минусы динамического/слабого типизирования так же не болят. Надо хорошо тестировать, вот и всё.


Ждём выступление Джавистов: Пфе, вот если бы по-настоящему крупный проект!.. :)

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

Тестирование бизнес-логики займёт на каком-то TypeScript ровно столько же времени и усилий, что и на каком-то JavaScript. Тестировать же типы принимаемых аргументов и возвращаемых значений не имеет смысла, поэтому никакой экономии вы не увидите.


IDE в разы лучше делает рефакторинг у статических яхыков

Голословное утверждение. На чём оно базируется?


динамика в таком случае даст плюсы за счет упрощения синтекса языка

Не совсем понятно, какой язык вы имеете в виду. Основные преимущества динамической типизации обычно находятся далеко не в упрощении синтаксиса.

Мда. И откуда появился миф «в динамическом языке нужно писать тесты на типы» не понятно, крайне печально такое постоянно слышать.

> Голословное утверждение. На чём оно базируется?
А это реально так. Нельзя просто ткнуть в поле и переименовать его во всем проекте сразу… вроде name -> firstName
А это реально так. Нельзя просто ткнуть в поле и переименовать его во всем проекте сразу… вроде name -> firstName

IDE не пользуюсь, но многие коллеги без них жить не могут. И лёгкость переименования переменных и полей является самым частым (и обычно, единственным) аргументом в пользу IDE в неизбежных холиварах на кухне.


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

Классика.

Вы просто пишете на JavaScript и ПОЭТОМУ он лучший. Не будет странным наблюдать, то же самое у всех ))). Но да, язык при этом может резко измениться ;). Типы и IDE это вполне себе удобство для кого-то, что плохого? Опционально же все на свете.

Пример-аналог: когда-то в сссрах я писал не то что без IDE, а прямо в кодах К580ИК80 и на бумажке, т.к. под мой первый ПК Орион128 тупо не было редактора, ассемблера и дизассемблера. Их то я собственно и писал, и страшно горд своей тогдашней крутизной до сих пор. И даже тесты были, о как, в 1988 кажется. Но не смешно ли это? Вроде бы как смешно, не находите?

Чем-то позиция фтопку типы, фтопку IDE, крутота наше все может быть фантомно близка каждому разработчику. Особенно если у него что-то получилось. Но всерьез считать это крутым советом… Мир будет скорее против. И будет прав. См. tiobe например.

ЗЫ. А может типизация — это заговор мировой закулисы? Или и вовсе одного человека, скажем Андерса Хейлсберга. TurboPascal, Delphi, J#, .Net, C# и о ужас TypeScript — все он, все он…
т.к. под мой первый ПК Орион128 тупо не было редактора, ассемблера и дизассемблера.

Он же был совместимым с Радио-86РК, подозреваю, куда больше чем мой Микроша. А для него я использовал *MICRON (редактор, асм идизасм по 2к, в дампах из журнала) с минимальными адаптациями, главная сложность была в другом алгоритме подсчёта контрольных сумм, из-за чего нельзя было понять где допустил ошибку. Решилось ручным дизассемблированием Монитора РК, и выносом из него программы подсчёта этих сумм.

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

А моей практике они встречаются по несколько раз на дню (на Java) и ни разу не встречались на JavaScript за 5 лет опыта. Угадаете почему? Нет, не потому что ненужны, а потом вместо пары секунд, требуют пары часов. Просто когда у человека не было топора всю жизнь, он может искрене верить, что ножом куда удобнее рубить дрова. Да и вообще зачем дрова рубить и так как-нибудь сгорят.

(и обычно, единственным) аргументом в пользу IDE в неизбежных холиварах на кухне.

Вы просто видимо не понимете, что такая хорошая IDE на статически типизированым языке. Переименование полей это ерунда, вот возможность полностью поменять всю архитектуру на порядки быстрее + умное автозаполнение + умное обнаружение самых разных потенциальных ошибок, позволяют программировать принципиально по-другому, когда рефаторинг всего проекта может выполнятся непрерывно.
А моей практике они встречаются по несколько раз на дню (на Java) и ни разу не встречались на JavaScript за 5 лет опыта. Угадаете почему? Нет, не потому что ненужны, а потом вместо пары секунд, требуют пары часов.

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


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

Вы читаете куда-то между строк. См. выше: многие мои коллеги по цеху JavaScript пользуются различными IDE, которые обеспечивают эту самую лёгкость переименования, бла бла. И усиленно пытаются (ну, пытались раньше) продать мне эту идею, хотя на практике пользуются ею крайне редко. Потому что обратная совместимость, когнитивная нагрузка, да и просто банальный вопрос "зачем?"


Вы просто видимо не понимете, что такая хорошая IDE на статически типизированым языке.

Отчего ж, вполне понимаю. Несколько лет разработки на Pascal и C++ даром не проходят, хоть и давно это было.


Переименование полей это ерунда, вот возможность полностью поменять всю архитектуру

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

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

DDD, например, предполагает подобные постоянные изменения кода при любых изменениях даже языка общения с бизнесом, не говоря о бизнес-тркбованиях.

> IDE в разы лучше делает рефакторинг у статических яхыков

В строго статически типизированных языках вам и компилятор подскажет, а вот в динамике как раз без IDE сложновато.
и это не говоря о том, что var может быть только локальным. Вернуть var нельзя, а значит мы всегда знаем какой типа в итогевернётся. И так как в джаве не может спонтанно возникнуть кастомного метода в рантайме у объекта — никаких неожиданностей никогда нет. О перегрузки по возвращаемому значению в джаве нет тоже.
>мы используем аннотации вроде NotNull

А как тестировщик удостоверится, что NotNull написан в нужном месте и, главное, не пропал при рефакторинге?

Можно настроить почти любой статический анализатор, который не даст вам вернуть потенциальный null в методе, где нет аннотации @Nullable или наоборот выдаст предупреждения на методы где null невозможен, но @NotNull не стоит.

Я имел ввиду использование в аргументах, типа:

public void setX(@NotNull final Object aX )


Не пойму, как ту использовать статический анализатор?
да примерно так:

@NotNull
private x;

public void setX(final Object aX ) { // будет варнинг так как возможен null Pointer
   x = ax;
}


private x; // можно настроить варнинг, с предложением поставить@NotNull или @Nullable

public void setX(final Object aX ) { 
   x = ax;
}


setX(new Object1()); // единственное место использование
...
public void setX(final Object aX )  // может настроить варнинг, так как в реальности aX никогда не будет null и возможно стоит добавать @NotNull на будущее


По-моему все такие варнинги можно банально в Идее настроить (там очень неплохой статический анализатор), не говоря уже об отдельных тулах
Понятно, благодарю. У нас все делается, признаться, по старинке, если в методе принимать null категорически нельзя, то используется проверка в runtime методами типа:

static void assertNotNull(Object value, String message)


Сейчас стали много писать на Go и даже JavaScript, с Java уходим, и, похоже, все эти новшества так и пройдут мимо. Есть устойчивое ощущение избыточности таких проверок. Но доказать не могу :)
с Java уходим, и, похоже, все эти новшества так и пройдут мимо.


Более интересна философия Kotlina, где null по сути разрешен только явно, или scal'ы, где null это обычный тип (трейт) и кроме null есть еще много других похожих типов.
>Более интересна философия Kotlina

Да, это хорошая вещь, но на любителя. У JRE есть, к тому же определенная техническая проблема — прекращена официальная поддержка 32-битных систем, а у нас много клиентов на таких. Так что Kotlin «отпал».
А Optional как-нибудь помогает в этой проблеме?
Я совершенно согласен насчёт количества тестов для python — там надо добиваться 100% покрытия тестами, иначе нелепые опечатки проскочат в продакшен.

Однако, я не согласен с утверждением, что статическая типизация java защищает от значительного числа ошибок. Выше написали про null (что примерно соответствует питоновому проклятью «TypeError: unsupported operand type(s) for +: 'NoneType' and 'list'»), а я могу докинуть ещё боли. Например, java позволяет выполнять полное сравнение для неполностью упорядоченных типов (например, float, у которого NaN, Inf, ненормализованные нули и другие ужасы). После типофашизма rust (тот же PartialEq) типизация java выглядит как очень расслабленная и прощающая идиотам их опечатки (и не прощающая не-идиотам опечатки идиотов).
Я совершенно согласен насчёт количества тестов для python — там надо добиваться 100% покрытия тестами, иначе нелепые опечатки проскочат в продакшен.

Нелепые опечатки должны отлавливаться линтером и не доходить даже до merge.

И каким образом линтер без информации о типах сможет понять где опечатка, а где нет?

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


Что-нибудь типа такого, самый банальный вид опечаток:


function() {
    var foo = 'bar';
    ...
    fooo; // ESLint ловит на ура
    foo = truue; // То же самое
}

И т.д. Где тут нужна информация о типах? И каким образом она поможет от наиболее распространённых случаев синдрома толстых пальцев, когда тип переменной остаётся таким же, но значение ошибочно? Вот как в примере с '_main__' внизу, или сделать опечатку в цифре, или ещё что-нибудь.


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

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

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


Что-нибудь типа такого, самый банальный вид опечаток:

function(p) {
    p.fooo; // ESLint тут бесполезен
    p.foo = 'baaar'; // То же самое
}

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

Вы не поверите: https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types


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

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

p.fooo; // ESLint тут бесполезен
Я не на 100% уверен, давно дело было, но статический анализатор в PyCharm питоновский такое вроде бы легко находил. Как дела обстоят в JS я не знаю, но питон так-то тоже с динамической типизацией.
Судя по вопросу вы никогда не программировали на языках со статической типизацией. Попробуйте, увидите мир с совершенно другой стороны.

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


function(p) {
    p.fooo; // ESLint тут бесполезен
    p.foo = 'baaar'; // То же самое
}

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


Вы не поверите: https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types

Нда, типофашизм крепчал… О таком я не знал, спасибо за просвещение. Не завидую тому, кто будет что-то похожее поддерживать...


Тестирование закрывает те дефекты, которые не смог отловить статический анализ.

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


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

Теоретически, да. На практике, при грамотно построенном тестировании эта нагрузка в пределах фонового шума.

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

Видимо очень давно. Смотрите, как выглядит код на современных статически типизированных языках: https://run.dlang.io/is/vjmZFx


а в будущем возможно и добавят проверку на нечёткое совпадение имён переменных и полей

В Idea это уже давно есть. Работает из рук вон плохо по сравнение с TS.


Не завидую тому, кто будет что-то похожее поддерживать...

Всё отлично поддерживается. Даже подсказки в IDE работают.


тестирование является основным инструментом верификации кода

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


статический анализ всего лишь дополнение, позволяющее существенно сократить расходы на тестирование определённого вида дефектов

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

Это дырявый инструмент. Проверяется проверяются несколько значений из бесчисленного множества.
Вы просто незнакомы с теорией тестирования видимо. Бесчисленное множество бьется на классы эквивалентности, число которых в большинстве случаев оказывается крайне невелико. И они великолепно покрываются тестами.
У него покрытие этих видов дефектов по определению больше, чем возможно при тестировании.
Это по какому определению так получается? Может определение или ссылку на него покажете?
Вся теория тестирования — о том, как латать дыры на тонущем корабле за конечное время. Для любого набора тестов можно придумать достаточно реалистичный код, который их не пройдёт.

Если в коде написано, что передать можно только экземпляры класса Foo, то ничего другого вы туда передать не сможете, даже если очень сильно накосячите с логикой. В более мощных системах типов вы ничего кроме 'foo' и 'bar' записать не сможете, откуда бы вы их ни получили.
Вся теория тестирования — о том, как латать дыры на тонущем корабле за конечное время.
Теория тестирования — о том, как исследовать продукт. Как делать продукт так, чтобы исследовать было проще и возможно было сделать это более полно, что делать чтобы ускорить этот процесс и получить более адекватные оценки при планировании. Она вообще не о латании дыр.
Для любого набора тестов можно придумать достаточно реалистичный код, который их не пройдёт.
Наверное можно, мне неинтересно даже думать так ли это, потому что я не вижу зачем. К чему этот довод?
Если в коде написано, что передать можно только экземпляры класса Foo, то ничего другого вы туда передать не сможете, даже если очень сильно накосячите с логикой. В более мощных системах типов вы ничего кроме 'foo' и 'bar' записать не сможете, откуда бы вы их ни получили.
И где же определение статического анализатора из которого что-то следует? Ваш комментарий выглядит как выводы из определения, но точно не как само определение, давайте все-таки с начала начинать, а не с середины.
Видимо очень давно.

Лет 20 уже как нашёл Исуса увидел свет динамики, и назад не оглядывался.


Смотрите, как выглядит код на современных статически типизированных языках: https://run.dlang.io/is/vjmZFx

Да мне в общем-то всё равно, как он выглядит. Более ёмкий синтаксис и type inference помогают сократить boilerplate, но развесистая клюква дерева типов никуда не денется, со всеми накладными расходами.


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


В Idea это уже давно есть. Работает из рук вон плохо по сравнение с TS.

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


Всё отлично поддерживается. Даже подсказки в IDE работают.

Речь не о подсказках, а о когнитивной нагрузке и расходах на проектирование/поддержку. Только не надо мне рассказывать о том, что всё это бесплатно и просто само по себе работает. Я слишком старый уже, чтобы в Деда Мороза верить.


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

Вот там внизу товарищ dimm_ddr уже по теории высказался, а я как человек практический скажу так: вы можете сколько угодно гоняться за своей красной селёдкой идеального кода без дефектов, а мне надо работу работать.


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


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


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


Ну вот как бы не сюрприз, что к похожим выводам некоторые крайне умные и очень опытные люди пришли ещё в конце 50-х. LISP наше всё, да.

UFO just landed and posted this here

Вы какой код хотите посмотреть, вышеупомянутого фреймворка? Вот здесь можно подписаться на пробную версию, скачать архив и посмотреть код. Лучше подписываться с одноразового емаела, чтобы потом моск не выклевали на тему купить. Это не я придумал. :(


Если интересно, как вся эта кухня тестируется, то вот статейка с бла-бла по верхам, ну и там же ссылка на код оркестратора. Сами юнит-тесты идут в архиве с фреймворком.

Из моей давнешней статьи про тестирование if __name__ == ‘__main__’ в питоне (https://medium.com/opsops/how-to-test-if-name-main-1928367290cb):


if __name__ == ‘_main__’:   # single underscore
    main()

if __name__ == ‘__main__’:
    sys.exit(main)   # forgot to call main

Как в такое отлавливать линтером будете?

Учитывая, что это выглядит как Канонiчный Пiтонъ, я бы ожидал от линтера достаточной продвинутости для понимания и анализа таких вот общераспространённых конструкций.


Ну т.е. ESLint как-то умеет понимать 'use strict'?

Частично, да. Но даже он пропустит __main_ в первой строке.
> То что в языках вроде Java проверяется компилятором и требует ноль тестов, в Питоне выродится в ужасный бойлерплейт. Как вы решаете эту проблему?

Используем питон >3.5 с type hints + прогон кода через mypy. Правда, рано или поздно возникает вопрос, зачем вообще нужно было выбирать питон, если везде нужно явно прописывать типы.
> надо проверить, что каждый метод возвращает результат нужного типа и кидает исключение
Зачем?
А вот так менее уродливо?

assertThat(string, not(blankString()));
assertThat(list, containsInAnyOrder(expectedValues));
assertThat(map, hasKey(expectedKey));

Связка assertThat из JUnit и matcher'ов из Hamcrest позволяет писать почти на чистом английском, а если обширного набора библиотечных matcher'ов недостаточно — всегда можно написать свои.

А почему не просто assert expected_key in map? Зачем всякие assertThat?

Очевидно, потому что оператора in в Java нет. А вот ключевое слово assert есть, но его использование считается плохой практикой. Во-первых, потому что assert в Java просто проверяет условие и выкидывает исключение, если условие ложно. С ним нельзя передать сообщение, по которому можно будет определить, что именно упало, не копаясь в стектрейсах, или вывести в лог сообщения о ходе проверки. Во-вторых, ключевое слово и его поведение гвоздями прибито к спецификации языка. Представьте, что вам нужно сделать сложную проверку, которую не напишешь в одну строчку после assert. Нужно выносить в отдельный метод. Для того чтобы использовать его с assert он должен возвращать boolean, то есть, никакой информации о ходе проверки вернуть нельзя. А хочется — и тут каждый начинает городить свои велосипеды, причём, возможно, даже в рамках одного проекта. Это промашка в дизайне языка, а философия обратной совместимости не позволяет её просто исправить. Это и приводит нас к тестовым фреймворкам, которые предоставляют стандартизированный API для тестов. assertThat — точка входа в один из таких фреймворков — JUnit. assertNull, assertEquals и тому подобные методы — специализированные реализации assertThat для наиболее частых сценариев проверок.
А фреймворк не может обработать этот assert и выдать нормальный вывод? В pytest assert — тоже ключевое слово, однако, pytest обрабатывает AssertionError таким образом, чтобы показать, что тест fail, и показать где именно. Например, если в тесте сказано assert a==b, то в выводе pytest будет написано, что a было «1234», а b — «123» с подчёркиванием где именно расхождение. То же для in, для аргументов функций и т.д.

Ведь в java можно поймать ассерт как исключение, не?

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


Не знаете случайно, как pytest добивается этой магии? Тут мало того, что он значение параметров знает вне стектрейса, так ещё и соответствующий код. Ведь само по себе expected_key in map это лишь True/False значение и AssertionError ничего не знает про то, откуда оно берётся.

Кажется нашел: https://docs.pytest.org/en/latest/assert.html#assert-details, http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html
Т.е. pytest налету подменяет assert код на что-то более информативное.
К сожалению я не знаю, насколько это возможно в джаве, но как по мне это выглядит неплохо :)

Я, кстати, не знал как он это делает. Да, круто.

В принципе, никто не мешает java-фреймворку для тестирования делать то же самое ещё до компиляции. Но тут уже сама религия java, где писать длинные унылые boilerplate паттерны с camlCase'ом — это часть общепринятого.
Философский вопрос — называть ли программой на языке Питон то, что вы пишете и что потом препроцессируется, чтобы сгенерировать реальную программу, которая уже подаётся на вход компилятору. По сути дела это некоторый мета-язык, лишь похожий на Питон синтаксически, но имеющий другую семантику. Разумеется, можно и для Джавы сделать препроцессор, имеются всякие кодогенераторы, но вроде как смысла нет. Если совсем припекает, можно воспользоваться другим JVM-языком, к примеру, Груви, где такие структуры вполне возможны. Смесь Груви и Джавы в одном проекте вполне легко устроить (мы это делаем). И по крайней мере вы пишете на конкретном языке, с конкретным синтаксисом и семантикой, а не непонятно на чём, что препроцессируется в Питон.
Ну вы передёргиваете. В коде тестов нет ни одной конструкции, которая была бы не питоном. pytest всего лишь добавляет поверх этого красивый вывод, но если полностью убрать pytest, то останутся обычные assert'ы и контекстные менеджеры (того же pytest'а) для ловли запланированных exception'ов.

То есть ваше предположение, что там «не питон» неверно. Это питон на 100%.

Так вроде вся суть же в красивом выводе, разве нет? Если красивый вывод не нужен, Java-версия assertTrue(foo(input).contains(output)) ничуть не отличается от питоновской. Ну окей, я понял, что вас тошнит от скобок, Лисп не для вас. Окей, понял, что вас тошнит от camelCase. Но эти аргументы не претендуют на объективность.

UFO just landed and posted this here
Я не понимаю о чём вы. Хорошо — это пофиксит все баги в коде так, чтобы тесты были не нужны? Не слышал про такой фреймворк.

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

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


Как сказал lany, это вкусовщина. Вот на мой вкус, я вынужден признать,
assert key in map
читаемее, чем
assertThat(map, hasKey(key));


Хотя я и работаю с Java и второй синтаксис мне гораздо привычнее

assert в Java просто проверяет условие и выкидывает исключение, если условие ложно. С ним нельзя передать сообщение

Разве кто-то запрещает?


assert false : "Я упало!";

Это промашка в дизайне языка,… Это и приводит нас к тестовым фреймворкам, которые предоставляют стандартизированный API для тестов. assertThat — точка входа в один из таких фреймворков — JUnit.

Насколько мне известно, assert никогда не позиционировался как инструмент для поддержки unit-тестов. Это скорее простой способ добавления проверок, не особо нужных в production, но полезных при отладке и диагностике ошибок.

AssertJ даже покрасивее будет, например
Frodo frodo = new Hobbit("Frodo");

assertThat(frodo.getName()).startsWith("Fro")
                           .endsWith("do")
                           .isEqualToIgnoringCase("frodo");


Пиво пить не брошу, потому-что он хороший.

Не очень понял смысл вашего примера. Речь топикстартера шла об «уродливости», а не о том чтобы написать самый минималистичный тест.

Суть и смысл BDD в том чтобы описывать поведение языком понимаемым человеком без лишних усилий. Таким образом код тестов может служить одновременно документацией к тестируемому коду.
Ну и это просто красиво. После BDD обычные тесты, с ассертами итп, выглядят уродливо.
Речь топикстартера шла об «уродливости», а не о том чтобы написать самый минималистичный тест.

Куча бессмысленных bdd-расшаркиваний — это и есть уродливость.


Суть и смысл BDD в том чтобы описывать тест языком понимаемым человеком без лишних усилий.

  1. "describe foo do it returns bar do expect foo to eq bar end end" гугл переводит как "описать foo сделать это возвращает бар действительно ожидать foo к концу конца эквалайзера".
  2. Никто кроме программистов читать ваши модульные тесты не будет. А программисту лучше читать формально точный програмный код, а не описание "своим языком", которое не полное и зачастую врёт.
  3. Часто, и в данном случае тоже, текстовое описание просто в точности повторяет программный код. В лучшем случае это тавтология, в худшем — описание с кодом разъезжается.
  4. В конце концов это даже не BDD: https://ru.wikipedia.org/wiki/BDD_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

Таким образом код тестов может служить одновременно документацией к тестируемому коду.

В лучшем случае код тестов может выполнять роль примеров в документации, но заменить её не способен, так как документация должна давать общие правила использования, а тесты — это всегда конкретные примеры с конкретными параметрами. Приведённый мной код вполне себе попадает в документацию. Например: https://dlang.org/phobos/std_ascii.html#isAlpha https://github.com/dlang/phobos/blob/master/std/ascii.d#L145


Ну и это просто красиво.

Сравнивать значения через метод to eq, когда в языке есть оператор сравнения == — это ничерта не красиво. Это карго-культ "человеческого" языка.


После BDD обычные тесты, с ассертами итп, выглядят уродливо.

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

А зря. Эстетичность, того, что делаешь, это важно. Если делать уродливые вещи уродливыми инструментами, то в жизни будет много красивого. Непонятно откуда, но, а вдруг?
> Куча бессмысленных bdd-расшаркиваний — это и есть уродливость.

На вкус и цвет все фломастеры разные.

> Никто кроме программистов читать ваши модульные тесты не будет. А программисту лучше читать формально точный програмный код, а не описание «своим языком», которое не полное и зачастую врёт.

В том и дело. «Формально» точный код порой тяжело читать, особенно если нет контекста. Пожалейте коллег.
Описание «своим языком» формально точно так-же «точное», просто написано читаемо.

> Часто, и в данном случае тоже, текстовое описание просто в точности повторяет программный код. В лучшем случае это тавтология, в худшем — описание с кодом разъезжается.

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

> В конце концов это даже не BDD: ru.wikipedia.org/wiki/BDD_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

без комментариев

> В лучшем случае код тестов может выполнять роль примеров в документации, но заменить её не способен, так как документация должна давать общие правила использования, а тесты — это всегда конкретные примеры с конкретными параметрами. Приведённый мной код вполне себе попадает в документацию. Например: dlang.org/phobos/std_ascii.html#isAlpha github.com/dlang/phobos/blob/master/std/ascii.d#L145

документация без примеров — плохая документация

> Сравнивать значения через метод to eq, когда в языке есть оператор сравнения == — это ничерта не красиво. Это карго-культ «человеческого» языка.

Вот только оператор сравнения `==` в некоторых языках может работать не так как ты ожидаешь.
Смысл использования to eq() не просто в самом сравнении, но и составлении правильного текста ошибки:

1) foo() returns bar
Failure/Error: expect(foo()).to eq('bar')

expected: "bar"
got: "zar"

(compared using ==)


Ну и сударь явно не понимает значения слова «карго-культ».

> Ну да, писать 5 строчек кода в место одной — вот где красота. Впрочем, с критериями «красиво/уродливо» вам стоило в художники пойти, а не в программисты. У программистов должны быть иные приоритеты.

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

код порой тяжело читать

Может стоит решать эту проблему, а не писать рядом "своими словами"?


«своим языком» формально точно так-же «точное»

Было бы оно точное, его можно было бы скомпилировать, выкинув весь остальной код.


Ваши «точные», но неструктурированные тесты

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


Вот только оператор сравнения == в некоторых языках может работать не так как ты ожидаешь.

Он работает так же, как в остальном програмном коде. Что ещё от него можно ожидать? Ну а что в некоторых языках нет перегрузки операторов и сравнение всегда по ссылке, так о том и речь, что некоторые языки имеют кривой дизайн, из-за чего приходится добавлять костыли типа to eq.


Смысл использования to eq() не просто в самом сравнении, но и составлении правильного текста ошибки

Все эти красоты не имеют смысла при использовании отладчика, а не отладке через repl.


Вы, сударь, видать любитель однострочников.

Нет. Одна строчка кода и 5 записанных в одну строку — не одно и то же.


Читаемость и поддерживаемость кода — не последний фактор в software engineering.

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


Ну и эстетическая красота — имхо тоже не последняя вещь. С вашим подходом надо всем жить в панельных хрущевках.

Исходные коды — не картинная галерея. Это сугубо утилитарная вещь. А вы не из тех, кто выстраивает из исходников ascii-картинки?

Я рад, что вы подняли вопрос про утилитарность и эстетику. Какой код лучше?


(https://snag.gy/kcErUv.jpg)

А ведь, казалось бы, сугубо утилитарная вещь.

Слева кабель-каналы на стене, уродство. Справа они наверняка спрятаны в стену. Это хорошо.

Главное, настоять на своей точке зрения любой ценой.

Поздравляю, настояли.
Чуть чуть поработаю капитаном. Автор коментария выше намекал на то что вопрос «что лучше?» совершенно бессмысленнен без уточнения для кого собственно лучше.
Вы причину со следствием не путайте. Левый код лучше не потому, что он красивый, а наоборот, красивый он потому, что сделан аккуратно, а не тяп-ляп.
То же самое касается и кода. Выразительный код выразителен не потому, что катарсис и читатель плачет, а потому что всё кратко, понятно и хорошо работает.
Вы точно не ошиблись оппонентом?
Может стоит решать эту проблему, а не писать рядом "своими словами"?

Эта проблема как раз и решается.


Было бы оно точное, его можно было бы скомпилировать, выкинув весь остальной код.

А оно и компилируется. Это обычный DSL.


Он работает так же, как в остальном програмном коде. Что ещё от него можно ожидать? Ну а что в некоторых языках нет перегрузки операторов и сравнение всегда по ссылке, так о том и речь, что некоторые языки имеют кривой дизайн, из-за чего приходится добавлять костыли типа to eq.

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


Все эти красоты не имеют смысла при использовании отладчика, а не отладке через repl.

А ну теперь все понятно. Это еще один аспект TDD/BDD который сударь не вкурил. Тесты используются не только для валидации, а так-же как часть процесса разработки и отладки.


Нет. Одна строчка кода и 5 записанных в одну строку — не одно и то же.

Если 5 строчек дают контекст и читабельность — это лучше чем одна ниндзя-строка которая делает все на свете.


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

Крайне спорное утверждение.


Что легче читать и поддерживать?


message = "foo"  

if (type == "dar" ) {  
    message = "bar"  
} elsif (is_some_other_condition) {  
    message = "har"  
}  

или


error = (type == "dar" ? "bar : (is_some_other_condition ? "har" : "foo"))

Исходные коды — не картинная галерея. Это сугубо утилитарная вещь. А вы не из тех, кто выстраивает из исходников ascii-картинки?

Да, а еще я код форматирую. Каюсь, виновен — жутко не утилитарно.

А оно и компилируется.

Вот это не компилируется: "foo() returns bar". Всё остальное — бойлерплейт и подражание bdd.


ассертом без контекста

О каком контексте идёт речь? Если о том, какая функция тестируется, то тест идёт сразу после функции. Да, даже, если это метод класса.


правильной ошибки

Что в ошибке не правильного?


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

Да нет, это похоже вы не вкурили в "остановку отладчика на исключениях". Когда он останавливается на ассерте — вам и так видно и код сравнения, и сравниваемые значения.


Что легче читать и поддерживать?

Не люблю повторяться:
"Нет. Одна строчка кода и 5 записанных в одну строку — не одно и то же."


Для тех, кто в танке — одна простая строчка кода. Надо ещё формальней? Одна строчка содержащая не более одного оператора.


Да, а еще я код форматирую.

Для красоты? Или есть иные цели?

Не флейма ради, а просто мимо проходил. Ternaries можно форматировать не вот так:


error = (type == "dar" ? "bar" : (is_some_other_condition ? "har" : "foo"))

А вот так:


error = type == "dar"             ? "bar"
      :  is_some_other_condition  ? "har"
      :  is_yet_another_condition ? "zar"
      :                             "foo"
      ;

Отлично читается, по-моему, и существенно компактнее простыней if/elsif/else. Очень подходит для определённого рода ситуаций, когда switch не подходит (или отсутствует, хе-хе), а синтаксис if/elsif займёт больше места, чем собственно условия и присвоения. И если ещё условие нужно будет добавить, то не надо переформатировать, просто строку вставить.


Ну т.е. полная вкусовщина конечно, но мне нравится.

Можно и так, но eq() удобнее потому-что составляет текст ошибки, например:

1) foo() returns bar
Failure/Error: expect(foo()).to eq('bar')

expected: "bar"
got: "zar"

(compared using ==)
pytest в assert'ы пишет всю информацию о том, что случилось и почему «не». Включая имена переменных, дифф между строками (если строки), словарей и т.д.
Видимо какая-то особая python-магия с парсингом исходников тестов :)

Ваш тест на мой взгляд typescript/c# программиста выглядит уродливо

Только благодаря тегам я понял, что это сарказм. Эх, сложно новичкам на хабре

Это старичкам трудно. Мы пишем с сарказмом, а новички не понимают, хотя и для вас тоже пишем. Видимо, надо прямее мысли излагать, но в старости мы уже не можем :-(

UFO just landed and posted this here

Про покрытие неоднократно упоминал в статье.

UFO just landed and posted this here
Это как статический анализ — сложно обойти.

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

Например, супертест со 100% покрытием.
public class A {
    public String fun1(boolean flag) {
       if(flag) return "Ok"
       else return "Error" 
    }
}

...
@Test 
public void testFun1() {
     a.fun1(true);
     a.fun1(false);
     System.out.println("Все работает!");
}

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

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

Вот если на такие тесты натравят, скажем, тулу с мутационным тестированием — Штирлиц будет как ниогда близок к провалу… (хотя и мутационное тестирование тоже не панацея)
UFO just landed and posted this here
UFO just landed and posted this here
Указанноу проблему пытается решить мутационное тестирование. Фреймворки для него, обычно, смотрят какой код покрыт тестами, далее формируют единичные изменения (мутации) и на каждое изменение производят запуск тестов. В результате отображают какие изменение были отловлены (какие мутанты убиты) и какими тестами, а какие мутанты выжили. Но если мутант выжил, то не обязательно проблема в тестах, вполне возможно, что есть избыточные проверки в коде и тесты не могли в принципе упасть. Ну или мутация «задела» какой-нибудь «ассерт».
и предположить что подобные ошибки в тестах были допущены не намеренно

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


системный подход в ловле таких проблем

Про это последний абзац статьи как раз :)


Большинство упомянутых в статье способов, к сожалению, легко находятся хорошим статическим анализатором
UFO just landed and posted this here
Но почему не проверить, что конфиг действительно загрузился (проверив хотя бы 1-2 тестовых значений)? Потом система поменяется и вместо эксепшена она будет возращать пустой конфиг, но все будут верить, что unit тестами она покрыта и более того tools, показывающие покрытие тестами будут утверждать, что все в порядке.
Плюс вы расчитывали, что система выкинет эксепшен, а она просто вернула ерунду (так бывает)
Потом система поменяется и вместо эксепшена она будет возращать пустой конфиг, но все будут верить, что unit тестами она покрыта и более того tools, показывающие покрытие тестами будут утверждать, что все в порядке.

Ват? Вы тестами собираетесь проверять будущее поведение?

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

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

То есть, да если класс поменяли и тесты стали падать тут несколько вариантов: тесты написаны неправильно, изменения ошибочны или изменения ломают прошлую логику класса и нужно внимательно смотреть правильно ли это.
Именно поэтому при серьезном рефакторинге лучше сначала дописать недостающие unit-test'ы, а только потом вносить изменения способные все сломать
проверять значения нужно в другом тесте
Просто на фоне оригинального сообщения ваше сообщение интерпретируется так: вы хотите проверять еще что-то, чего нет в другом тесте. Извините за недопонимание.
UFO just landed and posted this here
Не совсем вас понимаю, вот смотрите есть интерфейс

interface ConfigLoader {
     Config getConfig(File file)
}


Вы предлагаете писать два теста? Один для проверки что не выкидывается exception
private ConfigLoader configLoader = ...

public void testNotThrowException() {
        configLoader.getConfig(fileTestConfig);
}

А второй для проверки, что значение конфига правильное?
private ConfigLoader configLoader = ...

public void testConfigValueIsCorrect() {
        Config config = configLoader.getConfig(fileTestConfig);
        assertEquals(config, expectedConfig);
}

Вам не кажется, что два теста тут будут явно избыточными?

Потому что в одном тесте должно проверяться ровно одно утверждение.

Логично, но учтверждение «что конфиг из файла был загружен корректно» это одно утверждение, а то что не был выкинут exception это уже детали реализации. Иначе по такой логики нужно делать отдельные тесты на то что:
1. getConfig не выкинул checked exception,
2. getConfig не выкинул runtime exception,
3. getConfig не выкинул error,
4. getConfig не вернул null,
5. getConfig не вернул пустые значения во всех полях,
6. getConfig вернул нужные значения в нужных полях,

проверку количества значений. И типов значений. И и самих значений.

В статическом языке достаточно только equal'a, если я правильно вас понял.

Вот только тесты с тысячей ассёртов — куда большее зло чем «пустые» тесты.

Но тысяча тестов с одним ассертом вряд ли лучше. Просто ассертов в любом случае должно быть минимальное кол-во. но с максимальных покрытием возможных случаев.
UFO just landed and posted this here
Лучше — потому что из тысячи тестов упадёт только часть, и будет понятно какая именно часть

По ассерту вполне понятно где дефект. От того, что вы завернёте каждый ассерт в отдельный тест понятности не прибавится.


сломался только этот ассёрт или он и все последующие?

А какая разница?


внезапно вам нужно разбирать не одну спагетти-простыню, а сразу 10

Точно так же разбираете первую попавшуюся простыню. Какие проблемы?

UFO just landed and posted this here
Если же каждый кусок кода покрыт отдельным тестом — а то и не одним — у вас будет более гранулярная картина, где именно и что сломалось (=опять сэкономленное время).

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


auto data1 = GetData1()
auto data2 = GetData2()
auto data3 = GetData3()
auto result = Calculate(data1, data2, data3)
assert(result[1][1]==10)

auto data1 = GetData1()
auto data2 = GetData2()
auto data3 = GetData3()
auto result = Calculate(data1, data2, data3)
assert(result[1][2]==15)

...(тонна тестов)...


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

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


Это позволяет планировать свое время

Тут важнее понимать что именно сломалось и как это чинить, а не сколько тестов упало.


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

Ну да, копипаста на 90% одинаковых тестов — это не простыня :-)

UFO just landed and posted this here
Можно, конечно, запихнуть в этот тест проверку количества значений. И типов значений. И и самих значений. Вот только тесты с тысячей ассёртов — куда большее зло чем "пустые" тесты.

...


Вы просто путаете понятия «утверждение» и «assert». Утверждение может состоять из нескольких ассёртов — главное, чтобы оно выражалось фразой (в идеале вообще названием теста).

Переобулись, замечательно. Happy End.

UFO just landed and posted this here
Потому что в одном тесте должно проверяться ровно одно утверждение.

У вас получается сначала утверждение "загрузка конфига не вызывает ошибок", а потом "из конфига загружается то, что там написано". Но первое не имеет никакого смысла без второго, так как его проходит даже пустая функция. А второе включает в себя первое, как неотъемлемую часть. В результате получается, что при изменении апи, придётся править в 2 раза больше тестов, без какой либо пользы от такого разделения.


Вот только тесты с тысячей ассёртов — куда большее зло чем "пустые" тесты.

Почему?

UFO just landed and posted this here
после определённого развития, по опыту, никто не может сказать, что же именно данный тест всё-таки проверяет

Скоуп требований обычно пишется в названии теста. Если у вас название и содержимое теста разъезжается, то проблема тут не в числе ассертов. С одним ассертом оно точно так же разъедется.


тест с последовательностью операций проверок может прятать случайно созданные антипаттерны типа Sequential coupling

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


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


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


Через год-другой монолитный тест банально превращается в спагетти-функцию.

Давайте не доводить до абсурда и впадать в крайности. Объединять все тесты в один никто не предлагает. Но когда нужно получить значение и прочекать несколько его свойств — довольно глупо копипастить 100500 тестов.


тест-функция из 10000 строк

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


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

Какая разница править один тест 20 раз или править 20 тестов по одному разу?


Вы же экономите место, потраченное на код, и количество тестов.

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

UFO just landed and posted this here

“Многие уже поняли, что от них один вред. Написание тестов отнимает много времени, за которое вы могли бы сделать что-то более полезное.”
Немного странное мышление) Разработка и тестирование — единый процесс. К тому же, на тесты/сценарии можно вешать теги и тестировать их локально, не обязательно ждать результата CI.

То есть остальные советы, как лучше обмануть ревьюера и протащить в продакшен свой говнокод, вас не смущают? :)
  • Можно написать маленькую часть системы как зависимость, и в последующих версиях переопределить assert'ов.
  • Более надежный способ, написать библиотеку для генерации юнит тестов.

Добавлю, чтобы не спалиться на статическом анализаторе, создайте тесты в отдельном модуле и отключите его анализ. Пример для отключения sonar в Maven: <sonar.skip>true</sonar.skip>

Шутки шутками, но вообще существуют же автоматическое генерирование юнит-тестов. Например, Rational Test Realtime или тот же CANTATA.

Меня удивляет, что инструментальная обвязка для юнит-тестов во фронтэнде находится в каменном веке и никто ничего для этого не делает. Писать юнит-тесты для например ангуляра сегодня это 30% написания самого теста, 30% написания мок-данных и 40% — изоляция теста, инъекция зависимостей и прочее. Почему мои мок-данные не генерируются из интерфейсов и классов Typescript? Почему при добавлении публичной функции в сервис, никто не добавляет автоматически эту функцию в spy-object? Почему при инъекции нового сервиса в ангуляре этот же сервис не инъектится и в TestBed? Где генерация тестов с проверками на null и undefined?
Меня удивляет, что инструментальная обвязка для юнит-тестов во фронтэнде находится в каменном веке и никто ничего для этого не делает.

Делают. Вы просто не смотрите.


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

Ангуляр — самый многословный и тормозной фреймворк на сегодня. Странно ожидать от него лаконичных тестов. Кстати, лайфхак. Если вас тоже напрягает, что каждый ангуляровский тест исполняется сотни миллисекунд, то знайте, что происходит это из-за инициализации TestBed перед каждым тестом. И если при старте приложения подождать пока Ангуляр проинициализирует свой DI ещё можно, то делать это для каждого теста — мучительно долго. Если отключить сброс TestBed-а, то тесты начинают летать. Мне удалось ускорить тесты в 10 раз. Но тут надо иметь ввиду, что инстансы сервисов, получаемых через DI будут одни и те же. Благо достаточно замочить их один раз при инициализации TestBed и больше о них не думать. По ссылке выше используется именно такой подход — $ — это аналог TestBed из Ангуляра, в котором автоматически замочены все недетерминированные/внешние/асинхронные API. Например, мок адресной строки, который берёт/хранит урл не в адресной строке браузера, а в локальном свойстве.


Почему мои мок-данные не генерируются из интерфейсов и классов Typescript?

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


Почему при добавлении публичной функции в сервис, никто не добавляет автоматически эту функцию в spy-object?

Как автоматика поймёт надо ли это делать? Ну и вообще полезность шпионов довольно сомнительна. Кода с ними меньше не становится. Зато появляется дополнительное весьма ограниченное API, которое нужно знать, иначе ничего в тесте не понятно.


Почему при инъекции нового сервиса в ангуляре этот же сервис не инъектится и в TestBed?

Давно не трогал Ангуляр, но емнип там нужно TestBed инициализировать тем же модулем, что и всё приложение, — тогда все сервисы будут подсасываться из него.


Где генерация тестов с проверками на null и undefined?

Этим сам TS умеет заниматься при включении более строгих флагов компиляции.

Делают. Вы просто не смотрите.

Я ничего не понял, чем этот код прекрасен?
Как автоматика поймёт надо ли это делать?

Если метод публичный — то надо.
Ну и вообще полезность шпионов довольно сомнительна.

Мне на самом деле все равно, пусть не шпионы, пусть полностью мокнутые объекты. Главное — не я их должен писать, они должны генерироваться и обновляться на основе моего кода.
тогда все сервисы будут подсасываться из него.

Дык тогда у нас изоляция пропадает, так не пойдет.
Я ничего не понял, чем этот код прекрасен?

Тем, что там нет 70% бойлерплейта.


Если метод публичный — то надо.

В общем случае такая замена метода может сломать поведение. Поэтому автоматически такое делать для всего — опасно.


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

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


Дык тогда у нас изоляция пропадает, так не пойдет.

Ускорение тестов с 10с, до 1с того стоит. Кстати, в приведённом мной примере изоляция не страдает, так как инициализация контекста там ничего не стоит. Это чисто ангуляровский компромис.

Тем, что там нет 70% бойлерплейта.

Хорошо, разберусь. Еще вопрос — там же туду апп, а мне надо тестировать с объектами, каждый из которых имеет 35 полей, знаете что-нибудь толковое для моканья данных?

Моканья с какой целью? Обмануть тайпчекер? Ну так:


const taskMock = {} as unknown as Task
моканья с целью получения среднего типичного объекта, пригодного для тестирования. Что-то типа такого, но не для json schema, а для typescript. Я ему — интерфейс, он мне — рандомный объект с хорошо заполненными полями. Выигрыш нескольких минут моего времени. Ваш пустой объект у меня в тесте падать будет как только я попробую обратиться к полю Task.assignedPerson.id, например.

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

А генератору не обязательно запускаться каждый раз перед тестом, это может быть штука, которая работает единоразово, генерит бойлер для моков, а потом я вношу изменения руками. Например, сканирующий кодовую базу скрипт, выплевывающий мок-файлы для заданных интерфейсов, который я закоммичу в гит.
А когда вы измените интерфейсы, он интеллектуально обновит ваши правки?
При изменении интерфейсов IDE мне покажет, что этим изменением было затронуто, так что можно сказать, что в основном да. В худшем случае — меня предупредит компилятор, или если совсем швах — у меня упадет тест.
острые советы от Григория Остера ;-)
Sign up to leave a comment.

Articles

Change theme settings