Всех нас на работе то и дело пытаются заставить писать юнит-тесты. Многие уже поняли, что от них один вред. Написание тестов отнимает много времени, за которое вы могли бы сделать что-то более полезное. Если тест неожиданно начинает падать, ломается сборка на сервере непрерывной интеграции, не выкатывается вовремя релиз, бизнес теряет деньги и крайним оказываетесь вы, автор упавшего юнит-теста. При рефакторинге тесты причиняют головную боль, потому что начинают падать и приходится с этим разбираться.
Тем не менее злые начальники требуют больше тестов, говоря о так называемом «контроле качества». Особо хитрые менеджеры даже считают покрытие и не отпускают вас с работы, пока оно не будет достигнуто. Ваш код заворачивают на ревью, если в нём нет тестов или они чем-то не понравились. Сплошное расстройство!
Что же делать?
К счастью, есть способы писать надёжные юнит-тесты, которые никогда не упадут. Эти способы придумал не я, их успешно практикуют в ряде опенсорсных проектов. Все примеры, которые я приведу, взяты из реального кода. Поэтому нет причин и вам не воспользоваться тем, что уже применяется на практике другими разработчиками!
Самый первый и очевидный способ: ничего не проверять в юнит-тесте. Вот простой пример:
public void testSetFile() {
System.out.println("setFile");
File f = null;
BlastXMLParser instance = new BlastXMLParser();
instance.setFile(f);
}
Начальник требует стопроцентного покрытия? Отлично, протестируем, что пустой конструктор по умолчанию и тривиальный сеттер не падают с исключением. То что сеттер действительно что-то установил проверять не будем, тем более что по факту мы null перезаписали null'ом. Это надёжно, такой тест падать не должен.
Слишком банально и не удаётся такое пропихнуть на ревью? Можно поступить хитрее:
@Test
public void getParametersTest() {
List<IGeneratorParameter<?>> parameters = generator.getParameters();
containsParameterType(parameters, AtomColor.class);
containsParameterType(parameters, AtomColorer.class);
containsParameterType(parameters, AtomRadius.class);
containsParameterType(parameters, ColorByType.class);
...
}
Выглядит солидно, будто что-то действительно проверяется. Взглянем однако на метод containsParameterType:
public <T> boolean containsParameterType(List<IGeneratorParameter<?>> list, Class<T> type) {
for (IGeneratorParameter<?> item : list) {
if (item.getClass().getName().equals(type.getName())) return true;
}
return false;
}
Изящно, правда? Оба метода по отдельности выглядят разумно, но вместе они ничего не проверяют. Такое может прокатить, особенно если методы закоммитить по отдельности и отправить разным ревьюверам. Тем более что они в разных классах!
Однако так долго не протянешь. Злое начальство заподозрит неладное, не увидев никаких ассертов в коде. Ассерты всё-таки добавлять стоит. Но, например, так, чтобы они не выполнялись. Вот грубый подход:
for (int i = 0; i < 0; i++)
{
Assert.assertTrue(errorProbabilities[i] > 0.0d);
}
Цикл на 0 итераций. Такое пропустит разве что сильно пьяный ревьювер. Однако следующий вариант гораздо изящнее:
List<JavaOperationSignature> sigs = new ArrayList<>();
List<JavaOperationSignature> sigs2 = new ArrayList<>();
for (int i = 0; i < sigs.size(); i++) { // делаем вид, что заполняем списки
sigs.add(JavaOperationSignature.buildFor(nodes.get(i)));
sigs2.add(JavaOperationSignature.buildFor(nodes2.get(i)));
}
for (int i = 0; i < sigs.size() - 1; i++) { // делаем вид, что сравниваем
assertTrue(sigs.get(i) == sigs.get(i + 1));
assertTrue(sigs2.get(i) == sigs2.get(i + 1));
}
Тут уже многие ревьюверы не заметят подвоха! Оба цикла ни разу не выполняются, потому что граница — размер пустого списка. Берите на заметку.
Предположим, ваш коллега десять лет назад написал тест, проверяющий, что метод кидает исключение. Сделал он это старомодным способом, через catch. Прекрасно, допишите любых новых ассертов в конец метода. Никто не заметит, что они не выполняются:
try {
getDs().save(e);
} catch (Exception ex) {
return; // нормальный выход из теста здесь!
}
// Следующая строчка выполнится, если что-то пойдёт не так
Assert.assertFalse("Should have got rejection for dot in field names", true);
// А это не выполнится никогда
e = getDs().get(e);
Assert.assertEquals("a", e.mymap.get("a.b")); // Но никто этого не заметит!
Assert.assertEquals("b", e.mymap.get("c.e.g"));
Ваши менеджеры совсем обнаглели и смотрят на покрытие не только основного кода, но и тестов? Теперь они замечают такие штуки? Ладно, и с этим можно бороться. Будем писать ассерты, которые проверяют всякую ерунду. Например, что свежесозданный объект не равен null:
Assert.assertNotNull(new Electronegativity());
Если оператор new
у вас возвращает null, то у вашей виртуальной машины серьёзные проблемы. Поэтому такой ассерт надёжен как скала. Хотя, конечно, опытному ревьюверу сразу бросится в глаза. Более хитрый способ обмануть систему — проверить булево значение:
DocumentImplementation document = new DocumentImplementation(props);
assertNotNull(document.toString().contains(KEY));
assertNotNull(document.toString().contains(VALUE));
Благодаря автобоксингу примитивный boolean
заворачивается в объектный Boolean
, который, конечно, никогда не будет нуллом. Этот ассерт уже не так бросается в глаза, честно выполняется и ему совершенно наплевать, true там вернётся или false. Подобный фокус работает и с другими примитивными типами:
Assert.assertNotNull("could not get nr. of eqr: ", afpChain.getNrEQR());
В отрыве от контекста код выглядит вполне разумно. Ревьюверу придётся проявить истинную дотошность, чтобы заметить, что метод getNrEQR
возвращает примитивный int
и поэтому такой ассерт не может упасть.
Ещё отличный способ ничего не проверить — написать длинное сообщение к ассерту с конкатенацией разных компонентов, а второй аргумент вообще убрать:
Assert.assertNotNull("Attempt to test atom type which is not defined in the " +
getAtomTypeListName() + ": " + exception.getMessage());
Видите? Кажется, раз у нас длинное-длинное сообщение, то проверяется что-то серьёзное. На самом деле проверяется, что это самое сообщение не равно null, чего не может быть, потому что конкатенация строк в джаве всегда выдаст ненулевой объект.
Вообще, конечно, если хочется бросить пыль в глаза, то assertNotNull
ваш лучший друг. Но не единственный друг! К примеру, assertEquals
прекрасно можно использовать, чтобы сравнивать число с самим собой:
Assert.assertEquals(ac2.getAtomCount(), ac2.getAtomCount());
А если вас поймают за руку, всегда можно оправдаться, что вы проверяли стабильность метода getAtomCount
. Может кто-нибудь вместо простого геттера туда генератор случайных чисел засунет!
Если вы работаете с типом double, то самое время вспомнить про сравнение с NaN. К сожалению, assertNotEquals
тут вам не помощник, он слишком умный. Но всегда можно использовать assertTrue
:
Assert.assertTrue(result1.get(i) != Double.NaN);
Как известно, NaN ничему не равен, даже самому себе, а потому такое сравнение всегда истинно.
Также assertTrue
полезен для очевидных instanceof-проверок:
Assert.assertNotNull(cf.getRealFormat());
Assert.assertNotNull(cf.getImaginaryFormat());
Assert.assertTrue(cf.getRealFormat() instanceof NumberFormat);
Assert.assertTrue(cf.getImaginaryFormat() instanceof NumberFormat);
Методы getRealFormat
и getImaginaryFormat
и так возвращают NumberFormat
, так что instanceof проверяет разве что неравенство нуллу. Но на нулл мы и так проверили выше. Таким нехитрым образом число ассертов можно увеличить вдвое.
Есть ещё ряд способов, по которым я быстро не нашёл примеров. Скажем, можно использовать метод assertThat
из AssertJ и не воспользоваться результатом (например, assertThat(somethingIsTrue())
вместо assertThat(somethingIsTrue()).is(true)
). Можно завернуть текст теста в большой try { ... } catch(Throwable t) {}
так чтобы поймать и проигнорировать AssertionError
. Такого же эффекта можно добиться хитрее. Например, отправить ассерты в фоновый поток через CompletableFuture.runAsync()
и не сджойнить результат. Я уверен, вы и сами придумаете множество способов, чтобы противостоять менеджерскому произволу.
Но будьте осторожны. Начальство ни в коем случае не должно узнать про статический анализ. Иначе всё зря. Большинство упомянутых в статье способов, к сожалению, легко находятся хорошим статическим анализатором. Так что т-с-с-с!
Комментарии (136)
Hokum
04.01.2019 15:23+1Мутационное тестирование в проекты! :) И уже делать псевдо покрытие кода становится чуть сложнее. Хотя если человек не хочет писать тесты, то всегда найдет способ как их не писать :)
lany Автор
04.01.2019 16:02Это правильно, хотя не всегда удаётся настроить так, чтобы работало хорошо и быстро и не раздражало. Но надо стремиться, конечно.
amarao
04.01.2019 15:46-3Почему тесты на 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)
lany Автор
04.01.2019 16:11+4Вы же шутите, да? Вы же взрослый человек и понимаете, что это дело привычки, а семантика одна и та же выражается. Это всё равно что говорить, мол русский язык уродливо выглядит, все эти "Я тебя люблю", то ли дело английское "I love you".
amarao
04.01.2019 17:05-3Нет уж. Не "Я тебя люблю", а
abstract_factory(я, ты).builder(verb).builder(любить).make_possesive().to_sting()
. ВместоI.love(you)
.asm0dey
04.01.2019 14:22В котлине вы можете сделать I.love(you), но выглядит так, как будто love какое-то действие произведёт над you, Но нет. Поэтому красивве будет me.inLove = you
lany Автор
04.01.2019 16:15+4А холивара ради можно вот что обсудить. Я утверждаю, что чтобы протестировать API на динамически типизированном языке, вам потребуется вдвое больше кода. Потому что надо проверить, что каждый метод возвращает результат нужного типа и кидает исключение (или иным способом сигнализирует об ошибке) на параметры некорректных типов. То что в языках вроде Java проверяется компилятором и требует ноль тестов, в Питоне выродится в ужасный бойлерплейт. Как вы решаете эту проблему?
alexesDev
04.01.2019 16:28+3А на null вы все проверяете в каждом тесте?
lany Автор
04.01.2019 16:32+2В текущем проекте мы используем аннотации вроде
@NotNull
и annotation processor, который превращает их в рантайм-проверки. Это эффективно решает проблему и избавляет от необходимости проверять на нуллы. Но вы ткнули в больное место, конечно, за это плюсик!alexesDev
04.01.2019 16:35+1Я бы не сказал, что проверки на null — это больное место. Просто писать код и ожидать, что так просто методы null не возвращают — нормально. В любом динамическом языке можно везде писать @returnString аннотацию или вроде того, но никто не пишет.
Если такое нужно писать, то обычно это не проблемы тестов, а в принципе пахнущий год.lany Автор
04.01.2019 16:44Ну так нулл один, и действительно разумно ожидать, что его не возвращают. А типов много, и тогда разумно ожидать что? Методов, возвращающих строки, должно быть очень мало. Вот метод getName() вернёт строку или объект Name? Неочевидно, чего ожидать.
alexesDev
04.01.2019 16:47Кажется валидный java код в последних версиях, тот тоже не ясно
var name = person.getName();
Так же не ясно что тут происходит
person.getName().toLowerCase();
Это метод строки или объекта Namelany Автор
04.01.2019 16:56+2Это статически известно на этапе компиляции, можно всегда посмотреть в IDE или javadoc. Можно контролировать типы, например, выносом интерфейсов API в отдельный модуль с особыми правилами внесения изменений. Изменить тип можно только изменением декларации, никакое изменение тела метода тип не поменяет. Да, возможны редкие случаи, когда изменение типа метода не сломает весь остальной код, но это вполне нормально. В динамическом языке изменение тела одного метода может повлиять на результат других (которые делегируют к этому). Если делегация условная, другой метод может после такого изменения неожиданно возвращать разные типы в разных ситуациях. Я не понимаю, как можно уверенно рефакторить проект на динамическом языке, не обложившись тестами на типы аргументов и результатов методов.
alexesDev
04.01.2019 17:16С рефакторингом согласен, но даже со статической типизацией без тестов я не буду рефакторить, потому что типы не гарантируют корректную логику (толку мне от того, что getName вернет строку, если она всегда пустая). Поэтому все тоже самое — тестировать бизнес логику, а не аргументы и тип результата. Если брать JS, то тест
expect(person.getName()).toBe('Alex');
Вполне успешно тестирует и тип и поведение. Я пишу на js/js+статические типы/ruby/go/c++. В проектах крайне мал процент ошибок по типам… много ошибок логики, чуть меньше с null типами.
На динамических языках крайне сложно делать нормальные IDE, это да. Но много кому IDE не нужны просто, там обычно и используются динамические языки.
У меня тут вывод покрался… «если у вас проект на 10к+ строк кода, то лучше бы использовать язык со статической типизацией, потому что банально IDE очень сильно будет выручать». А для небольших проектов плюшки динамики очень сильно помогают, а минусы сильно не болят.nohuhu
04.01.2019 01:33У меня тут вывод покрался… «если у вас проект на 10к+ строк кода, то лучше бы использовать язык со статической типизацией, потому что банально IDE очень сильно будет выручать». А для небольших проектов плюшки динамики очень сильно помогают, а минусы сильно не болят.
Мой 6-летний опыт поддержки, развития, и многократных глубоких рефакторингов JavaScript фреймворка на 1.5 млн строк утверждает: и на крупных проектах процент ошибок по типам так же мал, и минусы динамического/слабого типизирования так же не болят. Надо хорошо тестировать, вот и всё.
Ждём выступление Джавистов: Пфе, вот если бы по-настоящему крупный проект!.. :)
vedenin1980
04.01.2019 01:44Так в том-то и дело, что на каком-то TypeScript можно было потратить на тесты значительно меньше времени (так тестировать статику проще), плюс IDE в разы лучше делает рефакторинг у статических яхыков, поэтому сомнительно, что динамика в таком случае даст плюсы за счет упрощения синтекса языка.
nohuhu
04.01.2019 03:40Так в том-то и дело, что на каком-то TypeScript можно было потратить на тесты значительно меньше времени
Тестирование бизнес-логики займёт на каком-то TypeScript ровно столько же времени и усилий, что и на каком-то JavaScript. Тестировать же типы принимаемых аргументов и возвращаемых значений не имеет смысла, поэтому никакой экономии вы не увидите.
IDE в разы лучше делает рефакторинг у статических яхыков
Голословное утверждение. На чём оно базируется?
динамика в таком случае даст плюсы за счет упрощения синтекса языка
Не совсем понятно, какой язык вы имеете в виду. Основные преимущества динамической типизации обычно находятся далеко не в упрощении синтаксиса.
alexesDev
04.01.2019 12:27Мда. И откуда появился миф «в динамическом языке нужно писать тесты на типы» не понятно, крайне печально такое постоянно слышать.
> Голословное утверждение. На чём оно базируется?
А это реально так. Нельзя просто ткнуть в поле и переименовать его во всем проекте сразу… вроде name -> firstNamenohuhu
04.01.2019 23:50А это реально так. Нельзя просто ткнуть в поле и переименовать его во всем проекте сразу… вроде name -> firstName
IDE не пользуюсь, но многие коллеги без них жить не могут. И лёгкость переименования переменных и полей является самым частым (и обычно, единственным) аргументом в пользу IDE в неизбежных холиварах на кухне.
Я только плечами могу пожать. В моей практике такие переименования по всему проекту встречаются крайне редко, поэтому особого внимания не заслуживают.
akhmelev
05.01.2019 01:08+1Классика.
Вы просто пишете на JavaScript и ПОЭТОМУ он лучший. Не будет странным наблюдать, то же самое у всех ))). Но да, язык при этом может резко измениться ;). Типы и IDE это вполне себе удобство для кого-то, что плохого? Опционально же все на свете.
Пример-аналог: когда-то в сссрах я писал не то что без IDE, а прямо в кодах К580ИК80 и на бумажке, т.к. под мой первый ПК Орион128 тупо не было редактора, ассемблера и дизассемблера. Их то я собственно и писал, и страшно горд своей тогдашней крутизной до сих пор. И даже тесты были, о как, в 1988 кажется. Но не смешно ли это? Вроде бы как смешно, не находите?
Чем-то позиция фтопку типы, фтопку IDE, крутота наше все может быть фантомно близка каждому разработчику. Особенно если у него что-то получилось. Но всерьез считать это крутым советом… Мир будет скорее против. И будет прав. См. tiobe например.
ЗЫ. А может типизация — это заговор мировой закулисы? Или и вовсе одного человека, скажем Андерса Хейлсберга. TurboPascal, Delphi, J#, .Net, C# и о ужас TypeScript — все он, все он…
vedenin1980
05.01.2019 03:54В моей практике такие переименования по всему проекту встречаются крайне редко, поэтому особого внимания не заслуживают.
А моей практике они встречаются по несколько раз на дню (на Java) и ни разу не встречались на JavaScript за 5 лет опыта. Угадаете почему? Нет, не потому что ненужны, а потом вместо пары секунд, требуют пары часов. Просто когда у человека не было топора всю жизнь, он может искрене верить, что ножом куда удобнее рубить дрова. Да и вообще зачем дрова рубить и так как-нибудь сгорят.
(и обычно, единственным) аргументом в пользу IDE в неизбежных холиварах на кухне.
Вы просто видимо не понимете, что такая хорошая IDE на статически типизированым языке. Переименование полей это ерунда, вот возможность полностью поменять всю архитектуру на порядки быстрее + умное автозаполнение + умное обнаружение самых разных потенциальных ошибок, позволяют программировать принципиально по-другому, когда рефаторинг всего проекта может выполнятся непрерывно.nohuhu
05.01.2019 05:40А моей практике они встречаются по несколько раз на дню (на Java) и ни разу не встречались на JavaScript за 5 лет опыта. Угадаете почему? Нет, не потому что ненужны, а потом вместо пары секунд, требуют пары часов.
Я начну с другого: что такое ужасное у вас творится с архитектурой проекта, чтобы массовые переименования полей/переменных по всему проекту требовались по нескольку раз на дню?
Просто когда у человека не было топора всю жизнь, он может искрене верить, что ножом куда удобнее рубить дрова. Да и вообще зачем дрова рубить и так как-нибудь сгорят.
Вы читаете куда-то между строк. См. выше: многие мои коллеги по цеху JavaScript пользуются различными IDE, которые обеспечивают эту самую лёгкость переименования, бла бла. И усиленно пытаются (ну, пытались раньше) продать мне эту идею, хотя на практике пользуются ею крайне редко. Потому что обратная совместимость, когнитивная нагрузка, да и просто банальный вопрос "зачем?"
Вы просто видимо не понимете, что такая хорошая IDE на статически типизированым языке.
Отчего ж, вполне понимаю. Несколько лет разработки на Pascal и C++ даром не проходят, хоть и давно это было.
Переименование полей это ерунда, вот возможность полностью поменять всю архитектуру
Вот тут я аж поперхнулся. Если у вас лёгкость полной замены архитектуры проекта является весомым аргументом в пользу выбора языка программирования, то мне даже добавить нечего. Извините, но мы с вами, похоже, на разных планетах живём и общий язык найти вряд ли удастся.
rustacean137
05.01.2019 00:30> IDE в разы лучше делает рефакторинг у статических яхыков
В строго статически типизированных языках вам и компилятор подскажет, а вот в динамике как раз без IDE сложновато.
asm0dey
04.01.2019 14:25+1и это не говоря о том, что var может быть только локальным. Вернуть var нельзя, а значит мы всегда знаем какой типа в итогевернётся. И так как в джаве не может спонтанно возникнуть кастомного метода в рантайме у объекта — никаких неожиданностей никогда нет. О перегрузки по возвращаемому значению в джаве нет тоже.
maxim_ge
04.01.2019 12:06>мы используем аннотации вроде NotNull
А как тестировщик удостоверится, что NotNull написан в нужном месте и, главное, не пропал при рефакторинге?vedenin1980
04.01.2019 12:59Можно настроить почти любой статический анализатор, который не даст вам вернуть потенциальный null в методе, где нет аннотации
@Nullable
или наоборот выдаст предупреждения на методы где null невозможен, но@NotNull
не стоит.maxim_ge
04.01.2019 13:15Я имел ввиду использование в аргументах, типа:
public void setX(@NotNull final Object aX )
Не пойму, как ту использовать статический анализатор?vedenin1980
04.01.2019 13:24да примерно так:
@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 на будущее
По-моему все такие варнинги можно банально в Идее настроить (там очень неплохой статический анализатор), не говоря уже об отдельных тулахmaxim_ge
04.01.2019 14:18Понятно, благодарю. У нас все делается, признаться, по старинке, если в методе принимать null категорически нельзя, то используется проверка в runtime методами типа:
static void assertNotNull(Object value, String message)
Сейчас стали много писать на Go и даже JavaScript, с Java уходим, и, похоже, все эти новшества так и пройдут мимо. Есть устойчивое ощущение избыточности таких проверок. Но доказать не могу :)vedenin1980
04.01.2019 15:01+1с Java уходим, и, похоже, все эти новшества так и пройдут мимо.
Более интересна философия Kotlina, где null по сути разрешен только явно, или scal'ы, где null это обычный тип (трейт) и кроме null есть еще много других похожих типов.maxim_ge
04.01.2019 15:07>Более интересна философия Kotlina
Да, это хорошая вещь, но на любителя. У JRE есть, к тому же определенная техническая проблема — прекращена официальная поддержка 32-битных систем, а у нас много клиентов на таких. Так что Kotlin «отпал».
amarao
04.01.2019 17:29+1Я совершенно согласен насчёт количества тестов для python — там надо добиваться 100% покрытия тестами, иначе нелепые опечатки проскочат в продакшен.
Однако, я не согласен с утверждением, что статическая типизация java защищает от значительного числа ошибок. Выше написали про null (что примерно соответствует питоновому проклятью «TypeError: unsupported operand type(s) for +: 'NoneType' and 'list'»), а я могу докинуть ещё боли. Например, java позволяет выполнять полное сравнение для неполностью упорядоченных типов (например, float, у которого NaN, Inf, ненормализованные нули и другие ужасы). После типофашизма rust (тот же PartialEq) типизация java выглядит как очень расслабленная и прощающая идиотам их опечатки (и не прощающая не-идиотам опечатки идиотов).nohuhu
04.01.2019 01:40Я совершенно согласен насчёт количества тестов для python — там надо добиваться 100% покрытия тестами, иначе нелепые опечатки проскочат в продакшен.
Нелепые опечатки должны отлавливаться линтером и не доходить даже до merge.
vintage
04.01.2019 10:15+1И каким образом линтер без информации о типах сможет понять где опечатка, а где нет?
nohuhu
04.01.2019 21:53А каким образом информация о типах поможет вам в борьбе с опечатками? Такое впечатление, что вы с этими типами носитесь, как с писаной торбой.
Что-нибудь типа такого, самый банальный вид опечаток:
function() { var foo = 'bar'; ... fooo; // ESLint ловит на ура foo = truue; // То же самое }
И т.д. Где тут нужна информация о типах? И каким образом она поможет от наиболее распространённых случаев синдрома толстых пальцев, когда тип переменной остаётся таким же, но значение ошибочно? Вот как в примере с '_main__' внизу, или сделать опечатку в цифре, или ещё что-нибудь.
Применять юнит-тестирования для защиты от таких банальностей — это стрельба из пушки по воробьям. Статический анализ и тестирование это два разных инструмента, закрывающие разные виды дефектов, и использовать нужно оба.
vintage
04.01.2019 23:06А каким образом информация о типах поможет вам в борьбе с опечатками? Такое впечатление, что вы с этими типами носитесь, как с писаной торбой.
Судя по вопросу вы никогда не программировали на языках со статической типизацией. Попробуйте, увидите мир с совершенно другой стороны.
Что-нибудь типа такого, самый банальный вид опечаток:
function(p) { p.fooo; // ESLint тут бесполезен p.foo = 'baaar'; // То же самое }
каким образом она поможет от наиболее распространённых случаев синдрома толстых пальцев, когда тип переменной остаётся таким же, но значение ошибочно?
Вы не поверите: https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types
Статический анализ и тестирование это два разных инструмента, закрывающие разные виды дефектов, и использовать нужно оба.
Тестирование закрывает те дефекты, которые не смог отловить статический анализ. В динамических языках статический анализ отлавливает куда меньше, чем в статических. Соответственно и наблюдается дополнительная нагрузка или на тестирование, или на багрепорты.
dimm_ddr
05.01.2019 16:33p.fooo; // ESLint тут бесполезен
Я не на 100% уверен, давно дело было, но статический анализатор в PyCharm питоновский такое вроде бы легко находил. Как дела обстоят в JS я не знаю, но питон так-то тоже с динамической типизацией.
amarao
04.01.2019 13:50Из моей давнешней статьи про тестирование
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
Как в такое отлавливать линтером будете?
nohuhu
04.01.2019 22:02Учитывая, что это выглядит как Канонiчный Пiтонъ, я бы ожидал от линтера достаточной продвинутости для понимания и анализа таких вот общераспространённых конструкций.
Ну т.е. ESLint как-то умеет понимать
'use strict'
?
fori1ton
04.01.2019 16:59+1А вот так менее уродливо?
assertThat(string, not(blankString())); assertThat(list, containsInAnyOrder(expectedValues)); assertThat(map, hasKey(expectedKey));
Связка assertThat из JUnit и matcher'ов из Hamcrest позволяет писать почти на чистом английском, а если обширного набора библиотечных matcher'ов недостаточно — всегда можно написать свои.amarao
04.01.2019 17:20+1А почему не просто
assert expected_key in map
? Зачем всякие assertThat?fori1ton
04.01.2019 18:05+1Очевидно, потому что оператора in в Java нет. А вот ключевое слово assert есть, но его использование считается плохой практикой. Во-первых, потому что assert в Java просто проверяет условие и выкидывает исключение, если условие ложно. С ним нельзя передать сообщение, по которому можно будет определить, что именно упало, не копаясь в стектрейсах, или вывести в лог сообщения о ходе проверки. Во-вторых, ключевое слово и его поведение гвоздями прибито к спецификации языка. Представьте, что вам нужно сделать сложную проверку, которую не напишешь в одну строчку после assert. Нужно выносить в отдельный метод. Для того чтобы использовать его с assert он должен возвращать boolean, то есть, никакой информации о ходе проверки вернуть нельзя. А хочется — и тут каждый начинает городить свои велосипеды, причём, возможно, даже в рамках одного проекта. Это промашка в дизайне языка, а философия обратной совместимости не позволяет её просто исправить. Это и приводит нас к тестовым фреймворкам, которые предоставляют стандартизированный API для тестов. assertThat — точка входа в один из таких фреймворков — JUnit. assertNull, assertEquals и тому подобные методы — специализированные реализации assertThat для наиболее частых сценариев проверок.
amarao
04.01.2019 18:14А фреймворк не может обработать этот assert и выдать нормальный вывод? В pytest assert — тоже ключевое слово, однако, pytest обрабатывает AssertionError таким образом, чтобы показать, что тест fail, и показать где именно. Например, если в тесте сказано assert a==b, то в выводе pytest будет написано, что a было «1234», а b — «123» с подчёркиванием где именно расхождение. То же для in, для аргументов функций и т.д.
Ведь в java можно поймать ассерт как исключение, не?bugy
04.01.2019 18:44Поймать исключение это одно дело, а найти его причину в данном случае выглядит сложнее.
Не знаете случайно, как pytest добивается этой магии? Тут мало того, что он значение параметров знает вне стектрейса, так ещё и соответствующий код. Ведь само по себе
expected_key in map
это лишь True/False значение и AssertionError ничего не знает про то, откуда оно берётся.bugy
04.01.2019 18:52+1Кажется нашел: 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 код на что-то более информативное.
К сожалению я не знаю, насколько это возможно в джаве, но как по мне это выглядит неплохо :)amarao
04.01.2019 19:00Я, кстати, не знал как он это делает. Да, круто.
В принципе, никто не мешает java-фреймворку для тестирования делать то же самое ещё до компиляции. Но тут уже сама религия java, где писать длинные унылые boilerplate паттерны с camlCase'ом — это часть общепринятого.lany Автор
04.01.2019 19:14Философский вопрос — называть ли программой на языке Питон то, что вы пишете и что потом препроцессируется, чтобы сгенерировать реальную программу, которая уже подаётся на вход компилятору. По сути дела это некоторый мета-язык, лишь похожий на Питон синтаксически, но имеющий другую семантику. Разумеется, можно и для Джавы сделать препроцессор, имеются всякие кодогенераторы, но вроде как смысла нет. Если совсем припекает, можно воспользоваться другим JVM-языком, к примеру, Груви, где такие структуры вполне возможны. Смесь Груви и Джавы в одном проекте вполне легко устроить (мы это делаем). И по крайней мере вы пишете на конкретном языке, с конкретным синтаксисом и семантикой, а не непонятно на чём, что препроцессируется в Питон.
amarao
04.01.2019 19:19Ну вы передёргиваете. В коде тестов нет ни одной конструкции, которая была бы не питоном. pytest всего лишь добавляет поверх этого красивый вывод, но если полностью убрать pytest, то останутся обычные assert'ы и контекстные менеджеры (того же pytest'а) для ловли запланированных exception'ов.
То есть ваше предположение, что там «не питон» неверно. Это питон на 100%.lany Автор
04.01.2019 06:02Так вроде вся суть же в красивом выводе, разве нет? Если красивый вывод не нужен, Java-версия
assertTrue(foo(input).contains(output))
ничуть не отличается от питоновской. Ну окей, я понял, что вас тошнит от скобок, Лисп не для вас. Окей, понял, что вас тошнит от camelCase. Но эти аргументы не претендуют на объективность.
Sirikid
04.01.2019 18:44Зачем корячится с
assert
если можно написать фреймворк который сразу все хорошо сделает?amarao
04.01.2019 18:58Я не понимаю о чём вы. Хорошо — это пофиксит все баги в коде так, чтобы тесты были не нужны? Не слышал про такой фреймворк.
Если же вы про использование assertThat, то мне оно просто неэстетично, плюс надо писать разные конструкции под разные сравнения. assert экономит скобки, не загромождает код (т.к. короче), и даже в отсутствие тестового фреймворка хоть как-то, но работает (т.е. его можно скопипастить в консоль и получить тот же assert, но не такой красивый).
bugy
04.01.2019 18:59Как раз pytest и есть такой framework, который в данном случае всё хорошо и делает, без необходимости изучать кучу методов этого фреймворка, а чисто на стандартных средствах языка.
К тому же это позволит проще мигрировать на другой фреймворк в случае необходимости/желания
Как сказал lany, это вкусовщина. Вот на мой вкус, я вынужден признать,
assert key in map
читаемее, чем
assertThat(map, hasKey(key));
Хотя я и работаю с Java и второй синтаксис мне гораздо привычнее
t3hk0d3
04.01.2019 21:56hold my beer
describe 'foo()' do it 'returns bar' do expect { foo() }.to eq("bar") end end
vintage
04.01.2019 10:28Завязывайте уже с алкоголем...
unittest { assert( foo() == "bar" ); }
t3hk0d3
04.01.2019 11:46Пиво пить не брошу, потому-что он хороший.
Не очень понял смысл вашего примера. Речь топикстартера шла об «уродливости», а не о том чтобы написать самый минималистичный тест.
Суть и смысл BDD в том чтобы описывать поведение языком понимаемым человеком без лишних усилий. Таким образом код тестов может служить одновременно документацией к тестируемому коду.
Ну и это просто красиво. После BDD обычные тесты, с ассертами итп, выглядят уродливо.vintage
04.01.2019 12:38Речь топикстартера шла об «уродливости», а не о том чтобы написать самый минималистичный тест.
Куча бессмысленных bdd-расшаркиваний — это и есть уродливость.
Суть и смысл BDD в том чтобы описывать тест языком понимаемым человеком без лишних усилий.
- "describe foo do it returns bar do expect foo to eq bar end end" гугл переводит как "описать foo сделать это возвращает бар действительно ожидать foo к концу конца эквалайзера".
- Никто кроме программистов читать ваши модульные тесты не будет. А программисту лучше читать формально точный програмный код, а не описание "своим языком", которое не полное и зачастую врёт.
- Часто, и в данном случае тоже, текстовое описание просто в точности повторяет программный код. В лучшем случае это тавтология, в худшем — описание с кодом разъезжается.
- В конце концов это даже не 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 строчек кода в место одной — вот где красота. Впрочем, с критериями "красиво/уродливо" вам стоило в художники пойти, а не в программисты. У программистов должны быть иные приоритеты.
amarao
04.01.2019 13:33А зря. Эстетичность, того, что делаешь, это важно. Если делать уродливые вещи уродливыми инструментами, то в жизни будет много красивого. Непонятно откуда, но, а вдруг?
t3hk0d3
04.01.2019 15:08> Куча бессмысленных 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. Мне жалко ваших коллег.
Ну и эстетическая красота — имхо тоже не последняя вещь. С вашим подходом надо всем жить в панельных хрущевках.
vintage
04.01.2019 16:00код порой тяжело читать
Может стоит решать эту проблему, а не писать рядом "своими словами"?
«своим языком» формально точно так-же «точное»
Было бы оно точное, его можно было бы скомпилировать, выкинув весь остальной код.
Ваши «точные», но неструктурированные тесты
Степень структурированности у нас одинаковая. От того, что вы добавили фактически коментариев в виде многовложенного DSL у вас структурированности не прибавилось.
Вот только оператор сравнения
==
в некоторых языках может работать не так как ты ожидаешь.Он работает так же, как в остальном програмном коде. Что ещё от него можно ожидать? Ну а что в некоторых языках нет перегрузки операторов и сравнение всегда по ссылке, так о том и речь, что некоторые языки имеют кривой дизайн, из-за чего приходится добавлять костыли типа
to eq
.
Смысл использования to eq() не просто в самом сравнении, но и составлении правильного текста ошибки
Все эти красоты не имеют смысла при использовании отладчика, а не отладке через repl.
Вы, сударь, видать любитель однострочников.
Нет. Одна строчка кода и 5 записанных в одну строку — не одно и то же.
Читаемость и поддерживаемость кода — не последний фактор в software engineering.
Читаемость и поддерживаемость одной строчки выше, чем пяти.
Ну и эстетическая красота — имхо тоже не последняя вещь. С вашим подходом надо всем жить в панельных хрущевках.
Исходные коды — не картинная галерея. Это сугубо утилитарная вещь. А вы не из тех, кто выстраивает из исходников ascii-картинки?
amarao
04.01.2019 16:41Я рад, что вы подняли вопрос про утилитарность и эстетику. Какой код лучше?
(https://snag.gy/kcErUv.jpg)
А ведь, казалось бы, сугубо утилитарная вещь.vintage
04.01.2019 17:54Вы причину со следствием не путайте. Левый код лучше не потому, что он красивый, а наоборот, красивый он потому, что сделан аккуратно, а не тяп-ляп.
t3hk0d3
04.01.2019 18:44Может стоит решать эту проблему, а не писать рядом "своими словами"?
Эта проблема как раз и решается.
Было бы оно точное, его можно было бы скомпилировать, выкинув весь остальной код.
А оно и компилируется. Это обычный 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-картинки?
Да, а еще я код форматирую. Каюсь, виновен — жутко не утилитарно.
vintage
04.01.2019 19:20А оно и компилируется.
Вот это не компилируется: "foo() returns bar". Всё остальное — бойлерплейт и подражание bdd.
ассертом без контекста
О каком контексте идёт речь? Если о том, какая функция тестируется, то тест идёт сразу после функции. Да, даже, если это метод класса.
правильной ошибки
Что в ошибке не правильного?
Тесты используются не только для валидации, а так-же как часть процесса разработки и отладки.
Да нет, это похоже вы не вкурили в "остановку отладчика на исключениях". Когда он останавливается на ассерте — вам и так видно и код сравнения, и сравниваемые значения.
Что легче читать и поддерживать?
Не люблю повторяться:
"Нет. Одна строчка кода и 5 записанных в одну строку — не одно и то же."
Для тех, кто в танке — одна простая строчка кода. Надо ещё формальней? Одна строчка содержащая не более одного оператора.
Да, а еще я код форматирую.
Для красоты? Или есть иные цели?
eugene_bb
04.01.2019 18:51Отпишите как победить code coverage stats
lany Автор
04.01.2019 19:03Про покрытие неоднократно упоминал в статье.
eugene_bb
04.01.2019 19:13Может я что-то недопонял, но мне кажется что термин «покрытие» в вашей статье и подсчёт реально выполненных строчек кода в течении теста это разные вещи.
Этого не добиться тем что «пустой конструктор по умолчанию и тривиальный сеттер не падают с исключением».
В этом и вопрос — как победить если начальник требует отчёта по результатам реального исполнения кода. Это как статический анализ — сложно обойти.vedenin1980
04.01.2019 20:00Это как статический анализ — сложно обойти.
Нет, покрытие подсчитывает лишь какой код был выполнен в результате теста, можно вызвать все ветки кода и потом вообще не проверить возвращаемое значение.
Например, супертест со 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("Все работает!"); }
Даже если потом будет проверяться используется ли возвращаемое значение, в статье выше описано четыреста относительно честных способов использовать возвращаемое значение без всякой реальной проверки.
Тем более, покрытие кода никак не проверит хитрые бизнес кейсы, когда код вроде бы формально работает, но пользы от него никакой.
Вот если на такие тесты натравят, скажем, тулу с мутационным тестированием — Штирлиц будет как ниогда близок к провалу… (хотя и мутационное тестирование тоже не панацея)eugene_bb
04.01.2019 20:24Да вы правы, я недопонял основную цель описанного вами метода — создать иллюзию проверок.
vedenin1980
04.01.2019 20:31Мопед не мой, я мимокрокодил :)
eugene_bb
04.01.2019 21:26Извиняюсь, не заметил смену собеседника.
На самом деле интересно другое, если отбросить тэг сарказма и предположить что подобные ошибки в тестах были допущены не намеренно, интересует, если ли системный подход в ловле таких проблем.
Например, меняем режим исполнения тестов и тесты падают (пусть не все, но хотя бы те которые специально помечены что должны упасть в таком режиме), т.е. проверка на то что тесты не всегда проходят с успехом.Hokum
03.01.2019 23:43+1Указанноу проблему пытается решить мутационное тестирование. Фреймворки для него, обычно, смотрят какой код покрыт тестами, далее формируют единичные изменения (мутации) и на каждое изменение производят запуск тестов. В результате отображают какие изменение были отловлены (какие мутанты убиты) и какими тестами, а какие мутанты выжили. Но если мутант выжил, то не обязательно проблема в тестах, вполне возможно, что есть избыточные проверки в коде и тесты не могли в принципе упасть. Ну или мутация «задела» какой-нибудь «ассерт».
amakhrov
04.01.2019 00:11и предположить что подобные ошибки в тестах были допущены не намеренно
примеры в статье из реальных (и, порой, довольно популярных) опен-сорс проектов. это просто ошибки от недосмотра, конечно же.
системный подход в ловле таких проблем
Про это последний абзац статьи как раз :)
Большинство упомянутых в статье способов, к сожалению, легко находятся хорошим статическим анализатором
DarkWanderer
04.01.2019 00:05Юнит-тест без проверок — достаточно частая и полезная вещь в сочетании с парадигмой fail-fast. Пример области применения — классы, загружающие конфиг из файла или embedded-ресурса. Отсутствия эксепшна означает что код отработал корректно — проверять значения нужно в другом тесте
vedenin1980
04.01.2019 01:13Но почему не проверить, что конфиг действительно загрузился (проверив хотя бы 1-2 тестовых значений)? Потом система поменяется и вместо эксепшена она будет возращать пустой конфиг, но все будут верить, что unit тестами она покрыта и более того tools, показывающие покрытие тестами будут утверждать, что все в порядке.
Плюс вы расчитывали, что система выкинет эксепшен, а она просто вернула ерунду (так бывает)nsinreal
04.01.2019 09:30Потом система поменяется и вместо эксепшена она будет возращать пустой конфиг, но все будут верить, что unit тестами она покрыта и более того tools, показывающие покрытие тестами будут утверждать, что все в порядке.
Ват? Вы тестами собираетесь проверять будущее поведение?
vedenin1980
04.01.2019 09:39+1Так основая ценность unit-test'ов именно в том, что сделав ошибку в будущем, вы узнаете о ней как можно раньше (в идеале на стадии первой же локальной сборки). Текущее поведение можно провериить просто вручную проверить, что функции возвращают.
Именно поэтому при серьезном рефакторинге лучше сначала дописать недостающие unit-test'ы, а только потом вносить изменения способные все сломать.
То есть, да если класс поменяли и тесты стали падать тут несколько вариантов: тесты написаны неправильно, изменения ошибочны или изменения ломают прошлую логику класса и нужно внимательно смотреть правильно ли это.nsinreal
04.01.2019 12:09Именно поэтому при серьезном рефакторинге лучше сначала дописать недостающие unit-test'ы, а только потом вносить изменения способные все сломать
проверять значения нужно в другом тесте
Просто на фоне оригинального сообщения ваше сообщение интерпретируется так: вы хотите проверять еще что-то, чего нет в другом тесте. Извините за недопонимание.
DarkWanderer
04.01.2019 12:03Я рассчитываю, как я и написал в комментарии, что содержимое конфига проверяется другим тестом. Потому что в одном тесте должно проверяться ровно одно утверждение.
Можно, конечно, запихнуть в этот тест проверку количества значений. И типов значений. И и самих значений. Вот только тесты с тысячей ассёртов — куда большее зло чем "пустые" тесты.
vedenin1980
04.01.2019 12:43Не совсем вас понимаю, вот смотрите есть интерфейс
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, если я правильно вас понял.
Вот только тесты с тысячей ассёртов — куда большее зло чем «пустые» тесты.
Но тысяча тестов с одним ассертом вряд ли лучше. Просто ассертов в любом случае должно быть минимальное кол-во. но с максимальных покрытием возможных случаев.DarkWanderer
06.01.2019 13:42Вы зачем-то сводите то, о чём я говорю, к абсурду.
1. getConfig не выкинул checked exception,
2. getConfig не выкинул runtime exception,
3. getConfig не выкинул error,
Это один тест (отсуствие эксепшна)
4. getConfig не вернул null,
5. getConfig не вернул пустые значения во всех полях,
6. getConfig вернул нужные значения в нужных полях,
Это второй тест («конфиг это именно такое дерево»). Точка.
Но тысяча тестов с одним ассертом вряд ли лучше. Просто ассертов в любом случае должно быть минимальное кол-во. но с максимальных покрытием возможных случаев.
Лучше — потому что из тысячи тестов упадёт только часть, и будет понятно какая именно часть контракта модуля (тысяча тестов это явно уже не класс) сломалась. В случае одного теста масштаб бедствия непонятен от слова совсем — сломался только этот ассёрт или он и все последующие? Кроме того, если вы поправили код низкоуровнегого компонента, есть вероятность, что у вас упадут несколько тестов в разных частях приложения — и внезапно вам нужно разбирать не одну спагетти-простыню, а сразу 10.
Разбиение тестов по утверждениям экономит время разработчика — самый дорогой ресурс.vintage
06.01.2019 14:31Лучше — потому что из тысячи тестов упадёт только часть, и будет понятно какая именно часть
По ассерту вполне понятно где дефект. От того, что вы завернёте каждый ассерт в отдельный тест понятности не прибавится.
сломался только этот ассёрт или он и все последующие?
А какая разница?
внезапно вам нужно разбирать не одну спагетти-простыню, а сразу 10
Точно так же разбираете первую попавшуюся простыню. Какие проблемы?
DarkWanderer
06.01.2019 14:53По ассерту вполне понятно где дефект.
Ну да, конечно. Вот после вашего изменения сфейлился такой тест:
auto data1 = GetData1()
auto data2 = GetData2()
auto data3 = GetData3()
auto result = Calculate(data1, data2, data3)
...(тонна ассёртов)...
assert(result[1][2]==15)
Ассёрт сфейлился — в чём ошибка? А она может быть в любом из кусков данных data1, data2, data3. И что такое result вообще? Для того чтобы понять что случилось, вам придётся перелопатить все 4 метода (а там наверняка еще и другой код вызывается). Если же каждый кусок кода покрыт отдельным тестом — а то и не одним — у вас будет более гранулярная картина, где именно и что сломалось (=опять сэкономленное время).
А какая разница?
Разница в том, что это позволяет точнее понимать эффект, который конкретное изменение кода привносит в кодовую базу — и насколько много изменений оно за собой потянет. Это позволяет планировать свое время — скилл, который важен и нужен на любом уровне (кроме, пожалуй, джуниоров).
Точно так же разбираете первую попавшуюся простыню. Какие проблемы?
Вопрос в том, что при нормальной организации тестов и кода у меня нет простыней. И да, это возможно. Просто надо понять, что чаще всего антипаттерны приносят не другие — плохие — разработчики, а мы сами.vintage
06.01.2019 15:28Если же каждый кусок кода покрыт отдельным тестом — а то и не одним — у вас будет более гранулярная картина, где именно и что сломалось (=опять сэкономленное время).
Опять вы подменили тезис. Проблема тут не в числе ассертов, а в косвенном тестировании. С одним ассертом у вас была бы та же самая проблема, но усугублённая копипастой:
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% одинаковых тестов — это не простыня :-)
vintage
04.01.2019 12:52+1Потому что в одном тесте должно проверяться ровно одно утверждение.
У вас получается сначала утверждение "загрузка конфига не вызывает ошибок", а потом "из конфига загружается то, что там написано". Но первое не имеет никакого смысла без второго, так как его проходит даже пустая функция. А второе включает в себя первое, как неотъемлемую часть. В результате получается, что при изменении апи, придётся править в 2 раза больше тестов, без какой либо пользы от такого разделения.
Вот только тесты с тысячей ассёртов — куда большее зло чем "пустые" тесты.
Почему?
DarkWanderer
06.01.2019 13:54Почему?
- из множества тестов упадёт часть; просто окинув взглядом их список можно понять что же примерно случилось. Одиночный «длинный» тест в случае проваленного ассёрта не выполняет остальные проверки
- после определённого развития, по опыту, никто не может сказать, что же именно данный тест всё-таки проверяет
- один тест с последовательностью операций проверок может прятать случайно созданные антипаттерны типа Sequential coupling — в то время как если для каждого теста объекты создаются заново, эта пробема будет легко видна
- Через год-другой монолитный тест банально превращается в спагетти-функцию. Да, её можно отрефакторить, но зачем, если можно было сразу сделать нормально?
У вас получается сначала утверждение «загрузка конфига не вызывает ошибок», а потом «из конфига загружается то, что там написано». Но первое не имеет никакого смысла без второго, так как его проходит даже пустая функция. А второе включает в себя первое, как неотъемлемую часть. В результате получается, что при изменении апи, придётся править в 2 раза больше тестов, без какой либо пользы от такого разделения.
Зато вам не придётся вспоминать, как же именно работает тест-функция из 10000 строк, и каждый тест вы будете править только один раз, а не 20 (потому что вы исправили первый ассерт — сломался второй — исправили второй, оказалось что сломан третий и т.д.)
Разработчикам платят за время — и экономить нужно именно время. Вы же экономите место, потраченное на код, и количество тестов. Если бы у вас вычитали 100 рублей из зарплаты за каждый добавленный тест и килобайт кода — ваш подход имел бы смысл :)vintage
06.01.2019 15:04после определённого развития, по опыту, никто не может сказать, что же именно данный тест всё-таки проверяет
Скоуп требований обычно пишется в названии теста. Если у вас название и содержимое теста разъезжается, то проблема тут не в числе ассертов. С одним ассертом оно точно так же разъедется.
тест с последовательностью операций проверок может прятать случайно созданные антипаттерны типа Sequential coupling
Если тестируется сценарий, то промежуточное состояние необходимо. И без множественных ассертов тут не обойтись.
Это довольно редкие кейсы, когда получение одного свойства влияет на значение другого. Если у вас такое возможно, то разумеется лучше разнести чтения свойств по разным тестам. Именно чтения свойств, а не ассерты по полученным из них значениям, которых может быть много больше.
Ну и наконец, это палка о двух концах. Ваш "Sequential coupling" может сработать как "чтение второго свойства после первого даёт верный результат", так и "чтение второго свойства после первого даёт не верный результат". Во втором случае несколько ассертов в одном тесте может наоборот выявить проблему, а не замаскировать её.
Через год-другой монолитный тест банально превращается в спагетти-функцию.
Давайте не доводить до абсурда и впадать в крайности. Объединять все тесты в один никто не предлагает. Но когда нужно получить значение и прочекать несколько его свойств — довольно глупо копипастить 100500 тестов.
тест-функция из 10000 строк
Я понимаю, что вам очень хочется доказать свою правоту, но давайте всё же предполагать, что собеседник не совсем идиот.
каждый тест вы будете править только один раз, а не 20 (потому что вы исправили первый ассерт — сломался второй — исправили второй, оказалось что сломан третий и т.д.)
Какая разница править один тест 20 раз или править 20 тестов по одному разу?
Вы же экономите место, потраченное на код, и количество тестов.
Я экономлю время на написание и поддержку тестов. Именно поэтому я пишу компонентные тесты, а не модульные. Поэтому у меня часто несколько ассертов на один тест. И поэтому у меня нет тестов на тривиальный код. И поэтому тесты у меня исполняются в таком порядке, чтобы отладчик сразу останавливался в проблемном месте.
EmotionTigran
04.01.2019 00:43“Многие уже поняли, что от них один вред. Написание тестов отнимает много времени, за которое вы могли бы сделать что-то более полезное.”
Немного странное мышление) Разработка и тестирование — единый процесс. К тому же, на тесты/сценарии можно вешать теги и тестировать их локально, не обязательно ждать результата CI.vedenin1980
04.01.2019 01:05+2То есть остальные советы, как лучше обмануть ревьюера и протащить в продакшен свой говнокод, вас не смущают? :)
rustacean137
04.01.2019 04:19- Можно написать маленькую часть системы как зависимость, и в последующих версиях переопределить assert'ов.
- Более надежный способ, написать библиотеку для генерации юнит тестов.
ViceCily
04.01.2019 08:21Добавлю, чтобы не спалиться на статическом анализаторе, создайте тесты в отдельном модуле и отключите его анализ. Пример для отключения sonar в Maven: <sonar.skip>true</sonar.skip>
maybe_im_a_leo
04.01.2019 09:01Шутки шутками, но вообще существуют же автоматическое генерирование юнит-тестов. Например, Rational Test Realtime или тот же CANTATA.
ganqqwerty
04.01.2019 13:05Меня удивляет, что инструментальная обвязка для юнит-тестов во фронтэнде находится в каменном веке и никто ничего для этого не делает. Писать юнит-тесты для например ангуляра сегодня это 30% написания самого теста, 30% написания мок-данных и 40% — изоляция теста, инъекция зависимостей и прочее. Почему мои мок-данные не генерируются из интерфейсов и классов Typescript? Почему при добавлении публичной функции в сервис, никто не добавляет автоматически эту функцию в spy-object? Почему при инъекции нового сервиса в ангуляре этот же сервис не инъектится и в TestBed? Где генерация тестов с проверками на null и undefined?
vintage
04.01.2019 13:45Меня удивляет, что инструментальная обвязка для юнит-тестов во фронтэнде находится в каменном веке и никто ничего для этого не делает.
Делают. Вы просто не смотрите.
Писать юнит-тесты для например ангуляра сегодня это 30% написания самого теста, 30% написания мок-данных и 40% — изоляция теста, инъекция зависимостей и прочее.
Ангуляр — самый многословный и тормозной фреймворк на сегодня. Странно ожидать от него лаконичных тестов. Кстати, лайфхак. Если вас тоже напрягает, что каждый ангуляровский тест исполняется сотни миллисекунд, то знайте, что происходит это из-за инициализации TestBed перед каждым тестом. И если при старте приложения подождать пока Ангуляр проинициализирует свой DI ещё можно, то делать это для каждого теста — мучительно долго. Если отключить сброс TestBed-а, то тесты начинают летать. Мне удалось ускорить тесты в 10 раз. Но тут надо иметь ввиду, что инстансы сервисов, получаемых через DI будут одни и те же. Благо достаточно замочить их один раз при инициализации TestBed и больше о них не думать. По ссылке выше используется именно такой подход —
$
— это аналог TestBed из Ангуляра, в котором автоматически замочены все недетерминированные/внешние/асинхронные API. Например, мок адресной строки, который берёт/хранит урл не в адресной строке браузера, а в локальном свойстве.
Почему мои мок-данные не генерируются из интерфейсов и классов Typescript?
Потому, что можно либо использовать реальные классы, либо просто привести пустой объект к нужному типу. Те поля, к которым будет обращение, вам в любом случае придётся задать вручную.
Почему при добавлении публичной функции в сервис, никто не добавляет автоматически эту функцию в spy-object?
Как автоматика поймёт надо ли это делать? Ну и вообще полезность шпионов довольно сомнительна. Кода с ними меньше не становится. Зато появляется дополнительное весьма ограниченное API, которое нужно знать, иначе ничего в тесте не понятно.
Почему при инъекции нового сервиса в ангуляре этот же сервис не инъектится и в TestBed?
Давно не трогал Ангуляр, но емнип там нужно TestBed инициализировать тем же модулем, что и всё приложение, — тогда все сервисы будут подсасываться из него.
Где генерация тестов с проверками на null и undefined?
Этим сам TS умеет заниматься при включении более строгих флагов компиляции.
ganqqwerty
04.01.2019 14:00Делают. Вы просто не смотрите.
Я ничего не понял, чем этот код прекрасен?
Как автоматика поймёт надо ли это делать?
Если метод публичный — то надо.
Ну и вообще полезность шпионов довольно сомнительна.
Мне на самом деле все равно, пусть не шпионы, пусть полностью мокнутые объекты. Главное — не я их должен писать, они должны генерироваться и обновляться на основе моего кода.
тогда все сервисы будут подсасываться из него.
Дык тогда у нас изоляция пропадает, так не пойдет.vintage
04.01.2019 14:16Я ничего не понял, чем этот код прекрасен?
Тем, что там нет 70% бойлерплейта.
Если метод публичный — то надо.
В общем случае такая замена метода может сломать поведение. Поэтому автоматически такое делать для всего — опасно.
Главное — не я их должен писать, они должны генерироваться и обновляться на основе моего кода.
Подумайте лучше в сторону компонентного тестирования, где такой проблемы не стоит.
Дык тогда у нас изоляция пропадает, так не пойдет.
Ускорение тестов с 10с, до 1с того стоит. Кстати, в приведённом мной примере изоляция не страдает, так как инициализация контекста там ничего не стоит. Это чисто ангуляровский компромис.
ganqqwerty
04.01.2019 14:24Тем, что там нет 70% бойлерплейта.
Хорошо, разберусь. Еще вопрос — там же туду апп, а мне надо тестировать с объектами, каждый из которых имеет 35 полей, знаете что-нибудь толковое для моканья данных?vintage
04.01.2019 14:39Моканья с какой целью? Обмануть тайпчекер? Ну так:
const taskMock = {} as unknown as Task
ganqqwerty
04.01.2019 15:24моканья с целью получения среднего типичного объекта, пригодного для тестирования. Что-то типа такого, но не для json schema, а для typescript. Я ему — интерфейс, он мне — рандомный объект с хорошо заполненными полями. Выигрыш нескольких минут моего времени. Ваш пустой объект у меня в тесте падать будет как только я попробую обратиться к полю Task.assignedPerson.id, например.
vintage
04.01.2019 16:14Если вы к нему обращаетесь, то это поле должно быть зафиксировано в тесте, а не полагаться на то, что новый генератор не начнёт генерировать id по новой схеме.
ganqqwerty
04.01.2019 17:13А генератору не обязательно запускаться каждый раз перед тестом, это может быть штука, которая работает единоразово, генерит бойлер для моков, а потом я вношу изменения руками. Например, сканирующий кодовую базу скрипт, выплевывающий мок-файлы для заданных интерфейсов, который я закоммичу в гит.
vintage
04.01.2019 17:59А когда вы измените интерфейсы, он интеллектуально обновит ваши правки?
ganqqwerty
04.01.2019 18:58При изменении интерфейсов IDE мне покажет, что этим изменением было затронуто, так что можно сказать, что в основном да. В худшем случае — меня предупредит компилятор, или если совсем швах — у меня упадет тест.
abar
Шутки-шутками, а вот добиваться 100% покрытия кода на куче DT Объектов — та ещё головная боль, которая никакой практической пользы не несет.
timiskhakov
Не сарказма ради, но, может, стоило убрать требование по 100% покрытию, если сами разработчики не видят в нем пользы? (Если оно, конечно, не продиктовано заказчиком, который за это платит.)
lany Автор
Убирайте, конечно, я только за!
vintage
Давайте лучше подумаем над более адекватной метрикой, чем «доля строк кода, в которые заходит исполнение тестов».
ganqqwerty
как насчет «здравый смысл»?
vintage
Мало у кого взят этот перк.
vyatsek
А за чем вам 100% покрытие? чем 100% лучше чем 90% больше не всегда ведь лучше?
Никто ведь не пытается поднять температуру человека с 36.6 до 37 градусов.
А статья супер :)
beatstream
И тут я завис при парсинге
Evengard
запятая после "90%" вернёт вашему парсеру душевное спокойствие.
SannX
Там больше подходит знак вопроса:
beatstream
Просто у меня там ассерт, который никогда не вызывается… И вот, кривой семантический парсер ушел в продакшн.
PS: хорошо хоть «за чем» нормально научился отрабатывать — в логи уходит сообщение «за шкафом».
JediPhilosopher
Несколько раз встречал как отдельных людей, так и целые компании, ставившие 100% покрытие как обязательную метрику кода. Где-то на хх даже в вакансиях такое встречалось.
Особо это бесит, когда в коде есть а) куча сгенеренного бойлерплейта типа toString() и прочей мути, которая в тестах обычно никого не интересует и б) куча catch'ей для CheckedExceptions (боль для Java), которые никогда не будут выброшены, но компилятор заставляет добавлять для них catch-блоки и писать в них что-то там (а пустые catch-блоки могут быть запрещены гайдлайнами), что в итоге тоже попадает в статистику и портит coverage.
Exponent
Както попал я в такой проэкт, 100% покрытие кода тестами. Одно изменение которое отнимало 5 мин требовало 2-3 часов исправлений тестов. Тогда убедился что это безумие. Гораздо лучше и практичней использовать интеграционные тесты, если что-то не так они тоже упадут, но зато ясно что система/подсистема работает.
dimm_ddr
По описанию это выглядит как проблема с неправильно написанными тестами, а не конкретно 100% покрытием. Впрочем первое может вызываться вторым, когда от тебя требуют покрыть то, что не имеет смысла покрывать, то и рождаются странные и хрупкие тесты.