Написание первых тестов
Я в целом противник методики TDD. На мой взгляд, при решении любой задачи нужно в первую очередь решить её, а уже потом покрывать тестами. Писать тесты на несуществующий код не имеет особого смысла, особенно если вы начинаете проект с нуля и ещё не до конца представляете как он будет выглядеть и работать (но уже знаете чего вы от него хотите). В случае разработки первой версии mkdev перед нами стояли такие важные задачи как автоматизация рутинных процессов обучения и предоставления ученикам и менторам удобного интерфейса для отслеживания выполнения заданий. После релиза первой версии (который произошёл на 23% быстрее, так как не пришлось сразу же тратить время на тесты) появились ещё более важные задачи: код для платежей, подписок, оповещений, фоновых задач и т.п.
Теперь, когда мы уже точно знаем в какую сторону движется разработка mkdev и какие его фичи прижились и нужны пользователям, нам необходимо покрыть их тестами. Этим я сегодня и займусь: в этом материале мы пройдём от состояния, в котором в приложении для тестов не настроено вообще ничего, до покрытия тестами основной функциональности проекта.
Внимание! Такое "позднее" покрытие тестами оправдано если у вас уже есть опыт написания надёжного работающего кода без значительных багов и при этом проект принадлежит вам лично, а не компании, на которую вы работаете. В случае наёмной работы обязательно покрывайте свой код тестами. Я ни в коем случае не агитирую против тестов. Я агитирую против траты времени на тесты до того, как вы решили поставленную перед вами задачу.
Приступим! В mkdev мы будем использовать rspec-rails, capybara и factory_girl. Нашей задачей на сегодня будет написать feature спеки, то есть тесты, которые проверяют правильность работы приложения со стороны пользователя. Возможно, вы уже столкнулись с написанием подобных тестов в одном из заданий курса по Rails ;)]
Добавляем необходимые гемы в Gemfile
:
group :test do
gem 'rspec-rails', '~> 3.0.0'
gem 'factory_girl_rails'
gem 'capybara'
end
Следуя инструкции гема rspec-rails
генерируем необходимые для тестов файлы:
rails generate rspec:install
Теперь необходимо подключить capybara
и factory_girl
в spec/rails_helper.rb
:
# spec/rails_helper.rb
# ...
require 'rspec/rails' # эта строчка там уже была
require 'capybara/rails' # вот эту строчку добавили
# ...
RSpec.configure do |config|
# ...
config.include FactoryGirl::Syntax::Methods
# ...
end
Можно переходить к написанию первого теста! Создаём папку spec/features/
, в ней файл course_spec.rb
. Как видно из названия, мы будем тестировать что-то связанное с курсами. Для каждого теста в этом файле нам понадобится залогиненный пользователь:
describe 'Taking a course' do
let!(:user) { create(:user, email: "bob@mail.ru", password: "qweqweqwe") }
before(:each) do
login("bob@mail.ru", "qweqweqwe")
end
end
Естественно, этот код ещё не работает, так как мы не создали factory для модели User
и не создали helper метод login
, внутри которого мы будем проходить через процедуру логина пользователя (подозреваю, мы будем часто логиниться в feature спеках, поэтому лучше сразу же заDRYим этот код).
# spec/factories/subscription.rb
FactoryGirl.define do
factory :subscription do
active true
expire_date Date.today + 4.weeks
end
end
# spec/factories/user.rb
FactoryGirl.define do
factory :user do
first_name "Boris"
last_name "Spider"
password "secret123"
password_confirmation { password }
customer_id "superbad"
after(:create) do |user|
user.subscription.update_attributes(active: true, expire_date: Date.today + 4.weeks)
end
end
end
# spec/support/login_helper.rb
def login(email, password)
visit root_path
click_link "Войти"
fill_in :user_email, with: email
fill_in :user_password, with: password
click_button "Войти"
end
Теперь гораздо лучше! Попробуем написать первый тест:
# spec/features/course_spec.rb
# ...
it "opens courses page after login" do
expect(page).to have_content "Текущие курсы"
end
# ...
Тест, запущенный командой bundle exec rspec
, успешно проходит. Теперь я хочу протестировать что если в системе существует курс с заданиями, то пользователь видит ссылку Начать курс, кликает по ней и видит после этого название первого задания. Нам понадобятся factory для Course
и Task
:
# spec/factories/course.rb
FactoryGirl.define do
factory :course do
title "Ruby"
end
end
# spec/factories/task.rb
FactoryGirl.define do
factory :task do
position 1
title "Task 1"
text "Do some crazy shit!"
end
end
Сам тест выглядит так:
# spec/features/course_spec.rb
# ...
context "can start and go through course" do
before(:each) do
course = create(:course, title: "Ruby on Rails")
task = create(:task, course: course, title: "Простой контроллер")
end
it "can start course" do
visit dashboard_root_path
click_link "Начать курс"
expect(page).to have_content "Простой контроллер"
end
end
# ...
Судя по всему новый пользователь может стартовать курс без каких-либо проблем. Протестируем теперь что он не увидит кнопку "Принять" после отправки задания на проверку, а так же то что после отправки на проверку в журнал задания добавляется новая запись.
# spec/features/course_spec.rb
# ...
context "sends a task for review" do
before(:each) do
visit dashboard_root_path
click_link "Начать курс"
click_link "Простой контроллер"
fill_in "user_task_pull_request_url", with: "http://github.com/pulls/1"
click_button "Добавить PR"
click_link "На проверку"
end
it "updates task journal" do
expect(page).to have_content "Отправлено на проверку"
end
it "doesn't show mentors buttons" do
expect(page).not_to have_content "Принять"
end
end
# ...
Обратите внимание: я не использую больше одного expect'а в каждом тесте. Это считается хорошим стилем, потому что в таком случае каждый тест проверяет одну конкретную вещь, а не сразу же сотню различных аспектов. Таким образом если упадёт один тест, то мы знаем что проблема только в одной части кода. Если бы у нас было несколько проверок в одном тесте, то мы бы не смогли узнать результат тех, что идут ниже проваленной проверки. Например:
it "updates task journal" do
expect(page).to have_content "Отправлено на проверку"
expect(page).not_to have_content "Принять"
end
Если первый expect вывалится с ошибкой, то мы узнаем что не работает запись в журнал. Но так как ошибка уже вывалилась, то мы не узнаем выводится ли пользователю кнопка "Принять" до тех пор, пока не починим предыдущую проверку.
Ещё один важный момент: обратите внимание насколько точно эти тесты воспроизводят действия пользователя. Я не проверяю сам код, которые отвечает за то, с чем столкнётся пользователь, я тестирую только отклик приложения на его действия. Внутренняя реализация моделей и контроллеров может полностью поменяться, но процедура работы с курсами и заданиями не должна сломаться после этих изменений. Именно это и гарантируют нам feature тесты: что приложение работает правильно со стороны пользователя. Согласитесь, что это, пожалуй, самое важное в любом приложении.
Пожалуй, стоит добавить ещё парочку небольших тестов, проверящих следующее:
- у простых пользователей нет доступа к админке
- у админов есть доступ к чему угодно
- у редакторов нет доступ к редактированию пользователей
Приведу целиком финальный тест, который у меня получился:
# spec/features/admin_spec.rb
require 'rails_helper'
describe 'Accessing admin' do
let!(:user) { create(:user, email: "bob@mail.ru", password: "qweqweqwe") }
before(:each) do
login("bob@mail.ru", "qweqweqwe")
end
it "doesn't show admin for regular user" do
expect(page).not_to have_content "Админка"
end
it "redirects user to account when he tries to access admin" do
visit admin_root_path
expect(page).to have_content "Текущие курсы"
end
context "admin user" do
before(:each) do
user.add_role :admin
visit admin_root_path
end
it "shows link to admin" do
expect(page).to have_content "Админка"
end
it "shows link to users managment" do
expect(page).to have_content "Ученики"
end
end
context "editor user" do
before(:each) do
user.add_role :editor
visit admin_root_path
end
it "shows link to admin" do
expect(page).to have_content "Админка"
end
it "shows link to users managment" do
expect(page).not_to have_content "Ученики"
end
end
end
Это были два первых написанных теста для mkdev.me. В другой статье я расскажу про то, в каких случаях и как писать тесты на модели, опять же, используя тесты mkdev.me в качестве примеров.