Comments 56
Абсолютно согласен с тем, что нету ничего плохого в тестах с реальной сетью, файловой системой и базой данных. Проблема в том, что многие справедливо заметят, что это уже Интеграционные а не Модульные тесты. Не вижу ничего плохого в том, чтобы иметь разные типы тестов ( вплоть до силениумных ) и пользоваться ими всеми. Вопрос стоит во времени исполнения. Я считаю, что именно Модульные тесты должны запускаться в процессе сборки ( а не отдельно).
Хороший совет про RamFS. Когда я налаживал тесты с реальной БД на PgSQL, то разница во времени исполнения достигала порядков при различных типах ФС ( ext3/ext4 ) и различных типах физических дисков.
Кстати на моем проекте у нас есть слой, который скрывает построение запросов для БД и мы очень эффективно смогли решить проблему тестирования, написав бэкенд для SQLITE, который может работает только в оперативной памяти.
Хороший совет про RamFS. Когда я налаживал тесты с реальной БД на PgSQL, то разница во времени исполнения достигала порядков при различных типах ФС ( ext3/ext4 ) и различных типах физических дисков.
Кстати на моем проекте у нас есть слой, который скрывает построение запросов для БД и мы очень эффективно смогли решить проблему тестирования, написав бэкенд для SQLITE, который может работает только в оперативной памяти.
мы очень эффективно смогли решить проблему тестирования, написав бэкенд для SQLITE, который может работает только в оперативной памяти
Почему Вам не подошли In-Memory Databases или размещение файла БД в /dev/shm/?
>>На самом же деле нет ничего страшного, если в модульных тестах будет использоваться база данных или файловая система
Нет. Это неверное утверждение. Т.к. ошибка будет «размазана» Вам придется дольше принимать решение в чем проблема: в базе данных или в тестируемом функционале?
Если Вы другим тестом протестировали работу с базой данной, то ей уже можно доверять! Если же вдруг в работе кода с базой данных есть бага, то ее обнаружат Ваши тестировщики и тогда Вы напишите еще один юнит-тест.
Задача разработчика: за вменяемое время быстро понять суть возникшей проблемы и именно поэтому он пишет юнит-тесты.
Грубо говоря ситуция как с электроникой. Наладчик ставит замеряет осцилогрофом\мультиметром разные компоненты, чтобы быстрее отсечь заведомо исправные, в которых копаться не нужно. Когда Вы пишите юнит-тесты Вы разбиваете Вашу систему на куски, чтобы впоследствии быстрее принять решение откуда начать дебажить. При этом не надо вдаваться в крайность проверять абсолютно каждую функцию, а спросить себя «Оправдано ли будет потратить сейчас время на тест?». Применение мозга не карается законом!!!
Нет. Это неверное утверждение. Т.к. ошибка будет «размазана» Вам придется дольше принимать решение в чем проблема: в базе данных или в тестируемом функционале?
Если Вы другим тестом протестировали работу с базой данной, то ей уже можно доверять! Если же вдруг в работе кода с базой данных есть бага, то ее обнаружат Ваши тестировщики и тогда Вы напишите еще один юнит-тест.
Задача разработчика: за вменяемое время быстро понять суть возникшей проблемы и именно поэтому он пишет юнит-тесты.
Грубо говоря ситуция как с электроникой. Наладчик ставит замеряет осцилогрофом\мультиметром разные компоненты, чтобы быстрее отсечь заведомо исправные, в которых копаться не нужно. Когда Вы пишите юнит-тесты Вы разбиваете Вашу систему на куски, чтобы впоследствии быстрее принять решение откуда начать дебажить. При этом не надо вдаваться в крайность проверять абсолютно каждую функцию, а спросить себя «Оправдано ли будет потратить сейчас время на тест?». Применение мозга не карается законом!!!
Эм. Строго говоря, наличие теста ускоряет написание кода. Вот как вы узнаете, что только что написанная функция действительно работает? Запустите всю систему с тем чтобы до неё добралось управление? Намного проще заранее написать 5 строк кода, верифицирующих результаты работы, а потом тыкать мышкой в точку слева от объявления функции-теста и жать «Run unit test». Вы получаете моментальный отклик на любое изменение, а так же сможете чётко для себя сформулировать, чего от функции хотите. Попробуйте, TDD это очень удобно.
>>Попробуйте, TDD это очень удобно.
Ежедневно пробую! Ежедневно помогает. Набил очень много шишек и поэтому говорю: «Не любая функция должна быть покрыта юнит-тестом».
>>Намного проще заранее написать 5 строк кода, верифицирующих результаты работы
Это в теории. На практике это не только 1-2 позитивных случая, но также 4-6 негативных, чтобы быть уверенным что-таки ДА я могу доверять этому куску кода! А пишем мы далеко не 10 и даже не 20 ф-ций. А при этом рефакторим не мало. Если при рождении кода «на каждый чих» писать по тесту Вы придете к мысли «что-то млин это дело Волокитно». Это было. Это пройдено и именно из-за этой мысли многие бросали применять TDD вообще!
Банальные примеры:
I. когда не надо:
1)
bool ElfParser::isCorrected( const char * data, int data_len );
Это проверка из примитивнейшего условия и на это писать тест глупо!
2)
Что мы в этом примере должны проверять? Что метод std::list::empty() работает как надо?
II когда надо:
Функция prepareInternalFormatData() в которой, к примеру, достаточно много раз вызываются read(), seek(), tellg(). Это уже очень сложный функционал и в случае проблем в этой функции дебажить надо будет не мало! Это надо покрывать юнит-тестом.
Всегда нужно руководствоваться здравым смыслом. Вместо мысли: «Раз функция значит надо тест», лучше применять вопросы: «Насколько сложна функция? Код функции уже устоялся?». Если оба вопроса «да», значит пишем тест, иначе мы просто задолбаемся актуализировать тесты.
Ежедневно пробую! Ежедневно помогает. Набил очень много шишек и поэтому говорю: «Не любая функция должна быть покрыта юнит-тестом».
>>Намного проще заранее написать 5 строк кода, верифицирующих результаты работы
Это в теории. На практике это не только 1-2 позитивных случая, но также 4-6 негативных, чтобы быть уверенным что-таки ДА я могу доверять этому куску кода! А пишем мы далеко не 10 и даже не 20 ф-ций. А при этом рефакторим не мало. Если при рождении кода «на каждый чих» писать по тесту Вы придете к мысли «что-то млин это дело Волокитно». Это было. Это пройдено и именно из-за этой мысли многие бросали применять TDD вообще!
Банальные примеры:
I. когда не надо:
1)
bool ElfParser::isCorrected( const char * data, int data_len );
Это проверка из примитивнейшего условия и на это писать тест глупо!
2)
bool SuperPuperClass::hasAnyStudents() {
return !this.studentsList.empty();
}
Что мы в этом примере должны проверять? Что метод std::list::empty() работает как надо?
II когда надо:
Функция prepareInternalFormatData() в которой, к примеру, достаточно много раз вызываются read(), seek(), tellg(). Это уже очень сложный функционал и в случае проблем в этой функции дебажить надо будет не мало! Это надо покрывать юнит-тестом.
Всегда нужно руководствоваться здравым смыслом. Вместо мысли: «Раз функция значит надо тест», лучше применять вопросы: «Насколько сложна функция? Код функции уже устоялся?». Если оба вопроса «да», значит пишем тест, иначе мы просто задолбаемся актуализировать тесты.
bool SuperPuperClass::hasAnyStudents() { return !this.studentsList.empty(); }
Что мы в этом примере должны проверять? Что метод std::list::empty() работает как надо?
Нет, здесь должны проверять, что hasAnyStudents() работает корректно.
Я думаю, что Вы правы. Ведь наличие списка studentsList — это детали реализации. Реализация завтра может стать другой.
Вот когда будет, тогда и надо задуматься о том что пора писать тест. Это очень похоже на разработку «в прок». А вдруг еще понадобится функция ф1() ?! Ну давай-тогда напишем! А если еще понадобится ф2() ?! Ну давай и ее напишем. В конечном итоге приложение обрастает многими нафиг никогда пригодившимися вещами из-за которых сложность проекта возросла значительно!
Есть такое правило, его удачно сформулировал Мартин в книге «Чистый код», гласит так: «Мы позволяем себе ошибиться один раз». Другими словами действие делается только тогда, когда в нем на практике возникла потребность, а не в умозрительном «А вдруг понадобится?».
С тестами ровно также, детали могут изменяться и этот факт, но могут остаться в том же виде в каком их написали только что! Вот когда Вы ее изменили второй раз, тогда имеет смысл париться, в противном случае рискуете излишне усложнить проект. Кто-то конечно может сказать, что юнит-тесты писать не сложно. Ошибается! Проходит очень много времени, пока программист осознает, что он уже хорошо пишет юнит-тесты. Очень часто стоит только сказать коллеге: «Там упало пару тестов и надо...» он даже договорить не даст, сразу же в лоб «Извини, но мне надо пофиксить багу по задаче....». А задача фиксания тестов очень даже часто возникает, к примеру коллега набажил, а сам в отпуск ушел и кто-то должен его подменить ;)
Есть такое правило, его удачно сформулировал Мартин в книге «Чистый код», гласит так: «Мы позволяем себе ошибиться один раз». Другими словами действие делается только тогда, когда в нем на практике возникла потребность, а не в умозрительном «А вдруг понадобится?».
С тестами ровно также, детали могут изменяться и этот факт, но могут остаться в том же виде в каком их написали только что! Вот когда Вы ее изменили второй раз, тогда имеет смысл париться, в противном случае рискуете излишне усложнить проект. Кто-то конечно может сказать, что юнит-тесты писать не сложно. Ошибается! Проходит очень много времени, пока программист осознает, что он уже хорошо пишет юнит-тесты. Очень часто стоит только сказать коллеге: «Там упало пару тестов и надо...» он даже договорить не даст, сразу же в лоб «Извини, но мне надо пофиксить багу по задаче....». А задача фиксания тестов очень даже часто возникает, к примеру коллега набажил, а сам в отпуск ушел и кто-то должен его подменить ;)
Очень часто стоит только сказать коллеге: «Там упало пару тестов и надо...» он даже договорить не даст
У нас в команде всеми принято правило — мы не коммитим код, если не проходят модульные тесты. То есть, стараемся исключить ситуацию нестабильной ветки разработки. Поэтому таких проблем пока еще не возникало.
Вот когда будет, тогда и надо задуматься о том что пора писать тест.
Может получиться ситуация, когда тест будет написать очень сложно. Потому что уже принято какое-то архитектурное решение и это решение не позволяет легко написать модульный тест.
Вполне возможно, что такой подход — специфика вашей работы.
>> мы не коммитим код, если не проходят модульные тесты
А если весь день писали код, завтра выходной — суббота или воскресение, или вообще отпуск? А вдруг не дай бог грипп? Будете откладывать комммит? Это глупо. Комитить надо, но не в trunk а в спец.ветку, а возможное фиксание тестов после комита или Вы или Ваши коллеги, это уже по ситуации.
>>Может получиться ситуация, когда тест будет написать очень сложно.
Значит и пользоваться будет сложно этим кодом! Значит надо рефакторить!
А если весь день писали код, завтра выходной — суббота или воскресение, или вообще отпуск? А вдруг не дай бог грипп? Будете откладывать комммит? Это глупо. Комитить надо, но не в trunk а в спец.ветку, а возможное фиксание тестов после комита или Вы или Ваши коллеги, это уже по ситуации.
>>Может получиться ситуация, когда тест будет написать очень сложно.
Значит и пользоваться будет сложно этим кодом! Значит надо рефакторить!
А если весь день писали код, завтра выходной — суббота или воскресение, или вообще отпуск? А вдруг не дай бог грипп? Будете откладывать комммит?
Чтобы избежать подобной ситуации используются маленькие коммиты. Ситуация, когда разработчик весь день работает над кодом и делает коммит только под конец рабочего дня крайне нежелательна. Потом и code review делать сложно и есть риск, что результаты работы за день не попадут в систему контроля версий.
Коммитить в пятницу вечером — дурной тон.
Что будет? Куда будет? Какая ф1()?
Вы ввели новый интерфейс — вы должны сейчас написать к нему тест. Вне зависимости от того, насколько проста его реализация.
У вас есть студенты, которые сейчас хранятся в studentList. И есть множество подсистем, которые написаны разными людьми и используют этот studentList.
Потом васяпупкен внёс коррективы в схему хранения, запустил тесты и увидел где что упало.
А то что теперь не работает hasAnyStudents() не увидел, так как вы к ней тест не написали, а он о её существовании может ничего и не знать.
Вы ввели новый интерфейс — вы должны сейчас написать к нему тест. Вне зависимости от того, насколько проста его реализация.
У вас есть студенты, которые сейчас хранятся в studentList. И есть множество подсистем, которые написаны разными людьми и используют этот studentList.
Потом васяпупкен внёс коррективы в схему хранения, запустил тесты и увидел где что упало.
А то что теперь не работает hasAnyStudents() не увидел, так как вы к ней тест не написали, а он о её существовании может ничего и не знать.
bool ElfParser::isCorrected( const char * data, int data_len );
Извините, но не могу понять для чего нужна функция. Возможно, что при наличии теста мне было бы немного проще. Это я о том, нужно ли писать тест на очевидные вещи. Проблема в том, что они могут быть неочевидны кому-то ещё.
лучше применять вопросы: «Насколько сложна функция? Код функции уже устоялся?». Если оба вопроса «да», значит пишем тест, иначе мы просто задолбаемся актуализировать тесты.
Гм… верно ли я понял, что вы пишете тест после реализации и определения сложности функции?
>>Извините, но не могу понять для чего нужна функция.
Всего знать не получится! Ну никак! Я вот к примеру понятия не имею из чего делаются пломбы, которые используют стоматологи. Я не знаю всех нюансов TCP протокола. И что с того? Проще надо быть!
От погружения в тему предметной области для чего пишется код юнит-тесты не избавят! Когда программер «в теме» ему многое очевидно! Тогда достаточно взглянуть на прототипы и все становится понятным.
>>Извините, но не могу понять для чего нужна функция.
>>что они могут быть неочевидны кому-то ещё
Вопрос многих вещей в каком-либо проект связан со степенью осведомленности. Когда программер работает в проекте он может быть:
* новичок — и тогда лучше ему не юнит-тесты читать, а с проектом знакомиться к примеру doxygen
* автор проекта — его уж точно названиями не удивишь, он сам их придумывает как правило
* давно присоединившийся к проекту — он уже осведомлен и понимает где и что находится.
Теперь посмотрим на прототип метода:
Часть №1: Это парсер, это парсер ELF — некая хрень, возможно Executable Linux Format
Часть №2: isCorrected — на русский «это корректный? Этому набору байт можно хоть как-то верить?»
Все вместе «Определение корректности набора байт при ELF парсинге»
В чем проблема?
>>Гм… верно ли я понял, что вы пишете тест после реализации и определения сложности функции?
Сначала пишу пользовательский код, как будто предстоящий код уже написан. Исходя из написанного понимаю что и как нужно делать. Далее оцениваю какие куски кода очень мудренные, рефакторю сразу же и потом оценив сложность тех или иных функций пишу юнит-тесты
Всего знать не получится! Ну никак! Я вот к примеру понятия не имею из чего делаются пломбы, которые используют стоматологи. Я не знаю всех нюансов TCP протокола. И что с того? Проще надо быть!
От погружения в тему предметной области для чего пишется код юнит-тесты не избавят! Когда программер «в теме» ему многое очевидно! Тогда достаточно взглянуть на прототипы и все становится понятным.
>>Извините, но не могу понять для чего нужна функция.
>>что они могут быть неочевидны кому-то ещё
Вопрос многих вещей в каком-либо проект связан со степенью осведомленности. Когда программер работает в проекте он может быть:
* новичок — и тогда лучше ему не юнит-тесты читать, а с проектом знакомиться к примеру doxygen
* автор проекта — его уж точно названиями не удивишь, он сам их придумывает как правило
* давно присоединившийся к проекту — он уже осведомлен и понимает где и что находится.
Теперь посмотрим на прототип метода:
Часть №1: Это парсер, это парсер ELF — некая хрень, возможно Executable Linux Format
Часть №2: isCorrected — на русский «это корректный? Этому набору байт можно хоть как-то верить?»
Все вместе «Определение корректности набора байт при ELF парсинге»
В чем проблема?
>>Гм… верно ли я понял, что вы пишете тест после реализации и определения сложности функции?
Сначала пишу пользовательский код, как будто предстоящий код уже написан. Исходя из написанного понимаю что и как нужно делать. Далее оцениваю какие куски кода очень мудренные, рефакторю сразу же и потом оценив сложность тех или иных функций пишу юнит-тесты
isCorrected на русский можно перевести как «скоректирован ли», но никак не «корректен ли», тогда было бы «isCorrect». Кто и когда должен был что-то корректировать мне не ясно, возможно, автор кода просто плохо знает английскую языку.
и тогда лучше ему не юнит-тесты читать, а с проектом знакомиться
Я считаю, что знакомиться с проектом можно и посредством чтения модульных тестов. По крайней мере, мы в команде стремимся к такому состоянию тестов.
Сначала пишу пользовательский код, как будто предстоящий код уже написан. Исходя из написанного понимаю что и как нужно делать.
Я могу ошибаться, но мне такой подход напоминает readme driven development. Тоже очень интересный подход к разработке.
>>Я считаю, что знакомиться с проектом можно и посредством чтения модульных тестов
Да, согласен, можно и так. Но если честно я читаю чьи-либо тесты только в самую последнюю очередь! ;) А в своих домашних проектах, вообще аудио-запись используя, открываю IDE и нужный кусок кода, а потом на мобилу говорю место кода, ревизию и че вообще тут происходит. Потому что на будущее пояснить надо, а писать в лом ;)
Да, согласен, можно и так. Но если честно я читаю чьи-либо тесты только в самую последнюю очередь! ;) А в своих домашних проектах, вообще аудио-запись используя, открываю IDE и нужный кусок кода, а потом на мобилу говорю место кода, ревизию и че вообще тут происходит. Потому что на будущее пояснить надо, а писать в лом ;)
Есть мнение (лень искать ссылку на статью), что даже геттеры/сеттеры свойств надо покрывать тестами. Обосновывают написание тестов на простые функции тем, что в дальнейшем их код может стать менее примитивным и перестать выполнять то, что от него ранее ожидалось.
Unit-тест должен тестировать только класс, к которому применим, всё остальное должно быть заменено моками. Вариант с подключением кучи настоящих зависимостей — это уже не unit-тест.
Мне кажется или статья сама себе противоречит?
Сначало говорится, что нужно поощерять разработчика скоростью выполнения тестов, да бы он меньше курил чаще запускал эти самые тесты. А потом говорится, что мол работаем с реальной бд, с реальной сетью, etc.
Соглашусь с первой частью, что модульные тесты нужно запускать чаще. А вот все реальная работа с физическими ресурсами это уже интеграционные или приемочные тесты, которые не требуют большой частоты запусков, хотя так же важны и способсвуют быстрейшему нахождению проблем в коде.
Сначало говорится, что нужно поощерять разработчика скоростью выполнения тестов, да бы он меньше курил чаще запускал эти самые тесты. А потом говорится, что мол работаем с реальной бд, с реальной сетью, etc.
Соглашусь с первой частью, что модульные тесты нужно запускать чаще. А вот все реальная работа с физическими ресурсами это уже интеграционные или приемочные тесты, которые не требуют большой частоты запусков, хотя так же важны и способсвуют быстрейшему нахождению проблем в коде.
Закономерный комментарий. Потребность тестировать взаимодействие с базой данных появилась из желания максимально использовать возможности модульного тестирования на проекте. То есть тестировать всё, что получится протестировать. Ну, а желание это продиктовано стоимостью ошибки. Чем раньше и полнее протестирована система, тем «дешевле», в конечном итоге, будут обнаруженные ошибки. Ведь даже если ошибки обнаружены отделом тестирования, их исправление является достаточно затратным занятием, не говоря уже об исправлении ошибок, обнаруженных в системе, отданной в эксплуатацию. Поэтому приходится искать компромисс между скоростью выполнения тестов и затратами на использование внешних ресурсов.
В общем, здесь скорее не противоречие, а некая «золотая середина».
В общем, здесь скорее не противоречие, а некая «золотая середина».
Ну так ведь это другой вид, тестирования и в юнит тестировании их не должно быть, как выше сказал унит тесты должны тестировать только одну ответсвенность, в правильном коде в котором соблюдены правила SOLID, одной ответсвенности соответсвует один класс. В итоге все другое не как не отностится к модульным тестам, в соответсвии с чем запускается намного реже.
Ну так ведь это другой вид, тестирования и в юнит тестировании их не должно быть
Здесь опять вопрос в определении того, что считать unit тестами. В моём понимании это тесты на сущности, описанные в коде. На классы. Класс может использовать базу данных, а может не использовать. В зависимости от этого мы принимаем решение, какой будет модульный тест.
унит тесты должны тестировать только одну ответсвенность, в правильном коде в котором соблюдены правила SOLID, одной ответсвенности соответсвует один класс.
А если ответственность класса заключается в том, чтобы выбрать определённую сущность из хранилища? Некий ObjectFinder. В статье рассматривается пример, когда в качестве хранилища используется MS SQL. Поэтому и рассматривается вариант использования своеобразного mock'а БД, который максимально приближен к «оригиналу». То есть, если mock реального класса должен соответствовать интерфейсу оригинала, то mock реальной базы данных должен соответствовать структуре реальной базы данных. Изменения в структуре тестовой базы данных, необходимые для успешного прохождения модульного теста, в дальнейшем транслируются на реальную базу данных. То есть, здесь также может работать правило test first.
В итоге все другое не как не отностится к модульным тестам, в соответсвии с чем запускается намного реже.
Поэтому мы (в команде) решили немного изменить классификацию и попробовать запускать тесты намного чаще. Ту классификацию, которую мы себе взяли в работу, я описал в одном из комментариев ниже.
Класс может использовать базу данных, а может не использоватьЕсли класс содержит бизнес-логику, то он не должен стучаться к базе напрямую ни при каких обстоятельствах. Есть паттерн «репозиторий». Такие классы скрывают собой базу данных, предоставляя над ней некую абстракцию. На них тоже нужны юнит-тесты, но они тоже по-хорошему не должны стучаться к настоящей БД, а использовать ту же SQLite с хранилищем в памяти, потому что это быстро и позволяет запускать тесты не настраивая никаких дополнительных сервисов типа сервера баз данных, достаточно только IDE и компилятора.
Возможно я вас неправильно понял, но я ни в коем случае не против разделения на слои и выделения абстракций для БД. В процитированной вами фразе я имел в виду, что класс может быть как репозиторием, так и калькулятором. И тесты на эти классы будут разными. В статье описан возможный подход к тестированию того слоя, который отвечает за взаимодействие с базой данных и http запросами. Один из подходов. Причём приближенный к тем условиям, в которых будет работать код. С оглядкой на скорость выполнения тестов. При этом я совершенно согласен, что слой доступа к данным должен быть настолько «тонким» насколько это возможно. Но этот слой тоже должен тестироваться. И очень хорошо, что это можно сделать посредством модульных тестов.
К своему стыду, я не знаю, как работает SQLite и насколько специфика работы с ней отличается от специфики работы с MS SQL. Вполне возможно, что классы, отвечающие за взаимодействие с БД, будут одинаково работать и с SQLite и с MS SQL. Просто изменяя connection string. Если это не так, то получается ситуация в которой мы тестируем не то, что будет работать на самом деле. Наверное, это даже хуже, чем отсутствие тестов. Так как даёт иллюзию работоспособности кода.
использовать ту же SQLite с хранилищем в памяти
К своему стыду, я не знаю, как работает SQLite и насколько специфика работы с ней отличается от специфики работы с MS SQL. Вполне возможно, что классы, отвечающие за взаимодействие с БД, будут одинаково работать и с SQLite и с MS SQL. Просто изменяя connection string. Если это не так, то получается ситуация в которой мы тестируем не то, что будет работать на самом деле. Наверное, это даже хуже, чем отсутствие тестов. Так как даёт иллюзию работоспособности кода.
Предполагается, что внешние зависимости должны быть изолированы и тривиальны. На примере вывода на консоль
class ConsoleWriter
{
public virtual void WriteToConsole(string output){
Console.Write(output);
}
}
Экземпляр этого класса подается в конструктор, либо в свойства, и тесты уже проверяют, что тестируемый объект вызывает метод, а не пишет непосредственно в консоль. Сам же ConsoleWriter не тестируется в силу его тривиальности.
Так же и с базой данных. Не нужно имитировать базу данных. Нужно ее изолировать. С O/RM это делается вполне себе красиво.
class ConsoleWriter
{
public virtual void WriteToConsole(string output){
Console.Write(output);
}
}
Экземпляр этого класса подается в конструктор, либо в свойства, и тесты уже проверяют, что тестируемый объект вызывает метод, а не пишет непосредственно в консоль. Сам же ConsoleWriter не тестируется в силу его тривиальности.
Так же и с базой данных. Не нужно имитировать базу данных. Нужно ее изолировать. С O/RM это делается вполне себе красиво.
Сам же ConsoleWriter не тестируется в силу его тривиальности.
Ну хорошо. А как определять границы тривиальности? В моём представлении, тривиальный код — это код, который просто делегирует вызов. Допустим, если нам нужен какой-нибудь статический метод. Например, пока не используем Microsoft Fakes, но тестировать вызов
DateTime.Now
уже нужно. В этом случае мы создаём «обёртку» с методом Now()
которая просто делегирует вызов стандартному DateTime
. И эту «обёртку» мы не тестируем.Так же и с базой данных. Не нужно имитировать базу данных. Нужно ее изолировать. С O/RM это делается вполне себе красиво.
К сожалению, с базой данных не так. И ORM не со всеми запросами справляется так эффективно, как этого хотелось бы.
К сожалению, с базой данных не так. И ORM не со всеми запросами справляется так эффективно, как этого хотелось бы.
Ну так это уже совсем другая история, которая зовется «НАГРУЗОНОЕ ТЕСТИРОВАНИЕ», при которых выявляется как раз тоги, слабые места приложения.
PS. Извините не хотел не кого обидеть, не кого тролить.
Ну так это уже совсем другая история, которая зовется «НАГРУЗОНОЕ ТЕСТИРОВАНИЕ», при которых выявляется как раз тоги, слабые места приложения.
PS. Извините не хотел не кого обидеть, не кого тролить.
Извините не хотел не кого обидеть, не кого тролить.
Да вроде все разумные люди :)
Ну так это уже совсем другая история, которая зовется «НАГРУЗОНОЕ ТЕСТИРОВАНИЕ», при которых выявляется как раз тоги, слабые места приложения.
Подразумевается ведь, что нагрузочное тестирование проводится на корректно работающей системе? А как понять, что система работает корректно? Нагрузочные тесты вроде не очень для этого подходят.
Допустим, есть метод некоего класса, который предназначен для выборки данных из БД — DataSelector. Выборки данных по определённым условиям. Понятно, что в модульных тестах на сущности, использующие этот класс, будет использоваться mock нашего DataSelector'а. Но ведь хочется протестировать и сам DataSelector. Не соврёт ли, возвращая данные из БД? Поэтому пишем модульные тесты на сам DataSelector. Потом, проведя нагрузочное тестирование, о котором вы говорите, мы понимаем, что вот этот DataSelector работает медленно. Ну вот использовали в качестве реализации метода какой-нибудь ORM для скорости разработки, а он строит неоптимальные запросы. Решили в целях оптимизации переписать метод без использования ORM. Чистый ADO.NET. Или Dapper какой-нибудь. И мы можем это сделать. Потому что у нас уже есть код, проверяющий как должна работать выборка этого DataSelector'а. То есть, я пытаюсь донести мысль, что нельзя просто сказать, что мы здесь используем ORM, поэтому тело метода, использующего ORM, считаем тривиальным и не тестируем.
Ну, а нагрузочное тестирование имеет смысл проводить после того, как стало понятно, что система прошла все проверки и работает корректно. Не смог найти кому принадлежит фраза: «Медленно, но правильно работающая программа лучше, чем работающая быстро, но неправильно». Может быть фразу переврал, но надеюсь, что суть передал верно.
На самом деле не хочу показатся упертым ослом :), соглашусь с вашей точкой зрение, ну с одой оговоркой. Что в большенстве случаев не стоит тестировать так. Допустим тот же DataSelector, по умолчанию должен работать как нам нужно(его разработчик — по идеи написал тесты и описал как правильно работать с ним в документации). а мы не верим разработчики и пишем не нужные тесты на него, и боимся то что обыного мока будет не достаточно.
его разработчик — по идеи написал тесты и описал как правильно работать с ним в документации
Подождите, подождите. Скорее всего мы не поняли друг друга. Я, когда говорю про тестирование сущностей работающих с базой данных, имею в виду те сущности, которые мы реализуем сами. То есть, наш код. Реализующий нашу бизнес логику.
Если код внешний по отношению к нам, то он (этот код) модульными тестами не покрывается.
Короче я потерял, ход ваших мыслей, в предыдущем коменте вроде как говорите про ORM, и уровень DAL, а в этом уже про бизнес логику. и про то что не стоит тестить уже написаное, тот же самый орм.
Хорошо. Попробую проиллюстрировать на выдуманном примере, который только описывает общую идею.
Допустим, у нас есть следующий класс, который так или иначе взаимодействует с базой данных. Класс реализует определённый интерфейс.
И есть класс, который использует наш выдуманный репозиторий. Например, такой:
Этот
Модульные тесты на
Модульные тесты на
Как-то так.
Допустим, у нас есть следующий класс, который так или иначе взаимодействует с базой данных. Класс реализует определённый интерфейс.
public class ArticlesRepository : IArticlesRepository
{
// Здесь используется ORM. Неважно какая.
private ORM _orm = new ORM();
// Пример вымышленный. Вместо массива может быть всё, что угодно.
public Article[] GetPublishedArticles()
{
return _orm.Articles.Where(a => a.Published).ToArray();
}
}
И есть класс, который использует наш выдуманный репозиторий. Например, такой:
public class ArticlesClient
{
private IArticlesRepository _articlesRepository;
public ArticlesClient(IArticlesRepository articlesRepository)
{
_articlesRepository = articlesRepository;
}
public int CalculatePublishedArticles()
{
return _articlesRepository.GetPublishedArticles().Length;
}
}
Этот
ArticlesClient
тоже может реализовывать какой-нибудь интерфейс. И использоваться в любом необходимом месте. В контроллере или ещё где-нибудь. Модульные тесты на
ArticlesClient
не будут использовать базу данных. В качестве реализации IArticlesRepository
будет использоваться mock. Модульные тесты на
ArticlesRepository
будут использовать базу данных, разворачиваемую в памяти. В статье описан способ с использованием MS SQL.ORM
же — внешний код по отношению к нашему. Возможно, это просто библиотека или набор библиотек, которые пришли к нам из NuGet. Мы не тестируем код этой библиотеки. Мы тестируем то, что с помощью него делаем. Нашу логику. Как-то так.
Все теперь понятно, стало что вы имели в виду.
Как по мне то такие вещи не обязательно тестировать именнов в модульных тестах.
Почему бы не тестированать работу ArticlesRepository в интреграционных тестах? которые запускаются не так часто как модульные тесты?
Как по мне то такие вещи не обязательно тестировать именнов в модульных тестах.
Почему бы не тестированать работу ArticlesRepository в интреграционных тестах? которые запускаются не так часто как модульные тесты?
Как уже сказано, это не юнит тесты. Это можно назвать интеграционными или behavior тестами. И они пишутся, как правило после реализации. Юнит тесты в рамках TDD пишутся до реализации и не должны тестировать внешние зависимости в принципе.
Тест не должен звучать как «объект должен достать из базы, потом посчитать что-то», он должен звучать как «объект должен вызвать метод, получающий что-то и посчитать».
Я в последнее время склоняюсь к мысли, что тесты в рамках TDD нужны не для того, чтобы чего-то там защитить, чтобы кто-то потом не сломал, а для того, чтобы код был простой и чистый. Простой код просто тестировать, его проще потом поддерживать. Соответственно код который трудно тестировать — трудно поддерживать. TDD заставляет разработчика писать хороший код.
Тест не должен звучать как «объект должен достать из базы, потом посчитать что-то», он должен звучать как «объект должен вызвать метод, получающий что-то и посчитать».
Я в последнее время склоняюсь к мысли, что тесты в рамках TDD нужны не для того, чтобы чего-то там защитить, чтобы кто-то потом не сломал, а для того, чтобы код был простой и чистый. Простой код просто тестировать, его проще потом поддерживать. Соответственно код который трудно тестировать — трудно поддерживать. TDD заставляет разработчика писать хороший код.
Как уже сказано, это не юнит тесты. Это можно назвать интеграционными или behavior тестами. И они пишутся, как правило после реализации. Юнит тесты в рамках TDD пишутся до реализации
Вопрос терминологии опять же. Если под «этим» подразумеваются тесты на сущности, работающие с БД, то в описываемом подходе вполне возможно писать тесты до реализации. Мы, собственно, на проекте так и делаем.
Тест не должен звучать как «объект должен достать из базы, потом посчитать что-то», он должен звучать как «объект должен вызвать метод, получающий что-то и посчитать».
Тесты на объект который должен вызвать метод и выполняющий подсчёт выглядят именно так. Другое дело, что на метод, который получает что-то мы тоже пишем тесты. Разворачивая БД в памяти.
Я в последнее время склоняюсь к мысли, что тесты в рамках TDD нужны не для того, чтобы чего-то там защитить
Я бы сказал «не только для того». Это одно из преимуществ TDD. Дополнительно к тем, которые вы описали.
Использование настоящей базы данных, даже если и на рамдиске, создает дополнительную нагрузку на чистку базы данных перед каждым независимым тестом, иначе может случиться так, что один тест, записавший в таблицу какие-нибудь данные и не удаливший их, отвалит другой тест, в совершенно другом месте, причем без отладки обнаружить причину отвала — невозможно. А отлаживать модульные тесты — это то же самое, что забивать гвозди в молоток — когда инструмент создает нам дополнительную работу. Модульные тесты — это замена отладки. То есть, по не прошедшему тесту, должно быть очевидно, какую строчку в коде надо править, чтобы тест прошел. Оставим реальные ресурсы интеграционным тестам: модульный тест пусть работает со стабами.
Чаще всего необходимость тестировать классы, подсовывая им реальные ресурсы, приходится из двойной ответственности оных. То есть они и принимают какое-нибудь интересующее нас бизнес решение и лезут в базу или в сеть. Между тем, если сеть или базу спрятать за своей абстракцией, то:
Лично я люблю модульное тестирование за не указанный вами пункт: модульное тестирование является экзаменом на дизайн. Если тест лаконичен, значит класс легко повторно использовать и у него простая ответственность. Если мне легко тестировать, значит будущим клиентам будет легко использовать. В том числе и повторно!
Чаще всего необходимость тестировать классы, подсовывая им реальные ресурсы, приходится из двойной ответственности оных. То есть они и принимают какое-нибудь интересующее нас бизнес решение и лезут в базу или в сеть. Между тем, если сеть или базу спрятать за своей абстракцией, то:
- Бизнес логика становится более повторно используемой с другими базами
- Тесты становятся более выразительными и лаконичными
- Происходит эпическое разделение ответственности, что сказывается на архитектуре приложения исключительно положительно
Лично я люблю модульное тестирование за не указанный вами пункт: модульное тестирование является экзаменом на дизайн. Если тест лаконичен, значит класс легко повторно использовать и у него простая ответственность. Если мне легко тестировать, значит будущим клиентам будет легко использовать. В том числе и повторно!
Лично я тоже склоняюсь к использованию реальной базы данных для тестирования работы тех компонентов, которым требуются данные из БД. Особенно в небольших проектах. Практически всегда можно обойтись Sql Compact'ом 4ой версии и инициализировать отдельную БД при каждом запуске теста, примерно вот так:
Тест обрабатывается за 1-2 секунды. Правда используется MSTests, вместо NUnit.
public TestContext TestContext { get; set; }
[TestInitialize]
public void SetupTestConnectionString()
{
string dbFilename = Path.Combine(TestContext.DeploymentDirectory, GetType().Name + ".sdf");
Database.DefaultConnectionFactory = new System.Data.Entity.Infrastructure
.SqlCeConnectionFactory("System.Data.SqlServerCe.4.0", Path.GetDirectoryName(dbFilename),
string.Format("Data Source={0}", dbFilename));
TestContext.AddResultFile(dbFilename);
}
Тест обрабатывается за 1-2 секунды. Правда используется MSTests, вместо NUnit.
То, о чём вы говорите верно в том случае, если используется одна база данных на весь набор тестов. В этом случае тесты становятся связанными между собой через базу данных и приходится чистить базу после выполнения каждого теста. И вполне возможна ситуация, при которой изменения в одном тесте влекут за собой падение всего набора. Тогда да, приходится отлаживать тесты. Не самая приятная ситуация. У нас был такой опыт. После которого мы пришли к тому решению, которое и описано в статье. База данных всегда новая. Создается перед выполнением теста. Полностью чистая. И каждый тест наполняет базу именно теми данными, которые ему нужны для работы. То есть выполняются предусловия, действие и постусловия.
Про экзамен на дизайн — спасибо!
Про экзамен на дизайн — спасибо!
Ну, создание базы каждый раз с нуля можно больно ударить по производительности. Тем более, что при более сложных организациях объектов (когда они друг на друга ссылаются), ваше требование держать тест коротким сводится на нет, потому что стадия arrange растягивается на 10-20 строчек, если не больше. В этом случае возникает естественное убрать из тестов дубликацию arrange-а в SetUp, но тогда SetUp становится узким местом, так как заполняет всеми возможными данными для всех тестов, так что скоро тесты начинают все равно неявно влиять друг на друга через SetUp. Если проверяются простые операции, типа что работают инсерты и селекты — этого но произойдет. Но если начать проверку бизнес логики методом заполнения базы данных — берегитесь этой сложности.
Плюс в работе с реальной базой данных есть тот недостаток, что сложно, например, проверить пейджинг. Чтобы проверить, что при наличии 1000 записей вернутся только 10, эти 1000 записей приходится пихать в базу. А это приводит к замедлению выполнения теста.
Плюс в работе с реальной базой данных есть тот недостаток, что сложно, например, проверить пейджинг. Чтобы проверить, что при наличии 1000 записей вернутся только 10, эти 1000 записей приходится пихать в базу. А это приводит к замедлению выполнения теста.
Ну, создание базы каждый раз с нуля можно больно ударить по производительности.
На сегодняшний день сборка проекта плюс запуск всех модульных тестов составляет чуть больше 4 минут. Это не то чтобы больно, но да, хочется быстрее. Про создание базы данных я неверно выразился. Под созданием базы данных подразумевается копирование файлов БД на RAM диск и подключение этих файлов к экземпляру SQL сервера. Опять же, это только один из возможных подходов к тестирования слоя взаимодействия с БД.
Тем более, что при более сложных организациях объектов (когда они друг на друга ссылаются), ваше требование держать тест коротким сводится на нет, потому что стадия arrange растягивается на 10-20 строчек, если не больше.
Подобная ситуация может также возникнуть при неправильном использовании паттерна «репозиторий». То есть, когда начинает появляться масса методов, которые логически друг с другом не связаны. Как правило, методы подобных классов связаны между собой только общим контектом подключения к БД. Это довольно легко проверить. Если при добавлении в сигнатуру метода контекста БД метод может стать статическим, то это оно и есть. Обычно подобные классы имеют тенденцию разрастаться до состояния «невлезания» в экран. Тесты на подобные классы очень сложны для реализации. Вернее, начинается всё вроде бы неплохо, но со временем код теста превращается в одну большую нечитаемую массу. В этой ситуации можно попробовать провести декомпозицию такого «репозитория». Превратить методы в классы. Для которых будут свои тесты.
Но если начать проверку бизнес логики методом заполнения базы данных — берегитесь этой сложности.
Поэтому мы стремимся максимум бизнес логики сосредоточить в сущностях, которым просто нужны данные. Независимо от источника.
Плюс в работе с реальной базой данных есть тот недостаток, что сложно, например, проверить пейджинг. Чтобы проверить, что при наличии 1000 записей вернутся только 10, эти 1000 записей приходится пихать в базу.
Для проверки постраничной выборки совсем необязательно вставлять в базу данных 1000 записей. Цель ведь заключается в том, чтобы проверить сам механизм постраничной выборки, а не тот факт, что из 1000 записей вернется только 10. Ведь можно 10 записей разбить на страницы? Нужно только указать размер страницы. Для теста он может быть равен 2 записям. Если со стороны заказчика есть требование, что на странице должно отображаться только 10 записей, то это требование может быть проверено при функциональном тестировании.
Для своих проектов весь слой базы данных я выделяю в отдельный интерфейс (или несколько).
Это сделано для того, чтобы можно было легко поменять реализацию БД, или вообще перейти на другой тип хранилища.
Получается, что этот слой содержит
1. Запросы к БД
2. Какую-то нехитрую логику.
К примеру, метод
User getOrCreateUser(String firstName, String lastName)
Ищет пользователя по имени, если не находит — создает пользователя и заполняет служебные колонки: dateCreated, dateUpdated.
Очевидно, это надо протестировать. Также как и автор статьи, я использую базенку в памяти.
Как бы вы все сделали?
И как называются такие тесты? Которые запускаются с юнит-тестами, на них похожи, полностью автономны от внешних зависимостей и т.п.
Это сделано для того, чтобы можно было легко поменять реализацию БД, или вообще перейти на другой тип хранилища.
Получается, что этот слой содержит
1. Запросы к БД
2. Какую-то нехитрую логику.
К примеру, метод
User getOrCreateUser(String firstName, String lastName)
Ищет пользователя по имени, если не находит — создает пользователя и заполняет служебные колонки: dateCreated, dateUpdated.
Очевидно, это надо протестировать. Также как и автор статьи, я использую базенку в памяти.
Как бы вы все сделали?
И как называются такие тесты? Которые запускаются с юнит-тестами, на них похожи, полностью автономны от внешних зависимостей и т.п.
Я бы во-первых, сделал две отдельные функции — найти пользователя и создать пользователя (вот и разделение ответственности в полной красе). В класс, в котором живут эти функции сделал бы protected доступ к DataSet-у, с которыми бы работали обе функции. Обе функции lookupUser и createUser (для которой отдал бы в класс интерфейс, скрывающий системное время, ибо время — это тоже внешний ресурс) сделал бы виртуальными. Далее, тестил этот класс самошунтированием — т.е. унаследовал бы мою тестовую фикстуру от него, проверил, что сначала при вызове функций lookupUser и createUser в моем DataSet-е все хорошо. Затем, замокав функции lookupUser и createUser проверил бы, что при возвращении нулевого количества пользователей из
lookupUser
фасадная User getOrCreateUser(String firstName, String lastName)
вызывает createUser
.Подход ваш понял, классно.
Один момент по поводу моей реализации тестов:
По ходу подготовки к тесту и его запуска, автоматически тестируется еще:
1. Слой миграций — все ли миграции БД прошли на in-memory БД и все ли хорошо. Тест неявно это покажет.
2. Слой ORM. Бывает, что не очень понятно, как замаппить такую-то колонку БД на поле объекта, особенно в связях many-to-many и каскадных сохранениях данных. Тесты с участием реального ORM очень помогают не «удивляться» особенностям ORM, вылезшим на PROD.
Один момент по поводу моей реализации тестов:
По ходу подготовки к тесту и его запуска, автоматически тестируется еще:
1. Слой миграций — все ли миграции БД прошли на in-memory БД и все ли хорошо. Тест неявно это покажет.
2. Слой ORM. Бывает, что не очень понятно, как замаппить такую-то колонку БД на поле объекта, особенно в связях many-to-many и каскадных сохранениях данных. Тесты с участием реального ORM очень помогают не «удивляться» особенностям ORM, вылезшим на PROD.
И как называются такие тесты? Которые запускаются с юнит-тестами, на них похожи, полностью автономны от внешних зависимостей и т.п.
Терминология и определения это вообще очень скользкая тема. Если жестко следовать всем определениям, то часть кода тестируется (как правило, не самая важная), а часть нет. В итоге, если сказать, что мы вот это и вот это не тестируем, то количество кода в нетестируемых местах начинает планомерно увеличиваться.
В общем, мы решили использовать другую классификацию тестов.
Функциональные тесты — это тесты, предназначенные для проверки функционирования какого-либо компонента программной системы. Грубо говоря, если наша система состоит из набора исполняемых модулей, то тестирование одного такого модуля будет являться функциональным. То есть, компонент системы является «чёрным ящиком» для которого описывается, что при входном значении X на выходе будет значение Y или, если рассматривать приложение типа WinForms, описание может выглядеть так: «Если нажать на эту кнопочку, то в этом поле появится такое-то значение». Проверка подобных условий и будет являться функциональным тестированием.
Интеграционные тесты — это тесты, предназначенные для тестирования системы в целом. Не отдельных компонентов, как в случае с функциональными тестами, а всей системы. Тесты этой группы самые «дорогие». Ведь зачастую, чтобы развернуть систему, требуется достаточно много времени и ресурсов. Иногда даже приходится дёргать системных администраторов, которые не всегда горят желанием заниматься проблемами разработчиков. В общем, интеграционное тестирование я отношу к разряду «экстремальных».
Ну и наконец, модульные или unit тесты — это тесты, предназначенные для проверки логики работы программных компонентов. Таких как классы, методы, процедуры или функции. Эти тесты самые «дешёвые» среди трёх. Для того, чтобы протестировать логику работы метода или функции не требуется прилагать какие-то значительные усилия. Всё, что нужно уже есть или доступно в рабочем окружении программиста. Ну, а раз модульные тесты наиболее доступны по сравнению с функциональными и интеграционными, то имеет смысл сосредоточиться именно на них и постараться получить от их использования максимум выгоды, что, собственно, и предполагает TDD.
К вопросу о юнит-тесты vs. интеграционные тесты, старенькое видео о плюсах и минусах:
www.devclub.eu/2011/06/06/asolntsev-real-life-unit-tests/
www.devclub.eu/2011/06/06/asolntsev-real-life-unit-tests/
Для запуска тестов взаимодействующих с БД, но без изменения структуры, можно использовать другой механизм — транзакции в DCOM. Он корректно работает с ADO, LINQ2SQL, Entity.
В NUnit наследовать все тесты с БД от класса:
Есть некоторые особенности использования, но они очень легко обходятся.
В NUnit наследовать все тесты с БД от класса:
namespace TestNamespace
{
using System.EnterpriseServices;
using NUnit.Framework;
[Transaction(TransactionOption.Required)]
public class RepositoryTestsBase : ServicedComponent
{
[TearDown]
public virtual void Teardown()
{
System.Data.SqlClient.SqlConnection.ClearAllPools();
if (ContextUtil.IsInTransaction)
{
ContextUtil.SetAbort();
}
}
}
}
Есть некоторые особенности использования, но они очень легко обходятся.
1. В конторе, где я сейчас работаю, следующая схема:
Два билда в TFS:
— Один просто билд+юнит тесты, второй — билд+интеграционные тесты.
— Второй в начале воссоздаёт виртуальную машину из чистого образа, инсталлирует в неё все msi и запускает систему. После этого запускает интеграционные тесты, работающие с реальной БД и прочая. Для совсем внешних зависимостей используется самодельный SoapMock.
Архитектура разбита так, что в юнит тестах Entity не тестируется (её как бы тестирует микрософт), но есть ДБ слой, который эмулируется.
2. Если в контроллерах много бизнес логики — это уже признак того, что возможно не всё чисто. Бизнес логику стоит выносить из контроллера, пусть он занимается только роутингом и сборкой моделей из данных, получаемых из отдельных модулей. Вот как раз эти модули можно легко и просто тестировать без привязки к контексту контроллера. И одтельно, если хочется, можно тогда тестировать сами контроллеры, передавая им эмулированые провайдеры бизнес логики. DI rulez.
Два билда в TFS:
— Один просто билд+юнит тесты, второй — билд+интеграционные тесты.
— Второй в начале воссоздаёт виртуальную машину из чистого образа, инсталлирует в неё все msi и запускает систему. После этого запускает интеграционные тесты, работающие с реальной БД и прочая. Для совсем внешних зависимостей используется самодельный SoapMock.
Архитектура разбита так, что в юнит тестах Entity не тестируется (её как бы тестирует микрософт), но есть ДБ слой, который эмулируется.
2. Если в контроллерах много бизнес логики — это уже признак того, что возможно не всё чисто. Бизнес логику стоит выносить из контроллера, пусть он занимается только роутингом и сборкой моделей из данных, получаемых из отдельных модулей. Вот как раз эти модули можно легко и просто тестировать без привязки к контексту контроллера. И одтельно, если хочется, можно тогда тестировать сами контроллеры, передавая им эмулированые провайдеры бизнес логики. DI rulez.
Sign up to leave a comment.
Unit тесты на практике