Pull to refresh

Comments 38

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

Отсюда можно сделать и выводы:
— Спасибо Джеймс за ваше мнение, но выводы мы сделаем сами.

Только это? Прежде чем читать, я пролистал немного материал. Это же портянка текста без кода с однотипным унылым оформлением. Автор перевода, походу, вообще не понимает материал. Точно такое же оформление перевода (шапку) я наблюдал в недавнем переводе статьи о том, что CSS - строго типизированный ЯП от пользователя, ушедшего в минусы. И у меня складывается ощущение, что некто, или нечто, стремится набить репутацию на Хабре.

Если есть замечания, озвучивайте (исходя из содержания и оформления оригинальной статьи, пожалуйста). Буду признателен, исправлю. Пока же, как говорил классик: «У меня складывается ощущение...», что вы ошибаетесь.
Слабо оформленная портянка текста без образцов кода — это типа не замечание?
Это перевод статьи, т.е. копия оригинала на другом языке, поэтому у вас есть замечательная возможность отписаться об этом автору, чтобы он в будущем радовал ваш глаз.
Вы хотите сказать, что не вы выбирали статью для перевода и размещения на Хабре?
Планирую перевести продолжение данной статьи: rbcs-us.com/documents/Segue.pdf. Там чуть больше «картинок», но всё так же нет примеров кода. Идеи «на подумать» именно это считаю важным в статьях.
Ну, сам автор, я думаю, код пишет лучше, чем статьи. Кликбейтный заголовок и растекается мыслью по древу. Я могу быть сто процентов согласным с его идеей, но она нифига не понятна массам. Я могу читать «Юнит-тесты не нужны», и понимать «тесты не нужны, и вы не должны за них платить». При этом, лишь опыт позволяет мне понять, что истерия по поводу создания целой подсистемы юнит-тестов — лишь фанатизм. Думаю, перевод статьи стоит оставить тем, кто имеет хороший опыт программирования, и может понять о чём говорит автор. Для Хабра было бы лучше набросать вольное изложение с примерами кода. Благо, сейчас на рынке очень много команд, которые прямо тащатся от избыточного юнит-тестирования.

Давайте на хорошо написанные юнит-тесты:


#[pure]
#[ensures(result >= a && result >= b)]
#[ensures(result == a || result == b)]
fn max(a: i32, b: i32) -> i32 {
    if a > b {
        a
    } else {
        b
    }
}

#[ensures(result == max(a, max(b, c)))]
fn max3(a: i32, b: i32, c: i32) -> i32 {
    if a > b && a > c {
        a
    } else {
        if b > c {
            b
        } else {
            c
        }
    }
}

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


Вот это — хорошо написанный юнит-тест.


Для заинтересовавшихся: https://viperproject.github.io/prusti-dev/user-guide/intro.html

Ну а дальше что?

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

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

Как на счет ситуации, когда степеней свободы не 3, а 30 или 100? Что специфицирует корректное поведение кода? Сам код, текстовое описание, или юнит-тесты? Кто проверяет полноту и корректность спецификации?

Единственные случай хорошо написанного юнит теста – это тот, который сэкономил время разработчиков. Но я вижу, как многие разработчики систематичеески переоценивают пользу юнит-тестов, создавая из них карго-культ ("баги в коде? нужно писать больше тестов!").

Это не две реализации. Это набор утверждений о свойствах, которыми должна обладать функция.


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


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


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


Ещё они дают code coverage, что для (условного) Rust менее полезно, а для питона — критически важно, потому что словить опечатку внутри блока exception — как нефиг делать.

#[ensures(result >= a && result >= b)]
#[ensures(result == a || result == b)]
Уверены, что это определение максимума? Вообще, в сложном случае утверждения о свойствах тоже потребуют доказательств.

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

Я как-то пытался в своём маленьком проекте по решению Адвент-календаря приблизиться к 100% покрытию (на Расте).

Не смог. Эта теория хорошо работает до первых цепочек с filter и map. Потом тестирующий фреймворк находит условия, которые никогда не выполняются. И помечает их как непокрытые. Или же те условия, которые я явно проверяю не может обнаружить в силу каких-то там особенностей анализа ветвлений в нативном коде.
Я считаю, что тестирование стоит привносить в проект, но без фанатизма.

Ну, как админ, я вообще сторонник red-green подхода (не в том смысле как его программисты используют, а в том смысле, что багфиксы начинаются с написания теста).


Принцип простой: есть багрепорт/баг — пишется тест, чтобы был красным. Фиксится, чтобы стал зелёным. Всё, больше такого бага никогда не будет. Тесты по мотивам реальных багов самые эффективные, потому что они ловят то, что уже сломалось (т.е. вероятность поломки чего выше, по сравнению с всем остальным). Я, повторю, не совсем программист, так что у нас поломки (для которых тесты) специфичные, но этот подход офигенно работает.


Вообще, code is liability, и это в полной мере касается и тестов. Тесты тоже liability, и их кто-то должен сопровождать. И это стоит денег. Так что да, без фанатизма.

>Принцип простой: есть багрепорт/баг — пишется тест, чтобы был красным.
Очень хороший вариант. И с экономической точки зрения тоже. Нет багов в продакшн? А тогда зачем тесты (еще, или вообще), если качество уже достаточно высокое?
Отличный подход. И это скорее всего будет интеграционный тест.

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

А если они "воспроизводятся" только на данных и нагрузке которые на проде?
А если сломалось только потому что "на проде забыли запись в ДБ добавить" (да, такое бывает).


А все эти ваши "2 + 2 = 4 YAY" глупо выглядят в реальности

Значит, им прийдёт нагрузка из продакшена. Я напоминаю, я админ, и мне воспроизвести нагрузку, в общем случае, можно в разумные усилия. Иногда это сложно (например, если нам надо 100500 SSL'ей делать, то это либо ферма, либо спецкарты для криптухи), но в общем случае, реализуемое. Для понимания, один из наших интеграционных тестов создаёт набор автономных систем, которые через два независимых роутера присылают свои маршруты на SUT (system under test) с одной стороны, а с другой стороны терминируют vxlan mesh внутри бонда в генераторы нагрузки. Ну, да, 4 минуты ансибла, плюс взорванные мозги сотрудников по разработке "карманного интернета", но у нас есть mock_internet в CI для соответствующего продукта.

Насчёт "на проде забыли запись добавить". Кто забыл? Робот? Плейбука, которая продакшен разворачивает? Вот если забыли, на это тест и будет.


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


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

Кстати, prusti умеет проверять недосягаемость ветвлений.

Вот у функции max3 у вас именно что получилось две реализации. Если требуется чтобы max3(a,b,c) было равно max(a,max(b,c)) — то в подавляющем большинстве случаев именно так и следует писать:


fn max3(a: i32, b: i32, c: i32) -> i32 {
    max(a, max(b,c))
}

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

Имхо, редко встретишь работающие юнит-тесты, после того как программист написавший их покинул проект.

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

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


Я давно заметил, что настоящего ООП в моих проектах мало осталось. И не в последнюю очередь из-за юнит-тестов. Классы чаще сборище публичных методов, что бы проще было тестировать. А Service-, Repository-Классы (java, spring) обязательно чрез интерфейсы, что бы удобнее было их мокать в юнит-тестах. Больше эти интерфейсы собственно нигде и не используются в коде, кроме как в папке test.

Так что test driven design можно сказать победил, по крайней мере в проектах где я сижу.
UFO just landed and posted this here
Сила привычки, да и хотя бы что то оставить из OOP :)

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

Это в какиеж такие времена? Когда я лично писал на фортране (а это было в 70-х и 80-х годах прошлого века), даже понятие юнит тестов еще не оформилось. Насколько я помню они стали популярны как раз после появления XP и книжки Бека, т.е. где-то самый конец 90-х. Fortran же, если на то пошло, появился в 1957.
UFO just landed and posted this here
>На фортране писали очень долго
Так а я о чем? Появился в 1957, поэтому «во времена Фортрана» — ну это как-то очень расплывчато. Да и сегодня в общем пишут.
UFO just landed and posted this here
Большие функции, для которых 80% покрытие было невозможным, разбивались на множество более мелких, для которых 80% уже было тривиальным. Такой подход повысил общий корпоративный показатель зрелости команд всего лишь за один год, потому как вы обязательно получаете то, что поощряете.

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

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

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

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


Вот вам синтетический пример:


function is_prime(n) {
    return is_prime_generic(n, () => check_primes(enumerate_possible_divisors(n)));
}

function check_prime(n, d) {
    return n % d != 0;
}

function* enumerate_possible_divisors(n) {
    for (let d = 3; d*d <= n; d+=2) {
        yield d;
    }
}

function is_prime_generic(n, next) {
    if (n < 2) return false;
    if (n == 2) return true;
    return next(n);
}

function check_primes(n, seq) {
    for (let d of check_primes) {
        if (!check_prime(n, d)) return false;
    }
    return true;
}

Кажется, он должен очень хорошо тестироваться. Но читать всё-таки проще вот этот код:


function is_prime(n) {
    if (n < 2) return false;
    if (n == 2) return true;
    for (let d = 3; d*d <= n; d+=2) {
         if (n % d == 0) return false;
    }
    return true;
}
Сегодня у нас есть хорошие статические анализаторы кода, которые выдадут вам сотню разных метрик качества кода без необходимости написания юнит тестов. Я согласен с прагматичным использованием юнит тестов для багфиксов + покрытие каки-то фундаментальных критических функций, изменение которых затрагивает бОльшую часть системы.
Вопрос: “Много ли информации содержится в этом тесте?” Другими словами, если “1” — это успешно выполненный тест, а “0” — упавший, тогда сколько будет информации в следующей строке результатов:
11111111111111111111111111111111
Существует несколько возможных ответов, обусловленных видом применяемого формализма, хотя большинство из них не верны. Наивный ответ — 32, однако, это биты данных, а не информации

По формуле Хартли 32, а как нужно подсчитывать?
Если вероятность успешного прохождения теста равняется 100%, тогда, по определению теории информации, этой информации нет вообще.

Но ведь она равняется 50%, а не 100%.
По формуле Хартли 32, а как нужно подсчитывать?

Обычно информацию по Шеннону считают.


Но ведь она равняется 50%, а не 100%.

С чего бы?

Тест либо успешен (1), либо нет (0). Поэтому 50% и я считаю по Хартли
Ага, развели тут классы и полиморфизм какой-то… Интерфейсы напридумывали… Назад к ФП и Фортрану! ;)
А если серьезно — то я не представляю как можно писать без юнит-тестов.
Если вы хотите сократить объем тестов, первое, что следует сделать, — это обратить внимание на те, которые ни разу за год не упали, и рассмотреть вопрос об их удалении. Такие тесты не предоставляют вам никакой информации, или, по крайней мере, очень мало информации.

Если вы хотите сэкономить на охранной сигнализации, то первое, что нужно сделать — это демонтировать сигнализацию на объектах, на которых она ни разу за год не сработала. Эта сигнализация не предоставляет вам никакой безопасности.
Sign up to leave a comment.

Articles