Написание первых тестов

Illustration of a young person sitting on the floor against a wall, engrossed in reading a tablet, wearing casual clothing with a distinct orange scarf and sneakers. Illustration of a young person sitting on the floor against a wall, engrossed in reading a tablet, wearing casual clothing with a distinct orange scarf and sneakers.

Я в целом противник методики 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 в качестве примеров.