Итак, начнем с этого чрезвычайно практичного подхода к тестированию.
Тестирование, я должен признаться, где-то всего лишь три или четыре года назад
я принял как религию. И то, что я имею в виду, отчасти так и было: мне говорили, что это
важно, я верил, что это важно, я пытался делать это, но именно этот подход действительно изменил
мою жизнь. И я надеюсь, изменит и вашу тоже.
И нет, я не о выборах, это ни в коем случае. Итак, давайте поговорим о
юнит-тестах. В книге есть небольшая вводная о различных видах тестирования.
Мы сначала заострим внимание на юнит-тестах и немного на функциональных тестах,
и как и со многими другими темами на этих лекциях, есть удобный акроним, который поможет нам запомнить
каким должны быть хорошие юнит-тесты. Итак, для начала - они должны быть быстрыми -
т.е. не должны выполняться слишком долго; они должны быть независимыми.
Это означает, что не должно иметь значения, какой тест надо запускать первым, какой - вторым
порядок не важен. Они не должны иметь зависимостей друг от друга.
Они должны быть воспроизводимыми - т.е. если тест находит ошибку, он должен находить ее каждый раз.
В некоторых случаях этого легко добиться, в других - довольно непросто.
Самоконтроль. Вот что означало тестирование не так давно для многих компаний:
Программное обеспечение перебрасывали через стену в отдел контроля качества, а там сотрудники отдела
вручную понажимают кнопочки в программе, что-то поделают в ней и - "О, оно работает, оно работает"
Но мы ведь не хотим больше этого, правда? Тест сам должен знать
прошел ли он или сломался. И чтобы принять это решение не должно требоваться человеческое вмешательство.
И, наконец, своевременность. Это означает, что тесты должны быть написаны практически одновременно
с программным кодом. Если код меняется, тесты также также должны быть изменены.
А на самом деле, мы собираемся делать это еще агрессивнее. Бы будем писать тесты в первую очередь, еще до того как будет написан код.
То есть настолько своевременно, насколько вы можете.
Итак что все это значит? Зачем нам нужны быстрые тесты?
Затем, что мы можем запускать какое-то подмножество тестов все время.
И если у нас тысячи и тысячи юнит-тестов, что не является необычным даже для проектов средних размеров,
это может занять, как вы догадываетесь, минуту или две, чтобы выполнить набор тестов и это будет нас тормозить.
Все что нам нужно - возможность быстро запускать только те тесты, которые относятся к конкретному куску кода,
над которым мы работаем таким образом, чтобы не сбиваться с ритма.
Независимость нужна по той же причине, чтобы мы могли запустить любое подмножество тестов
и в любом порядке, в каком захотим.
Поэтому, плохо, когда есть множество тестов, которые можно запускать
только тогда, когда перед ними запускали какие-либо другие тесты.
Воспроизводимость, как вы снова догадываетесь, означает что запустив тест N раз мы получим одинаковые результаты.
Если мы хотим локализовать ошибку и включить возможность автоматической отладки, воспроизводимость важна.
Самоконтроль: как я уже говорил, человек не должен проверять результат.
Это означает, что мы можем иметь тесты, выполняющиеся в фоне все время
и когда бы мы не внесли изменения, которые ломают что-то в 25 милях в другом участке кода,
какой-нибудь тест обнаружит это и привлечет наше внимание.
И, наконец, своевременность. Как я и говорил, мы собираемся использовать разработку через тестирование,
в которой тесты пишутся перед тем, как будет написан программный код.
Мы будем использовать RSpec, который я рассматриваю как предметно-ориентированный язык для написания тестов.
Для тех, кто не знаком с подобными языками, скажу, что в основе это вроде небольшого языка программирования
который умеет делать небольшое количество вещей в рамках одной предметной области. Т.е. это не язык общего назначения.
На самом деле мы уже видели примеры подобного языка. Миграции являются разновидностью DSL.
Это небольшое подмножество операторов, чья единственная работа - описывать изменения в схему базы данных..
Т.о., миграции оказались DSL, который встроен в Ruby, т.е.
миграции являются всего лишь кодом на Ruby, но стилизованные под задачи, которые они выполняют.
На самом деле мы увидим, что RSpec - похожий пример. Итак, мы будем называть все подобное внутренним DSL.
Он реализован внутри другого языка. Регулярные выражения - тоже внутренний DSL.
Он как подмножество действий, которые мы можем выполнять в регулярных выражениях.
Другой пример - внешний или автономный DSL - это SQL. SQL-запросы к базам данных.
Это отдельный язык, и те, кто работали с другими фреймворками
перед тем, как перейти к Rails, обычно заканчивали тем, что писали SQL-запросы
и затем передавали кому-то, да? Так вот это очень яркий пример работы с разными языками.
Итак, в RSpec, каждый тест называется спекой - от "спецификация" (specification).
Как ни странно, они размещаются в каталоге, называемом "spec" - потому что нам нравится все делать просто.
В Rails есть генератор, "rspec:install", который создает структуру подкаталогов.
Все это есть в книге и в последующих демонстрациях, которые мы покажем сегодня, мы подразумеваем,
что мы уже выполнили эти подготовительные шаги.
Итак, с чего все начинается? Подкаталоги каталога spec организованы так, чтобы отражать структуру
нашего приложения. Так вот: в app/models у вас лежат ваши модели,
а в spec/models у вас лежат spec-файл для каждой модели. Ничего удивительного.
Подобным же образом располагаются спеки для контроллеров. А как насчет представлений (view)?
Вообще, мы не будем делать спеки для представлений. Сделать их можно, но будет это несколько
кривовато - много из того, что мы хотим проверить в представлении, на самом деле
можно проверить в контроллере, и мы увидим это в сегодняшнем примере.
К тому же, мы решили, что наш подход для веб-приложений, с которыми напрямую взаимодействуют пользователи
- через создание пользовательских историй, которые описывают те части приложения, с которыми взаимодействует заказчик.
Таким образом, то, что является частью представления - что должно быть видимым в представлении
и то, что может быть нажато и т.д., мы для всего этого будем использовать Cucumber.
И в дальнейшем мы так и будем поступать.
Таким образом в основном мы будем уделять внимание RSpec с точки зрения
написания спецификаций для наших моделей и контроллеров.
Итак, давайте начнем с примера новой гипотетической фичи для RottenPotatoes,
при помощи которой мы можем добавлять фильмы, используя данные из TMDb.
TMDb - реальный сайт. Он вроде IMDb, но только не коммерческий, что-то вроде open-source проекта.
Идея в том, что у них есть вся информация о фильмах, и если мы хотим
добавить фильм в RottenPotatoes, то почему бы нам просто не скачать нужную информацию оттуда?
На деле, когда мы обсуждали пользовательские истории, в одной из них был шаг, в котором говорится
"Я заполняю ключевые слова для поиска, Я хочу найти фильм "Начало",
и когда я нажимаю кнопку, мне предлагают "Искать на TMDb", не так ли?
Таким образом, предполагается, что должна быть кнопка, по нажатию которой наше приложение обратится к TMDb
и проверит, есть ли там "Начало", и если да, возьмет информацию о нем оттуда.
Вопрос в том, что мы должны сделать. Какой код нужно написать
и какой вид тестирования нужно применить.
И прежде, чем мы займемся этим, помните наш разговор о "Кулинарии Rails",
рецептах, как готовить при помощи Rails? Вспомните, что когда мы добавляем какую-либо новую фичу
это означает, что нам нужен новый маршрут (route), новый метод контроллера. Также нам может понадобиться,
а может и нет, новое представление. Это зависит от того, может ли эта фича использовать существующее представление
или мы должны сделать новое. Эти шаги мы должны выполнять всегда.
Итак, давайте займемся всем этим, но будем делать по одному шагу за раз.
Ладно, так. Это идея, которую мы будем видеть многократно. Я всего лишь приучаю вас к фразе
"Код, который вы бы хотели иметь." Это очень сильная мысль, как только вы к ней привыкнете.
Она странно звучит, когда слышите ее первый раз, но она действительно очень сильная.
Итак, мы спрашиваем себя: "Хорошо, когда пользователь нажимает на эту кнопку "Искать на TMDb", мы знаем, что
где-то должен быть метод контроллера, который получит все то, что было отправлено через форму.
Так что должен делать этот метод? Что должен делать метод контроллера, когда получает форму поиска?
Если бы нас спросили об этом, и если бы мы написали, что он должен делать на разговорном языке
наш ответ звучал бы как "Ладно, смотри, он должен вызывать метод (который мы еще не написали)
который сходит на TMDb и поищет там фильм". Разумно.
"Если он там найдет фильм, он должен сформировать некоторое представление, чтобы показать результат поиска
(и снова, мы пока еще не создали это представление, но логически, это как раз то, что мы собираемся
написать)." Сегодня на третий шаг нам времени не хватит, но для неуспешного исхода сценарий таков:
"Если фильм не найден, метод должен перенаправить на главную страницу RottenPotatoes и сообщить
"Ничего не найдено". И если если вы просмотрите пример в книге,
мы на самом деле вариант неуспешного исхода проработали в главе, посвященной BDD.
Итак, у нас есть эти два момента. Сосредоточимся на №1 и №2 - это то, что метод контроллера должен делать,
и вот как мы будем это описывать. Где моя мышь? Итак, поехали.
Рассмотрим, как мы опишем все эти требования при помощи RSpec. Итак...
Ничего особенного здесь ни происходит, видите. Все достаточно просто, не так ли?
И это корректный код RSpec. SpecHelper - это просто файл, который RSpec создает как часть шага установки.
Он просто выполняет кое-какие вещи, гарантирующие, что все необходимое подгружено.
И собираемся начать, спросив "Что должен проверять этот тест?"
Или, мы собираемся описать поведение MoviesController.
У контроллера есть много вариантов поведения, но нас волнует одно конкретное, а именно
поведение во время поиска на TMDb. Итак, мы можем представить, для каждого поведения в контроллере
как растет наша спека, мы будем добавлять больше описательных блоков внутри этого внешнего
"describe MoviesController", и так далее вкладывать, вкладывать, вкладывать.
Все, что я здесь сделал - это переписал те три требования, о которых мы говорили.
Итак, "it" - это на самом деле вызов метода в RSpec. Он принимает аргумент, который является
строкой, описывающий, что должно произойти. И как мы увидим,
он принимает также второй аргумент, который является процедурой, которая и выполняет тест.
Но пока, все что мы сделали - лишь сделали перевод. Мы обдумали три требования
которые должен удовлетворять контроллер, и мы написали эти три требования в RSpec.
Этого достаточно, чтобы выполнить запуск, и у нас есть скринкаст, который я рекомендую вам посмотреть.
Он соответствует главе книги и в нем как раз все это делается.
Все, что он делает - запускает три теста, которые ничего не делают. Поэтом вывод RSpec желтый, да?
Желтый означает "еще не реализовано", точно как и в Cucumber.