Статьи

Напишите модульный RSpec

Кубики блок. Сборка концепции. На белом.

RSpec — самая популярная среда тестирования в средах Ruby / Rails. Два из его самых больших преимуществ — это возможность писать чистые, лаконичные и модульные тесты и комбинировать их с множеством различных фреймворков (backend, frontend, API … и т. , Цель этой статьи — продемонстрировать, как создавать тестовые сценарии, которые легко понять и поддерживать, а также уменьшить количество кошмаров по рефакторингу

Ваш воображаемый проект

Ради этой статьи мы начнем со следующей предпосылки:
1. Ваш сайт («Погода хорошая») — это приложение, которое предоставляет информацию о прогнозах погоды и текущей погоде в некоторых регионах на основе данных, вводимых пользователем.
2. Он предоставляет REST API (формат данных JSON).
3. Чтобы получить доступ к API, пользователям необходимо зарегистрироваться и получить токен авторизации, используемый для последующих запросов.

Методы, которые предлагаются через API:

ПОЛУЧИТЬ/
— Текущая погода в городе (токен, город)
— Погода в районе города (токен, город, область)
— Пользовательский ввод по погоде в городе (токен, город)

ПОЧТА/
— Создать пользовательскую запись для погоды в городе (токен, город, комментарий)
— Создать пользователя (токен, имя, фамилия, адрес электронной почты, пароль)
— Логин (электронная почта, пароль)
— Выход (токен)

УДАЛЯТЬ/
— Удалить пользователя (токен, пользователь)
— Удалить запись пользователя о погоде в городе (токен, пользователь, город)

Проблема

Проект только начался. Вы слышали об этом инструменте RSpec и хотите использовать его в качестве основного фреймворка для написания тестов. Вы никогда не работали с ним и не знакомы с Ruby. Вы медленно продвигаетесь в написании своих тестов и изучаете больше RSpec / Ruby каждый день. В настоящее время у вас есть методика написания тестов, и она работает. Хорошо! По мере развития проекта, когда у приложения появляется все больше и больше функций, вы можете писать свои тестовые сценарии. Это становится для вас второй натурой, но вы начинаете понимать, что вы пишете один и тот же код в нескольких местах. У вас нет времени на рефакторинг существующих тестовых сценариев, и вы боитесь потерять время и сломать уже работающие вещи.

Часто говорят: «Если он не сломан, не чините его». Но в этом случае это не относится. Существует множество тестовых сценариев, и, в конце концов, мы понимаем, что они не подлежат обслуживанию. Технический отдел накапливается, потому что мы не писали спецификации умным способом.

Решение

Нельзя избежать рефакторинга тестов просто потому, что функции меняются ежедневно. Вопрос в том, будете ли вы делать это легким или трудным путем? Чтобы сделать это простым способом, вам нужно писать тесты модульно, чтобы избежать дублирования кода. Учитывая API, представленный в приложении, вот несколько возможных вариантов использования для покрытия:

  1. Создайте нового пользователя и убедитесь, что вы можете войти и выйти с этим пользователем.
  2. NEG — попробуйте войти с несуществующим пользователем.
  3. С существующим пользователем, получить прогноз погоды для конкретного города (город существует).
  4. NEG — с существующим пользователем, получить прогноз погоды для конкретного города (город не существует).
  5. С существующим пользователем, получите прогноз погоды для определенного района города.
  6. NEG — С существующим пользователем, получите прогноз погоды для конкретной области города (область находится в минусе).
  7. С существующим пользователем, получить вход от пользователей для определенного города.
  8. С существующим пользователем, добавьте новый вход для определенного города и убедитесь, что этот вход находится в списке входов.
  9. NEG — Удалить существующего пользователя и попытаться войти с этим пользователем.
  10. Удалите существующего пользователя, создайте нового пользователя, войдите в систему с этим пользователем и убедитесь, что комментарии от предыдущего пользователя для определенного города были удалены.

Это лишь некоторые из случаев, и, конечно, есть еще случаи, которые необходимо охватить. Придерживаясь описанных здесь случаев, уже есть некоторые шаблоны многократного использования. Нам понадобится общий класс для предоставления методов для отправки запросов REST. Кроме того, некоторые действия будут повторяться, например, создание нового пользователя, вход в систему, выход из системы, получение прогноза для города, получение пользовательских данных о погоде в городе и т. Д.

Настройка RSpec

Перво-наперво, пришло время настроить RSpec. Используйте стандартную rspec --init command которая устанавливает начальную структуру для тестов RSpec:

 <root_project_directory> - spec spec_helper.rb .rspec 

Поскольку тесты будут модульными и будут использовать некоторые начальные данные конфигурации, хорошим способом для начала является создание двух дополнительных каталогов: config и lib в корневом каталоге проекта. Конфиг для каждой среды будет жить в config.yml . В библиотеке будет многократно используемый код для наших тестовых скриптов.

Настройка config.yml

Config.yml выглядит так:

 environment: server: http://test.weatherisgood.com port: 7000 

Многоразовые детали

Как уже говорилось ранее, нам нужен общий класс для отправки запросов REST. Это будет класс Sender внутри файла sender.rb :

 require 'rubygems' require 'rest_client' require 'json' require 'yaml' class Sender attr_reader :server_url def initialize config_yml = YAML::load(File.open("./config/config.yaml")) @server_url = config_yml['environment']['server'].to_s + ":" + config_yml['environment']['port'].to_s end def send_request(endpoint, method, args={}, headers={}) unless ['GET','POST','DELETE'].include? method raise "Incorrect REST method has been specified" end if method == 'GET' begin response = RestClient.get(@server_url + endpoint, headers) return JSON.parse(response) rescue => e return JSON.parse(e.response) end elsif method == 'POST' begin response = RestClient.post(@server_url + endpoint, args, headers) return JSON.parse(response) rescue => e return JSON.parse(e.response) end elsif method == 'DELETE' begin response = RestClient.delete(@server_url + endpoint, headers) return JSON.parse(response) rescue => e return JSON.parse(e.response) end end end end 

Код довольно понятен. В конструкторе создайте server_url из данных конфигурации. Метод send_request указывает конечную точку, метод REST, возможные аргументы и заголовки (для аутентификации). Теперь у нас есть централизованный класс, используемый для отправки и анализа запросов REST.

Следующее, что нам нужно, это реализация общих действий (вход / выход из системы, создание нового пользователя, получение прогноза погоды и т. Д.). Для простоты эти методы *.rb внутри одного файла *.rb , но мы можем использовать несколько файлы для разделения кодовой базы по функциональности. Давайте назовем это weatherisgood_api.rb (как это ни парадоксально, наименование является одной из самых сложных вещей в разработке программного обеспечения, и, вероятно, для этого есть более подходящие названия :))

 require 'rubygems' require 'json' require 'sender' class WeatherIsGoodApi attr_reader :sender, :authorization_token def initialize @sender = Sender.new end def login(email, password) response = @sender.send_request('/auth/login', 'POST', {:email=>email, :password=>password}) @authorization_token = response['data']['token'] unless response['data']['token'].nil? return response['data']['token'] end def logout response = @sender.send_request('/auth/logout','POST', {}, {:token=>@authorization_token}) end def create_user(firstname, lastname, email, password) response = @sender.send_request('/user/create', 'POST', {:firstname=>firstname, :lastname=>lastname, :email=>email, :password=>password}) end def get_weather_today_for_city(city) response = @sender.send_request("/weather/#{city}/today", 'GET', {}, {:token=>@authorization_token}) end def get_users_input_for_weather(city) response = @sender.send_request("/weather/#{city}/today/user", 'GET', {}, {:token=>@authorization_token}) end end 

Как вы можете видеть, существуют методы для каждого из действий, которые будут выполняться с API. Вход в систему получает токен авторизации, который будет использоваться в последующих запросах.

RSpec Многоразовая магия

С базовой реализацией для вызовов REST и общих действий мы можем использовать концепцию совместного использования RSpec для дальнейшего разделения действий, выполняемых на этапах тестирования. Это важно, потому что определенные шаги теста будут обеспечивать различный ввод для этих действий и ожидать различного вывода (хороший пример — положительные и отрицательные тесты для входа в систему). Для этого используйте RSpec- shared_context который определяет действия, которые принимают входные параметры и выполняют проверки внутри it блокируется так же, как и обычные блоки describe :

 require 'rubygems' require 'json' shared_context "Login" do |weatherisgood, email, password| it "succeeds for user: #{email}" do user_login_success = false output = weatherisgood.login(email, password) user_login_success = true unless output.nil? expect(user_login_success).to eq(true) end end shared_context "NEG - Login" do |weatherisgood, email, password| it "does not succeed for invalid user: #{email}" do user_login_failure = false output = weatherisgood.login(email, password) user_login_failure = true if output.nil? expect(user_login_failure).to eq(true) end end shared_context "Create user" do |weatherisgood, firstname, lastname, email, password| it "creates user account for user: #{firstname} #{lastname}" do user_account_created = false output = weatherisgood.create_user(firstname, lastname, email, password) user_account_created = true unless output['data']['success'].nil? expect(user_account_created).to eq(true) end end shared_context "NEG - Create user (user already exists)" do |weatherisgood, firstname, lastname, email, password| it "does not create user account for user: #{firstname} #{lastname} since it already exist" do error_message = "" output = weatherisgood.create_user(firstname, lastname, email, password) if output['data']['success'].nil? error_message = output['data']['error'] end expect(error_message).to eq('User already exists!') end end shared_context "Get weather for city" do |weatherisgood, city| it "gets weather forecast for city #{city}" do weather_forecast_retrieved = false output = weatherisgood.get_weather_today_for_city(city) # Do some checks here for the content and if everything is ok set weather_forecast_retrieved to true expect(weather_forecast_retrieved).to eq(true) end end shared_context "NEG - Get weather for city" do |weatherisgood, city| it "does not get weather forecast for city #{city} that does not exist" do error_message = "" output = weatherisgood.get_weather_today_for_city(city) if output['data']['success'].nil? error_message = output['data']['error'] end expect(error_message).to eq('City does not exist!') end end shared_context "Get user input on weather for city" do |weatherisgood, city| it "gets users input on weather forecast for city #{city}" do weather_forecast_retrieved = false output = weatherisgood.get_users_input_for_weather(city) # Do some checks here for the content and if everything is ok set weather_forecast_retrieved to true expect(weather_forecast_retrieved).to eq(true) end end 

Обратите внимание, что я использую префикс «NEG» в некоторых общих контекстах, чтобы указать отрицательную проверку (например, вход в систему с неверными учетными данными). shared_context s вызываются из тестовых случаев. Создайте тестовый пример 1_spec.rb и поместите его в каталог spec . Я обычно помещаю один тестовый пример в один *.rb файл для понимания и обслуживания. Вот пример:

 require 'rubygems' require './lib/weatherisgood_api' require './lib/shared/weatherisgood_spec' user = "bakir" password = "mypassword" city = "Sarajevo" city_doesnot_exist = "MyCity" weatherisgood = WeatherIsGoodApi.new describe "Test Case 1: Login with user and get weather check for city that exists and doesn't exist" do context "Login with user #{user}" do include_context "Login", weatherisgood, user, password end context "Get weather for city #{city} that exists" do include_context "Get weather for city", weatherisgood, city end context "Get weather for city #{city_doesnot_exist} that does not exist" do include_context "NEG - Get weather for city", weatherisgood, city_doesnot_exist end end 

Несколько вещей, на которые стоит обратить внимание:
1. Я использую context для разделения шагов теста.
2. Контрольный пример описывается с использованием describe . Тестовые шаги описаны с использованием context . Это личное предпочтение различать тестовый пример и шаг теста.
3. Внутри каждого context у нас есть include_context и имя shared_context для вызова, передавая параметры по мере необходимости.
4. При выполнении этого теста выходные данные (с использованием документации --format в файле .rspec) выглядят следующим образом:

 Test Case 1: Login with user and get weather check for city that exists and doesn't exist Login with user bakir@myemail.com succeeds for user: bakir@myemail.com Get weather for city that exists gets weather forecast for city Sarajevo Get weather for city that does not exist does not get weather forecast for city MyCity that does not exist Finished in 0.00124 seconds 3 examples, 0 failures 

Как видите, вывод очень четкий и включает в себя:
— Описание этапа тестирования («Войти с пользователем [email protected]»)
— Ожидаемый результат шага теста («успешно для пользователя: [email protected]»)
Желательно, чтобы ваши тестовые этапы были описаны как можно более четко, чтобы результат был читабельным и полезным.

Вывод

В этом уроке я попытался объяснить, почему нам нужно многократное использование в нашем тестовом коде RSpec. Каждое изменение, необходимое для REST API, может быть сделано в одном месте (класс WeatherIsGoodApi ). Кроме того, ожидаемые результаты для конкретной тестируемой функции корректируются в одном месте ( shared_context в shared_context ). Это один из подходов для повторного использования, но вы наверняка могли бы shared_context другие. Суть в том, что прежде чем писать тесты, подумайте, потому что это может легко укусить вас в долгосрочной перспективе. Если это произойдет, процесс возвращения и изменения дизайна ваших тестов может быть хуже, чем написание их с нуля.