Pull to refresh

Comments 111

Да ё-ж моё-ж! Вы действительно думаете, что достаточно одной строчки?

assertEquals(5, hypotenuse(3, 4));

А вы уверены, что у вас по ходу не возникнет переполнений? (сумму квадратов придётся считать, — не шуточки) Что они правильно отработаются? Ваш тест это тестирует? Нет?! Ну так грош ему цена!

Вы написали замечательную мысль: «Он (тест) на порядок проще, чем тестируемая программа». Не следует ли уже из этого, что тест не тестирует все аспекры работы программы?

Я хочу пояснить. Я не против юнит-тестов. Юнит-тесты — это хорошо. Но! Во-первых, даже рассчёт гипотенузы нельзя протестировать одной строкой; и во-вторых, даже написав тестов больше, чем тестируемого кода, верить тестам полностью нельзя.
В методе testHypotenuseCalculation — достаточно. Код метода testHypotenuseOverflow, очевидно, не приведен. :)
Да ладно, всё равно не достаточно. Может быть корень квадтратный считается не правильно, если дробная часть аргумента лежит в пределах от 0.1 до 0.2? А? Такое бывает.

А может быть квадратный корень считается просто не достаточно точно? Вы (автор статьи) уверены, что такой тест прокатит?

assertEquals(1.0000000000001, hypotenuse(1, 1.0000000000001));

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

assertEquals(1.0000000000001, hypotenuse(0, 1.0000000000001));

вот вам пример того, что и в тестах могут быть ошибки :-) даже в однострочных.
Эта ошибка легко находится, т.к. не совпадает с кодом.
Это из разряда тех кто говорит, что покрытие теста кодом должно быть 100%. Что тоже свидетельствует о недопонимании сути юнит-тестирования.
Юнит тест должен открывать суть тестируемого метода, и на один метод, может быть несколько тестов.
Если в целях избежания ошибки вы сочтете необходимым проверять метод на переполнение, допишите еще один тест. Если захотите проверять ошибки округления — еще один тест, и т.д.
Каждый из этих тестов будет подчеркивать одну особенность поведения тестируемого метода.
Что бы проверить все и вся, одних юнит-тестов недостаточно. Юнит-тесты нужны лишь что бы дать большую гарантию того, что код работоспособен, чем простая компиляция проекта.
Никто кроме программиста не может оценить риски. Есть основания считать, что в определнных случаях точность падает — пиши тест. Есть баг репорт — пиши тест.
Я дам вам яблоко. Вы скажете «недостаточно»? Ну ладно, не хочешь как хочешь… Постоянно читаю комменты в духе «этого мало», «это не очень хорошо». Что за юношеский максимализм кругом? :) Желать большего — само по себе неплохо. Но зачем при этом отказываться от малого, да ещё плеваться в ту сторону? Бери и радуйся, что дают и денег не просят :)

Хотя юнит-тесты как раз просят, но это окупается. И фокус как раз в том, что окупается до определённого предела. 3-5 проверок написать — ок, хорошо, не шибко трудно, и вероятность наступить на грабли снижается в разы. 10 — эээ, ну, туда сюда, можно, если времени не жалко. Ну минус ещё одни грабли, теоретически. Но 100% покрывать — блин, не в космос запускаем, поправить всегда можно, это дешевле обойдётся. К тому же, сколько соломку не стели, обязательно рано или поздно навернёшься мимо подстилок. Так стоит ли в три наката стелить?

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

Также хорошая практика добавлять к тестам ещё один после прыжка на грабли. Первый раз грабли бьют просто по лбу, а второй раз — ещё и по самолюбию, а в голову лезут мысли «а не дурак ли я?». Зато когда от повторного прыжка на те же грабли спасает тест, гладишь себя по голове и говоришь «о, какой я оказывается предусмотрительный» :)
Если следовать TDD, то первая реализация метода будет return 5; :) Потом вводим второй ассерт, например, assertEquals(10, hypotenuse(6, 8)); он фэйлит и только потом реализуем его через корень квадратов. Если писать тесты, обращаясь с юнитом как с чёрным ящиком, документируя его поведение, то assertEquals(5, hypotenuse(3, 4)); ни о чём не говорит, по крайней мере, человеку забывшему теорему Пифагора, два ассерта дают большее понимание, по-моему.
Если человек забыл теорему Пифагора, то и два ассерта не дадут ему понимания.
Для этой цели следует использовать название тест-метода:

@Test
public void hypotenuseSquareEqualsToSumOfKatetsSquares() {
  assertEquals(5, hypotenuse(3, 4));
}
Когда тестов нет, то верить совсем нечему.
<humor>На это можно взглянуть иначе: «нет искушения чему-то поверить».</humor>
Если нет тестов, то ты уже веришь коду.
humour, not humor
labour, not labor
colour, not color
Вспомнил про css свойство color, полез в википедию.
colour, labour, humour — британский английский
color, labor, humor — американский английский

Так что вы оба правы.
To the citizens of the United States of America from Her Sovereign Majesty Queen Elizabeth II

In light of your failure in recent years to nominate competent candidates for President of the USA and thus to govern yourselves, we hereby give notice of the revocation of your independence, effective immediately.
(You should look up 'revocation' in the Oxford English Dictionary.)

Her Sovereign Majesty Queen Elizabeth II will resume monarchical duties over all states, commonwealths, and territories (except Kansas, which she does not fancy).

Your new Prime Minister, Gordon Brown, will appoint a Governor for America without the need for further elections.

Congress and the Senate will be disbanded. A questionnaire may be circulated next year to determine whether any of you noticed.
(Продолжить)

To aid in the transition to a British Crown dependency, the following rules are introduced with immediate effect:

— 1. The letter 'U' will be reinstated in words such as 'colour,''favour,' 'labour' and 'neighbour.'
Likewise, you will learn to spell 'doughnut' without skipping half the letters, and the suffix '-ize' will be replaced by the suffix '-ise.'
Generally, you will be expected to raise your vocabulary to acceptable levels. (look up 'vocabulary').

— 2. Using the same twenty-seven words interspersed with filler noises such as like' and 'you know' is an unacceptable and inefficient form of communication. There is no such thing as U.S. English. We will let Microsoft know on your behalf. The Microsoft spell-checker will be adjusted to take into account the reinstated letter 'u' and the elimination of '-ize.'

— 3. July 4th will no longer be celebrated as a holiday.

— 4. You will learn to resolve personal issues without using guns, lawyers, or therapists. The fact that you need so many lawyers and therapists shows that you're not quite ready to be independent. Guns should only be used for shooting grouse. If you can't sort things out without suing someone or speaking to a therapist,then you're not ready to shoot grouse.

— 5. Therefore, you will no longer be allowed to own or carry anything more dangerous than a vegetable peeler. Although a permit will be required if you wish to carry a vegetable peeler in public.

— 6. All intersections will be replaced with roundabouts, and you will start driving on the left side with immediate effect. At the same time, you will go metric with immediate effect and without the benefit of conversion tables. Both roundabouts and metrication will help you understand the British sense of humour.

— 7. The former USA will adopt UK prices on petrol (which you have been calling gasoline) of roughly $10/US gallon. Get used to it.

— 8. You will learn to make real chips. Those things you call French fries are not real chips, and those things you insist on calling potato chips are properly called crisps. Real chips are thick cut, fried in animal fat, and dressed not with ketchup but with vinegar.

— 9. The cold, tasteless stuff you insist on calling beer is not actually beer at all. Henceforth, only proper British Bitter will be referred to as beer, and European brews of known and accepted provenance will be referred to as Lager. Canadian or South African beer is also acceptable, as they are pound for pound the greatest sporting nations on earth and it can only be due to the beer. They are also part of the British Commonwealth — see what it did for them. American brands will be referred to as Near-Frozen Gnat's Urine, so that all can be sold without risk of further confusion.

— 10. Hollywood will be required occasionally to cast English actors as good guys. Hollywood will also be required to cast English actors to play English characters. Watching Andie Macdowell attempt English dialogue in Four Weddings and a Funeral was an experience akin to having one's ears removed with a cheese grater.

— 11. You will cease playing American football.
There is only one kind of proper football; you call it soccer. Those of you brave enough will, in time, be allowed to play rugby (which has some similarities to American football, but does not involve stopping for a rest every twenty seconds or wearing full kevlar body armour like a bunch of nancies).?

— 12. Further, you will stop playing baseball. It is not reasonable to host an event called the World Series for a game which is not played outside of America. Since only 2.1% of you are aware there is a world beyond your borders, your error is understandable. You will learn cricket, and we will let you face the South Africans first to take the sting out of their deliveries.

— 13. You must tell us who killed JFK. It's been driving us mad.

— 14. An internal revenue agent (i.e. tax collector) from Her Majesty's Government will be with you shortly to ensure the acquisition of all monies due (backdated to 1776).

— 15. Daily Tea Time begins promptly at 4 p.m. with proper cups, with saucers, and never mugs, with high quality biscuits (cookies) and cakes; plus strawberries (with cream) when in season.

God Save the Queen!

PS:? Only share this with friends who have a good sense of humour (NOT humor)!

ах, ну и кончено: простите за офф-топик :)
Я никогда не слышал, что «Unit-tests»что-то гарантируют. Это абсурдная точка зрения.

Тут все немного иначе, на мой непросвещенный взгляд. Unit-tests — не для получения гарантии, что, мол, код все всегда делает правильно. Они для того, чтобы на этапе написания атомов кода локализовать возможные простые ошибки/опечатки.

В приведенном автором примере, метод assertEquals(5, hypotenuse(3, 4)); может выдать assertion и сразу избавить нас от отладки в недрах смежных классов, которые считают гипотенузу и еще много чего вокруг. Например, пусть наше приложение накладывает на картинку супер-пупер-мега-фильтр, который, в частности, считает гипотенузы. Обычный случай: нарисовали, получили что-то столь же далекое от ожидаемого, как картины Глазунова от Мона Лизы. Локализовать проблему без unit-теста (если проблема — в том, что мы забыли возвести в квадрат один из катетов) — будет где-то в стопиццот раз сложнее, чем если бы мы не поленились и на стадии написания кода добавили малюсенький unit-test.

А теперь представьте, что наш фильтр последовательно выполняет 20 преобразований, состоящих из десяти несложных математических операций каждое. Отладчик нам поможет на третий месяц, а unit-test — сразу.
> Я никогда не слышал, что «Unit-tests»что-то гарантируют. Это абсурдная точка зрения.
Естественно, что 100% гарантий не даст никто, но тесты дают хоть какую-то надежду. Например:
* поменяв поведение в базовой функциональности, вы не поломали проекты пользователей;
* не будет регресса бага.
и т.п.
> «Unit-tests — не для получения гарантии, что, мол, код все всегда делает правильно. Они для того, чтобы на этапе написания атомов кода локализовать возможные простые ошибки/опечатки.»

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

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

P.S. Но насчет гарантий все верно — на 100% ничего гарантировать нельзя, ибо тесты — это код, а код пишется человеком, а следовательно — хоть и в меньшей мере, но тоже расположен к ошибкам.
Да ладно, ладно, это ж я утрирую. Конечно, ещё несколько важных случаев следует добавить в тест: когда все нули, когда все три — очень маленькие или очень большие числа, а также когда они отрицательные. Если это важно в вашем контексте. Если неважно, то не надо и тестировать.

Что значит «верить тестам полностью нельзя»? Можно и нужно. То, что тест тестирует — он тестирует совершенно точно, этому и надо верить. Вы, вероятно, хотели сказать, что тест не может гарантировать абсолютную правильность программы — ну так это и я сказал.
«Unit-tests — не для получения гарантии, что, мол, код все всегда делает правильно. Они для того, чтобы на этапе написания атомов кода локализовать возможные простые ошибки/опечатки.»

Полностью поддерживаю. Иначе получается, что не нужно верить себе, когда проверяешь правильность работы программы. Кому же тогда верить :)

К слову, у вас в статье не отмечено, что тесты, кроме того, что важны по отдельности, являются очень мощным инструментом «все вместе», поскольку запускаются пачками, исключая необходимость при каждом изменении все перепроверять вручную.
Как-то у вас два коммента противоположных получилось. Не скопировали во второ раз? :)
А насчёт пачки — по-моему она как раз важнее отдельных тестов, не будь пачек смысла писать тесты было бы намного меньше
С суммой квадратов как будто бы правильнее так:
double hypotenuse(double a, double b)
{
   if(b>a) {double c=a;a=b;b=c;}
   double frac = a/b;
   return a*sqrt(1+frac*frac);
}

Так можно и 10^200 подавать на вход. Промежуточный результат всегда в рамках конечного результата.
в имена тестов добавьте нижние подчеркивания, а то они у вас нечитаемые.
вообще, хорошее правило именования предложил Рой Ошеров в своей книге The Art of Unit Testing:

MethodName_StateUnderTest_ExpectedBehavior

❂ MethodName—The name of the method you’re testing
❂ StateUnderTest—The conditions used to produce the expected behavior
❂ ExpectedBehavior—What you expect the tested method to do under the specified conditions

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

Насчёт именования — да, это хорошая идея, но не единственная. Другой вариант мог бы быть такой: добавить в JUnit код, который бы сам вставлял пробелы. Кажется, я что-то такое где-то видел.

А ещё один вариант — это писать юнит-тесты не на Java, а на Java-совместимом языке, например, Scala. Там синтаксис позволяет писать так, чтобы «MethodName», «StateUnderTest» и «Expected Behaviour» были отдельно, каждое на своём месте:
www.scalatest.org/getting_started_with_bdd
> В теле тест-метода слишком много assert.

Я всегда в assert подписываю текстовое сообщение, тогда таких проблем не возникает.

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

Чтобы убедиться, что тест тестирует, надо его сломать. Например, в качестве одного из ожидаемых выходов подать заведомо неверный результат и убедиться, что действительно тест падает. Затем исправить и убедиться, что тест не падает.
Не соглашусь, с тем, что циклы в тестах это хорошо, автор четко указал, что тесты — это и документация к коду, а цикл однозначно усложняет чтение и «отлов» того, что упало.
Циклы — это конечно удобно в плане написания теста, но не более.
Цикл читать значительно проще, чем копипасту. Потому что он занимает меньше места на экране. Не вижу потенциала для внесения ошибок в конструкции for(Object element: elements) {...}. Где тут можно ошибиться, чтобы получился компилируемый, но неправильно работающий код?
Ошибка может быть на уровне логики (механическая не правильно подсунули эталонные данные, логическая, когда эталонные данные верны, а тестируемый код отработал не правильно и т.д.) и сложность заключается именно в том, что не известно на какой итерации цикла сломалось приложение (Хотя тут могу ошибаться, т.к. работал исключительно с PHPUnit, в каком формате выкидывают ошибку JUnit, увы не в курсе)
Чтобы определить итерацию, как раз нужны адекватные текстовые сообщения ко всем ассертам. Ошибки на уровне «неправильно подсунули эталонные данные» возникают и без циклов.
Например, метод, обрабатывающий массив и возвращающий итератор как тестировать без цикла? Через .next?
Нет, конечно, циклы сами по себе не являются злом и их можно использовать в юнит-тестах, так же как и в любом другом коде. Имелось в виду, что эти циклы не должны повторять ту же логику, что есть с тестируемой программе.
Еще есть мнение, что юнит-тесты и код тестируют друг друга, так как они взаимосвязаны.

То есть в некотором смысле тестами для тестов является сам тестируемый код.
Точно так: если не проходит тест то ошибка или в тесте или в коде!
Интересное мнение. Добавил в текст.
Кстати да, не раз исправлял опечатки в тестах или регулярки, когда вроде тест должен проходить, но не проходит
Тесты программы, которая работает с БД очень активно и сложно, либо сами очень сложны, либо перестают быть ЮНИТ-тестами, поскольку тестируют сразу большие куски программы
А это называется интеграционными тестами.
Независимо от размера, они перестают быть ЮНИТ-тестами, потому что они тестируют не ЮНИТ, а взаимодействие с внешними системами — а это уже интеграционное тестирование.
Для этого надо вынести код, непосредственной производящий операции с БД, в отдельные классы (типа DAO), и в юнит-тестах подменять его заглушками (mock).
Вот совсем не обязательно использовать моки. Можно в памяти базу поднять, как это делает grails. Очень удобно и менее трудоемко.
И абсолютно не показывает слабые стороны линтера ))
Я вот волею судеб работал с ним. А вы знаете, что если в составе уникального ключа ему передать null, то он не проверяет запись на уникальность?
ну вот ни разу с ним не работал. И что-то, если быть честным, не хочется. Тогда выход действительно один — моки. Хотя они и несколько усложняют написание тестов. С более привычными(относительно конечно) неплохо работает как раз метод с базой в памяти.
Надеюсь, что мне больше НИКОГДА не придется с этим убожеством работать.
Честно, это просто кошмар. Три джойна — база зависла и завесила весь сервер.

А можете про моки поподробнее рассказать?
могу. обычно мок — какой-то объект, который сложно создать просто так. Например сущность общения с бд(выражаюсь расплывчато ибо в разных подходах по разному), либо очень много зависимостей, а в тесте нужна только маленькая часть функционала.
Допустим пример — у нас есть класс, метод которого принимает строку, разбирает ее и сохраняет в БД. Для этого у нас есть специальные сущности — парсер и «сохраняльщик».
Мы создаем моки этих сущностей. и вот тут происходит некоторое изменение в написании теста. Нам ведь не нужно тестировать, что парсер именно распарсит входную строку это должно отражаться в тестах парсера. Мы просто должны убедиться что вызван данный метод у парсера. То же самое и с «сохраняльщиком».

тема отдельной статьи)
Ну так вот изза очень специфической БД непонятно, как тестировать сохраняльщика (а отличие от парсера).
Выносите работу с БД из метода в отдельный объект, причём используя Dependency Injection (aka Inversion of Control), задавая объект-сохраняльщик извне (конструктор, сеттер, параметры метода, конфиг и т. п.). Тестируете сохраняющий метод этого нового объекта (прежде всего, что он сохраняет нормально всё, что подаётся на вход в корректном формате). Затем пишите тест на основной метод, но класс сохраняльщика указываете не тот, который создали, а мок, который только проверяет был ли вызов сохраняющего метода с нужными параметрами. Мок, как правило, создаётся фреймворком тестирования автоматически путем интроспекции настоящего класса.

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

на нормальной ДБ я бы все это и через ДАО порешал
Хм… это как?! Что вы делаете ручками, чего нельзя сделать в тесте?
Да. С моками часто можно оторваться от реальности. Нужны тесты на моки:)
И скорость его тоже не тестирует…
нагрузочное тестирование!)
Нет, вы не поняли. То, с чем мускуль или погри справляются менее чем за секунду, гребаный линтер может и секунд 30 выполнять.
Поэтому работа других баз не показательна.
честно. сочувствую вам, вы работаете с продуктом который не похож на другие(хотя наверное должен) и каждый шаг как у скалолаза — неизвестно куда выведет. Требуйте надбавки!
Просто он единственный сертифицирован (предположительлно за невероятного размера откат) на первую категорию секретности.
Боюсь представить с какими данными Вы работаете и для кого, у нас ДСП информация вполне себе хранится в Oracle, DB2.
Как говорят в фильмах — я мог бы сказать, но пришлось бы вас убить
Я, вероятно, не совсем правильно все эт понимаю. В моей нынешней аппликации вся ее работа для пользователя состоит в нажатии одной кнопки. А дальше начинается работа с БД. И вот в ней-то и была куча ошибок. И еще кучи ошибок не было )). Единственное, что имело смысл тестировать в этой программе — это работу с БД в разных ее аспектах — оптимальность запросов, валидация внешних данных по отношению к данным в БД, обновление БД, внесение новой информации в БД…
Заглушками там можно было закрыть что угодно практически… но приложение осталось бы непротестированным. Более того, в БД вносились не простые объекты, а достаточно массивные, причем вместе со своими связями. А поскольку БД еще и не в третьей нормальной форме…
В моей нынешней аппликации вся ее работа для пользователя состоит в нажатии одной кнопки.

Вы имплементировали DWIM-button?!

В вашем случае могут быть полезны (а могут и нет) примерно такие unit-тесты (в псевдокоде):

dbo = new DataBaseObject();
putDataBaseObject(dbo);
dbo1 = getDataBaseObject(dbo.id);
assertEquals(dbo, dbo1);
К сожалению, эта кнопка — только Открыть файл.
Дальше в логе приложения (доступном пользователю) происходит некий триллер с информацией о валидации, обновлении и загрузке данных в БД. Но спасибо за английскую версию того, что язнал под названием «Сделать пиз....»

К сожалению, объекты нелинейны, а достаточно сложны по структуре, да еще и допускают примерно 3 486 784 401 вариаций (правильных и неправильных) каждый.
Ну я потому и оговорился, что могут быть полезны, а могут — и нет.

Unit-тесты — не панацея, они не умеют стирать белье и бегать за пивом. Но я вижу минимум два применения:
  • валидация данных (unit-test читает заведомо правильный и заведомо неправильный файлы, проводит проверку корректности и проходит/валится соответственно),
  • обновление данных.
  • . Молотком удобно заколачивать гвозди, но неудобно гладить рубашки. И наоборот.
В процессе выяснилось, что легче менять файлы руками и мсотреть, что получилось при разборе, чем писать на это тесты.
Но да, к сожалению не все можно легко протестировать и, к сожалению, оказывается, что человек иногда полезнее компьютера…
В процессе выяснилось, что легче менять файлы руками и мсотреть, что получилось при разборе, чем писать на это тесты.

Почти всегда тесты окупятся. Вот всего несколько примеров:
  • Ваш проект разросся и в команду добавили еще двоих ретивых программистов, которые начали с того, что (потому что на дворе 2048 год :-)) заменили во входных файлах long на long long;
  • Вы уволились, а компания продолжает жить и подавать на вход файлы с новыми «данными»;
  • Вы сами дописали новый функционал, но забыли прогнать тесты.

В последнем случае unit-тесты просто сразу при первой перекомпиляции рухнут. В первых двух — достаточно скормить им «новые» входные файлы и сразу понять, что не так.
У нас джава и все типы у нас с больших букв )) Long.
Слава Богу, проект для МЧС, мы скорее всегго не будем никогда дорабатывать это ПО.
3 — да, ту сказать нечего.

Но все равно тесты получаются не юнит…
Меня тоже это останавливает от написания тестов.
ИМХО, любую такую программу можно отрефаторить так, что архитектура подойдет для юнит-тестов. Другой вопрос, что эта архитектура может быть менее оптимальной, менее понятной, менее удобной. Но тестируемой.
Вот пока что не вижу плюсов в рефакторинге это всего на TDD-совместимую архитектуру.
Обычно считается, что тестируемая архитектура = оптимальная, удобная, понятная архитектура. Многие известные люди пишут об этом в блогах и говорят на конференциях.

Насчёт рефакторинга: да, вполне возможно, существующую систему переписывать только из-за юнит-тестов и не стоит. Основная идея TDD состоит в том, чтобы систему изначально планировать под юнит-тесты.
«Обычно считается, что тестируемая архитектура = оптимальная, удобная, понятная архитектура»
На практике всегда получалось именно так!
Оптимальная (в плане оптимизации скорости/ресурсов) далеко не всегда. Выделение объектов/методов для удобства тестирования/чтения/понимания обычно имеет накладные расходы.
Архитектура не связана с юнит тестами никак. Иначе не сохраняется принцип KISS.
KISS — Keep It Simple, Silly
Ключевые слова — Keep Simple, т.е. сохраняй простоту. Простое решение необязательно модульное. Можно положить в один метод валидацию данных, обработку и INSERT в базу данных. Если у вас на сайте одна форма — то это более простое решение, чем выделять класс валидатора, классы ORM и MVC.
Но менее тестируемое, вот в чем загвоздка. Для валидатора можно написать тесты, для ORM — можно. А для одного подобного метода — нельзя, тесты будут ужасными и в несколько раз больше, чем сам метод.
И это только один пример из множества.

<irony>Ключевое слово - Silly</irony>
но можно протестировать и этот один метод)
Можно конечно. Просто получится либо криво, либо долго и дорого — дешевле и проще тестировщику заплатить, заодно и «тест с улицы» получится.
> Просто получится либо криво, либо долго и дорого
Зависит от вас. Не агитирую за юнит тесты.
Вы правы насчет простоты, но тут нужен контекст.

Допустим, если вы пишете простейшее решение для микро-задачки, вам не нужны все эти архитектурные навороты, ибо они усложняют систему и отвлекают от собственно задачи. Но у софта есть такой параметр как «maintainability» — он проявляется с ростом системы, и если изначально было известно, что система в итоге должна быть большой и сложной — идти простым путем не особо то верно.
Позвольте не согласиться, именно благодаря юнит-тестам (при условии, что они пишутся изначально) создается правильная архитектура приложения
Может, но не всегда. TDD мотивирует писать меньшие по объему классы и методы и только. Архитектуру он не контроллирует.
Ну как же не контролирует. TDD стимулирует, навскидку: декомпозицию, DI, разделение ответственности и, само собой, DRY, фактически заставляя разработчика уменьшать связанность компонентов, чтобы их можно было тестировать отдельно, чтобы юнит-тесты не превращались в функциональные/интеграционные/приёмочные.
С вышеперечисленным согласен.
Но сделать нормальное(быстрое) undo/redo в текстовом редакторе используя только подход TDD нельзя. Надо садиться и продумывать архитектуру приложения отдельно.
Т.е. качественно реализовать локальную задачу используя TDD можно, а продумать архитектуру уже нельзя.
Можно сесть и быстро сделать тормозное undo/redo, а потом его оптимизировать и рефакторить пока скорость не устроит, не боясь что-то сломать :) Оба подхода, имхо, имеют право на жизнь. Возможно, с продуманной заранее архитектурой разработка и выйдет быстрее (особенно если аналогичные задачи уже решались), но наверняка рабочее решение (пускай и не оптимальное) вы получите быстрее с TDD — в дедлайн будет что заказчику показать :), пускай и не соответствующее требованиям по скорости, но уже рабочее. А при традиционном проектировании есть риск, что показать сможете, например, только Undo, пускай и супербыстрое.

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

Я второй случай и имел в виду. TDD, не TDD, а написать так, что придется переписывать можно:)
Если юнит-тесты становятся слишком большими, это значит, что ваши «юниты», то есть атомарные элементы кода слишком большие. А это уже проблема архитектуры — она становится неподдерживаемой в любом случае.

Так что считайте, что у юнит-тестов есть еще такой удобный бенефит — это как сигнал того, что архитектура начинает развиваться в неправильном направлении.
Тесты, на мой взгляд — контракт между разработчиками. При разборе чужого можно посмотреть тесты в качестве некой спецификации того как работает код. Кроме того при подход Test First, мы сначала пишем тест, прогоняем его, убеждаемся что падает(нет реализации еще!) пишем сам рабочий код, прогоняем тест -работает! Итог: есть спецификация на код(«тест») и функционал точно работающий так как описано в тесте. Незаменимо в рефакторинге! Как иначе обеспечить то, что ты не поломал api-класса?
тезисы понравились.

подскажите, а как правильно писать юниттесты для такого рода компонент, как Data Abstract Layer?
уточните пожалуйста, очень абстрактный вопрос.
окей, уточняю. есть требования — написать слой работы с данными. на входе дто сущности, на выходе — инсерты и апдейты в бд. и наоборот — на входе вызовы методов слоя с параметрами, на выходе дто сущности.

Вопрос: как правильнее всего писать тесты в этом случае?
окей) Вы же знаете какие дтошки у вас имеются и примерно какие запросы у вас генерируются?
ну вот!) вы уже все написали что вам необходимо! в тесте вы будете создавать дто, и передавать на ваш слой работы с данными, и анализировать, что на выходе будет нужный вам запрос. и наоборот. Это юнит-тестирование.
Однако тут для полного цикла тестирования придется написать и интеграционные тесты:
подавать на вход дтошки, скажем на запись или обновление и анализировать, что данные изменились.
acerv, только готовые запросы не пишите в тестах, в будущем будет очень мешать. Лучше или условие вроде «запрос содержит „а“ или объект, описывающий содержание запроса.
>Надеюсь, эта заметка прекратит вечные споры…
Даже не надейтесь.
Неужели люди наивно полагают, что библиотека юнит-тестирования не покрыта юнит-тестами? /thread
А как тестировать отрисовку? Я вот покрываю тестами какие-то атомные участки, типа математики, выборки, проверки, но как покрыть приложение в целом?

Допустим, у меня есть ui-элемент UiCircle, а у него метод animatedMove, который меняет позицию центра. Конечно, я могу проверить координаты центра через Н кадров, но ведь по какой-то причине у меня может не обновиться буфер, или что-то типа такого.
Или как оттестировать draggable? Пользователь может таскать элемент. На тестах всё может быть хорошо, а на практике — облом.
Или юнит-тесты такое и не покрывают?
Подпишусь под «смотрите в сторону функционального тестирования».

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

Вот из всех этих тестов и получается покрытие приложения в целом.
На мой взгляд, тесты — это возможность убедиться в работоспособности программы при дальнейшем рефакторинге или добавлении функционала. Как известно, принцип «не трогай пока работает» на практике не всегда выполним. Так что покрывать необходимо основной и наиболее часто используемый функционал. Перефразирую принцип Парето: 20% покрытие кода тестами проверит 80% процентов функциональности.
из скромного опыта — нормальное покрытие — 60-70%, потому как геттеры и сеттеры тестировать бесполезно.
Надо различать, по-моему, ситуации «сначала код — потом тесты» и «сначала тесты — потом код». В первой да, покрыть весь код тестами весьма затруднительно бывает, особенно если на это и расчёта не было. А вот второй — причиной написания каждой строчки кода должен быть или неработающий тест, или рефакторинг кода, уже покрытого тестами.

И кстати, на практике несколько раз сталкивался, что проверяя после каждой итерации «изменившиеся требования заказчика — изменившийся код» 80% наиболее часто используемой функциональности проекта рано или поздно слышишь, что что-то из 20% перестало работать, а самое главное нет представления на какой именно итерации всё сломалось. А тогда ещё и VCS не пользовался
>В теле тест-метода слишком много assert. Такой тест-метод проверяет слишком много аспектов.
А вот это уже ничего не значит, в идеале один юнит тест покрывает одну ситуацию одного метода.
Если метод формирует по правилам большую структуру то количество assert зависит от величины результирующих изменений метода и всего-то и количество ассертов ничего не свидетельствует о качестве теста.
Потому-то из рекомендаций нужно писать рекомендацию, делать тэсты покороче и проверять один возможны путь в одном методе за раз чтобы легче локализировать место падения теста.
Угу, тоже глаз резануло, учитывая что в другом окне открыт тест из десятка ассертов — проверяет, что в HTML коде есть форма, а в форме есть все необходимые поля :) Можно было бы, наверное, проверить и одним ассертом, но такой многоэтажный он бы получился…
Скорее, можно было бы проверить 10-ю юнит-тестами. Тогда при «падении» одного из полей, сразу понятно, какое это поле. Но тут уж вам виднее.
В параметре message проще написать <имя контрола> missed, чем писать testLoginInputPresentInRegisterForm. Кстати, задумываюсь сделать один assert, но в цикле по массиву с тэгами.
Ну, как я и говорю, вам виднее.

Причем, одно дело, когда нужно проверить только присутствие элементов с определенным Name/Id на форме, а другое — когда нужно проверять другие параметры этих элементов (значение, позиция, таб-индекс, и прочее). В зависимости от подробности тестируемой логики, может быть вообще, тест-класс под каждое поле и наборы методов вроде «elementShouldBe_OnForm», «elementShouldContain_ValidValue», «elementValue_ShouldBe_Changeable», «elementTabFocus_ShouldBe_RightAfter_PreviousElement» и все такое прочее.
Sign up to leave a comment.

Articles