Немного магии для RSpec-тестов
Что это?
🆎 An English version of this text is also available: README.md.
RSpecMagic — набор расширений для написания компактных и выразительных тестов.
Установка
💡 Предполагается, что RSpec в нашем проекте мы уже настроили.
Добавляем в Gemfile
:
gem "rspec_magic"
#gem "rspec_magic", git: "https://github.com/dadooda/rspec_magic"
Добавляем в автозагрузку RSpec (обычно это spec/spec_helper.rb
):
require "rspec_magic/stable"
require "rspec_magic/unstable"
RSpecMagic::Config.spec_path = File.(".", __dir__)
Настройка spec_path
нужна для некоторых фич, например, include_dir_context.
Вычисленный путь должен указывать на spec/
в директории проекта.
См. Подробно.
Фичи
alias_method
Matcher, сверяющий, что метод является alias'ом другого метода.
describe User do
it { is_expected.to alias_method(:admin?, :is_admin) }
end
context_when
Создаём стереотипный контекст, задающий внутри себя одну или несколько let
-переменных.
Блоки ниже взаимозаменяемы.
context_when name: "Joe", age: 25 do
it do
expect([name, age]).to eq ["Joe", 25]
end
end
context "when { name: \"Joe\", age: 25 }" do
let(:name) { "Joe" }
let(:age) { 25 }
it do
expect([name, age]).to eq ["Joe", 25]
end
end
См. Подробно.
described_sym
described_sym
и me
— представление имени described_class
в виде Symbol
.
Помогает не «долдонить» мнемоническим названием тестируемого класса, например,
при создании записей с помощью factory.
describe UserProfile do
it { expect(described_sym).to eq :user_profile }
it { expect(me).to eq :user_profile }
end
С factory:
describe UserProfile do
let(:uprof1) { create described_sym }
let(:uprof2) { create me }
…
end
include_dir_context
♒︎ Эта фича добавлена недавно и может измениться.
Организуем общие контексты (shared_context) в иерархии. Автоматически включаем нужные общие контексты в наш тест.
Шаги:
Убеждаемся, что в настройках правильно прописана
RSpecMagic::Config.spec_path
. Она должна указывать наspec/
.По файловому дереву тестов создаём файлы общих контекстов с одинаковым именем, например,
_context.rb
. Содержимое_context.rb
всегда имеет вид:shared_context __dir__ do … end
Добавляем в условный
spec_helper.rb
:# Загружаем иерархию shared contexts. Dir[File.("**/_context.rb", __dir__)].each { |fn| require fn }
В spec-файле добавляем вызов
include_dir_context
в тело главногоdescribe
:describe … do include_dir_context __dir__ … end
Например, наш spec-файл это spec/app/controllers/api/player_controller_spec.rb
.
В главный describe
будут последовательно загружены, если они есть, контексты из файлов:
spec/_context.rb
spec/app/_context.rb
spec/app/controllers/_context.rb
spec/app/controllers/api/_context.rb
См. Подробно.
use_letset
Создаём на уровне describe
метод для задания let
-переменных, автоматически составляющих коллекцию типа Hash
.
describe do
# Метод -- `let_a`. Коллекция -- `attrs`.
use_letset :let_a, :attrs
# Декларируем переменные, которые составляют коллекцию `attrs`.
let_a(:age)
let_a(:name)
subject { attrs }
# Ни одна переменная пока не задана, поэтому коллекция будет пустой.
it { is_expected.to eq({}) }
# Задаём `name` и видим его в коллекции.
context_when name: "Joe" do
it { is_expected.to eq(name: "Joe") }
# Задаём `age` и видим обе переменные в коллекции.
context_when age: 25 do
it { is_expected.to eq(name: "Joe", age: 25) }
end
end
end
Если передан блок, let_a
работает как обычный let
. Такой вариант изредка тоже бывает полезен:
describe do
use_letset :let_a, :attrs
let_a(:age) { 25 }
let_a(:name) { "Joe" }
it { expect(attrs).to eq(name: "Joe", age: 25) }
end
use_method_discovery
Создаём автоматическую let
-переменную, содержащую имя метода или action,
вычисленное из текста вышестоящего describe
.
describe do
use_method_discovery :m
subject { m }
describe "#first_name" do
it { is_expected.to eq :first_name }
end
describe ".some_stuff" do
it { is_expected.to eq :some_stuff }
end
describe "GET some_action" do
describe "intermediate context" do
it { is_expected.to eq :some_action } # (1)
end
end
end
m
находит ближайший подходящий контекст, формат текста которого допускает выемку имени метода.
См. (1) — m
пропустила вольно отформатированный "intermediate context"
и сработала
на "GET some_action"
.
Подробно
Про установку
stable
иunstable
— наборы фич. В наборunstable
входят фичи, добавленные недавно. Они могут измениться в следующих версиях.Можно включить только конкретные фичи. Например:
require "rspec_magic/stable/use_method_discovery"
Про context_when
Контекст можно исключить из обработки, приписав к началу
x
:xcontext_when … do … end
Можно определить свой метод форматирования строки для отчёта:
describe "…" do def self._context_when_formatter(h) "when #{h.to_json}" end context_when … do … end end
context_when
эффективно работает в паре с use_letset, обычно для задания атрибутов тестируемого объекта.Значения
let
-переменных вычисляются на уровнеdescribe
. Если нужны значения, вычисляемые на уровнеit
, следует использовать обычныйlet(…) { … }
внутри контекста.
Про include_dir_context
Есть в RSpec классная штука — общие контексты, они же shared_context.
Замысел простой: где-то (в условном spec_helper.rb
) мы создаём нечто общее через shared_context "то сё"
,
а потом через include_context "то сё"
импортируем материал в нужный там тест.
Наполнять shared_context
можно чем угодно — общими тестами, let
-переменными, но главное —
методами уровня describe
(def self.doit
) и методами уровня it
(def doit
).
Чем не библиотека?
Казалось бы — вот оно счастьюшко, сочиняй свои расширения, разноси по общим контекстам, где надо импортируй и радуйся. Но есть неприятная особенность: штатные средства организации контекстов очень примитивны и опираются на глобальные уникальные имена.
RSpec не позволяет организовывать общие контексты в иерархии, чтобы автоматически импортировать контексты-«библиотеки» в группы spec-файлов, как то: всем тестам моделей — одно, всем тестам контроллеров — другое, а всем им вместе — третье.
Чтобы поддерживать мало-мальский порядок, приходится натужно придумывать общим контекстам уникальные имена, и в каждом spec-файле перечислять импортируемое унылым повторяющимся списком:
describe … do
include_context "basic"
include_context "controllers"
include_context "api_controllers"
…
end
И это ещё продвинутый уровень. Чаще всего даже так не делают, а просто сваливают все расширения в кучу и включают сразу всё, просто потому, что «некогда разбираться».
Что даёт include_dir_context
?
- Возможность организовывать общие контексты в иерархии.
- Возможность автоматически включать наборы того, что нужно, туда, куда нужно.
Как это делать, описано в основной главе.
Copyright
Продукт распространяется свободно на условиях лицензии MIT.
— © 2017-2024 Алексей Фортуна