RSpec Начиная с 3.0.0
Эта страница документирует рекомендуемые тестовые хелперы с поддержкой цепочки методов.
Установка
require "servactory/test_kit/rspec/helpers"
require "servactory/test_kit/rspec/matchers"RSpec.configure do |config|
config.include Servactory::TestKit::Rspec::Helpers
config.include Servactory::TestKit::Rspec::Matchers
# ...
endХелперы
Хелпер allow_service
Выполняет мок вызова .call с указанным результатом.
Возвращает объект-билдер с поддержкой цепочки методов.
before do
allow_service(PaymentService)
.succeeds(transaction_id: "txn_123", status: :completed)
endХелпер allow_service!
Выполняет мок вызова .call! с указанным результатом.
При конфигурации неудачи выбрасывает исключение вместо возврата Result с ошибкой.
before do
allow_service!(PaymentService)
.succeeds(transaction_id: "txn_123", status: :completed)
endЦепочки методов
succeeds
Конфигурирует мок для возврата успешного результата с указанными outputs.
allow_service(PaymentService)
.succeeds(transaction_id: "txn_123", status: :completed)fails
Конфигурирует мок для возврата неудачного результата.
allow_service(PaymentService)
.fails(type: :payment_declined, message: "Card declined")С мета-информацией:
allow_service(PaymentService)
.fails(type: :validation, message: "Invalid amount", meta: { field: :amount })С пользовательским классом исключения:
allow_service(PaymentService)
.fails(
CustomException,
type: :payment_declined,
message: "Card declined"
)with
Указывает ожидаемые inputs для срабатывания мока.
allow_service(PaymentService)
.with(amount: 100, currency: "USD")
.succeeds(transaction_id: "txn_100")Метод with поддерживает матчеры аргументов (см. Матчеры аргументов).
then_succeeds
Конфигурирует последовательные возвращаемые значения для нескольких вызовов.
allow_service(RetryService)
.succeeds(status: :pending)
.then_succeeds(status: :completed)then_fails
Конфигурирует последовательный возврат с неудачей при следующем вызове.
allow_service(RetryService)
.succeeds(status: :pending)
.then_fails(type: :timeout, message: "Request timed out")Матчеры аргументов
including
Сопоставляет inputs, содержащие как минимум указанные пары ключ-значение.
allow_service(OrderService)
.with(including(quantity: 5))
.succeeds(total: 500)allow_service(OrderService)
.with(including(product_id: "PROD-001", quantity: 5))
.succeeds(total: 1000)excluding
Сопоставляет inputs, не содержащие указанные ключи.
allow_service(OrderService)
.with(excluding(secret_key: anything))
.succeeds(total: 750)any_inputs
Сопоставляет любые аргументы, переданные сервису.
allow_service(NotificationService)
.with(any_inputs)
.succeeds(sent: true)no_inputs
Сопоставляет случай, когда аргументы не переданы.
allow_service(HealthCheckService)
.with(no_inputs)
.succeeds(healthy: true)Автоматическая валидация
Хелперы автоматически валидируют inputs и outputs на соответствие определению сервиса.
Валидация inputs
При использовании with хелпер проверяет, что указанные inputs существуют в сервисе:
# Вызывает ValidationError: unknown_input не определен в ServiceClass
allow_service!(ServiceClass)
.with(unknown_input: "value")
.succeeds(result: "ok")Валидация outputs
Хелпер проверяет, что указанные outputs существуют и соответствуют ожидаемым типам:
# Вызывает ValidationError: unknown_output не определен в ServiceClass
allow_service!(ServiceClass)
.succeeds(unknown_output: "value")# Вызывает ValidationError: order_number ожидает Integer, получен String
allow_service!(ServiceClass)
.succeeds(order_number: "not_an_integer")Пример
RSpec.describe UsersService::Create, type: :service do
describe ".call!" do
subject(:perform) { described_class.call!(**attributes) }
let(:attributes) do
{
email:,
first_name:,
last_name:
}
end
let(:email) { "john@example.com" }
let(:first_name) { "John" }
let(:last_name) { "Kennedy" }
describe "validations" do
describe "inputs" do
it do
expect { perform }.to(
have_input(:email)
.type(String)
.required
)
end
it do
expect { perform }.to(
have_input(:first_name)
.type(String)
.required
)
end
it do
expect { perform }.to(
have_input(:last_name)
.type(String)
.optional
)
end
end
describe "internals" do
it do
expect { perform }.to(
have_internal(:email_verification)
.type(Servactory::Result)
)
end
end
describe "outputs" do
it do
expect(perform).to(
have_output(:user)
.instance_of(User)
)
end
end
end
describe "and the data required for work is also valid" do
before do
allow_service!(EmailVerificationService)
.with(email: "john@example.com")
.succeeds(valid: true, normalized: "john@example.com")
end
it do
expect(perform).to(
be_success_service
.with_output(:user, be_a(User))
)
end
end
describe "but the data required for work is invalid" do
describe "because email verification fails" do
before do
allow_service!(EmailVerificationService)
.fails(type: :invalid_email, message: "Email is not valid")
end
it "returns expected error", :aggregate_failures do
expect { perform }.to(
raise_error do |exception|
expect(exception).to be_a(ApplicationService::Exceptions::Failure)
expect(exception.type).to eq(:invalid_email)
expect(exception.message).to eq("Email is not valid")
expect(exception.meta).to be_nil
end
)
end
end
end
end
endclass UsersService::Create < ApplicationService::Base
input :email, type: String
input :first_name, type: String
input :last_name, type: String, required: false
internal :email_verification, type: Servactory::Result
output :user, type: User
make :verify_email
make :create_user
private
def verify_email
internals.email_verification = EmailVerificationService.call!(
email: inputs.email
)
end
def create_user
outputs.user = User.create!(
email: internals.email_verification.normalized,
first_name: inputs.first_name,
last_name: inputs.last_name
)
end
endМатчеры
Матчер have_input have_service_input
type
Проверяет тип инпута. Предназначен для одного значения.
it do
expect { perform }.to(
have_input(:id)
.type(Integer)
)
endtypes
Проверяет типы инпута. Предназначен для нескольких значений.
it do
expect { perform }.to(
have_input(:id)
.types(Integer, String)
)
endrequired
Проверяет обязательность инпута.
it do
expect { perform }.to(
have_input(:id)
.type(Integer)
.required
)
endoptional
Проверяет опциональность инпута.
it do
expect { perform }.to(
have_input(:middle_name)
.type(String)
.optional
)
enddefault
Проверяет дефолтное значение инпута.
it do
expect { perform }.to(
have_input(:middle_name)
.type(String)
.optional
.default("<unknown>")
)
endconsists_of
Проверяет вложенные типы коллекции инпута. Можно указать несколько значений.
it do
expect { perform }.to(
have_input(:ids)
.type(Array)
.required
.consists_of(String)
)
endit do
expect { perform }.to(
have_input(:ids)
.type(Array)
.required
.consists_of(String)
.message("Input `ids` must be a collection of `String`")
)
endinclusion
Проверяет значения опции inclusion инпута.
it do
expect { perform }.to(
have_input(:event_name)
.type(String)
.required
.inclusion(%w[created rejected approved])
)
endit do
expect { perform }.to(
have_input(:event_name)
.type(String)
.required
.inclusion(%w[created rejected approved])
.message(be_a(Proc))
)
endtarget
Проверяет значения опции target инпута.
it do
expect { perform }.to(
have_input(:service_class)
.type(Class)
.target([MyFirstService, MySecondService])
)
endit do
expect { perform }.to(
have_input(:service_class)
.type(Class)
.target([MyFirstService, MySecondService])
.message("Must be a valid service class")
)
endschema input (^2.12.0) internal (^2.12.0) output (^2.12.0)
Проверяет значения опции schema инпута.
it do
expect { perform }.to(
have_input(:payload)
.type(Hash)
.required
.schema(
{
request_id: { type: String, required: true },
user: {
# ...
}
}
)
)
endit do
expect { perform }.to(
have_input(:payload)
.type(Hash)
.required
.schema(
{
request_id: { type: String, required: true },
user: {
# ...
}
}
)
.message("Problem with the value in the schema")
)
endmessage input (^2.12.0) internal (^2.12.0) output (^2.12.0)
Проверяет message из последнего чейна. Работает только с чейнами consists_of, inclusion и schema.
it do
expect { perform }.to(
have_input(:ids)
.type(Array)
.required
.consists_of(String)
.message("Input `ids` must be a collection of `String`")
)
endmust
Проверяет наличие ожидаемого ключа в must инпута. Можно указать несколько значений.
it do
expect { perform }.to(
have_input(:invoice_numbers)
.type(Array)
.consists_of(String)
.required
.must(:be_6_characters)
)
endМатчер have_internal have_service_internal
type
Проверяет тип внутреннего атрибута. Предназначен для одного значения.
it do
expect { perform }.to(
have_internal(:id)
.type(Integer)
)
endtypes
Проверяет типы внутреннего атрибута. Предназначен для нескольких значений.
it do
expect { perform }.to(
have_internal(:id)
.types(Integer, String)
)
endconsists_of
Проверяет вложенные типы коллекции внутреннего атрибута. Можно указать несколько значений.
it do
expect { perform }.to(
have_internal(:ids)
.type(Array)
.consists_of(String)
)
endit do
expect { perform }.to(
have_internal(:ids)
.type(Array)
.consists_of(String)
.message("Internal `ids` must be a collection of `String`")
)
endinclusion
Проверяет значения опции inclusion внутреннего атрибута.
it do
expect { perform }.to(
have_internal(:event_name)
.type(String)
.inclusion(%w[created rejected approved])
)
endit do
expect { perform }.to(
have_internal(:event_name)
.type(String)
.inclusion(%w[created rejected approved])
.message(be_a(Proc))
)
endtarget
Проверяет значения опции target внутреннего атрибута.
it do
expect { perform }.to(
have_internal(:service_class)
.type(Class)
.target([MyFirstService, MySecondService])
)
endit do
expect { perform }.to(
have_internal(:service_class)
.type(Class)
.target([MyFirstService, MySecondService])
.message("Must be a valid service class")
)
endschema input (^2.12.0) internal (^2.12.0) output (^2.12.0)
Проверяет значения опции schema внутреннего атрибута.
it do
expect { perform }.to(
have_internal(:payload)
.type(Hash)
.schema(
{
request_id: { type: String, required: true },
user: {
# ...
}
}
)
)
endit do
expect { perform }.to(
have_internal(:payload)
.type(Hash)
.schema(
{
request_id: { type: String, required: true },
user: {
# ...
}
}
)
.message("Problem with the value in the schema")
)
endmessage input (^2.12.0) internal (^2.12.0) output (^2.12.0)
Проверяет message из последнего чейна. Работает только с чейнами consists_of, inclusion и schema.
it do
expect { perform }.to(
have_internal(:ids)
.type(Array)
.consists_of(String)
.message("Internal `ids` must be a collection of `String`")
)
endmust
Проверяет наличие ожидаемого ключа в must внутреннего атрибута. Можно указать несколько значений.
it do
expect { perform }.to(
have_internal(:invoice_numbers)
.type(Array)
.consists_of(String)
.must(:be_6_characters)
)
endМатчер have_output have_service_output
instance_of
Проверяет тип выходящего атрибута.
it do
expect(perform).to(
have_output(:event)
.instance_of(Event)
)
endcontains
INFO
В релизе 2.9.0 чейн with был переименован в contains.
Проверяет значение выходящего атрибута.
it do
expect(perform).to(
have_output(:full_name)
.contains("John Fitzgerald Kennedy")
)
endnested
Указывает на вложенное значение выходящего атрибута.
it do
expect(perform).to(
have_output(:event)
.nested(:id)
.contains("14fe213e-1b0a-4a68-bca9-ce082db0f2c6")
)
endМатчер be_success_service
it { expect(perform).to be_success_service }with_output
it do
expect(perform).to(
be_success_service
.with_output(:id, "...")
)
endwith_outputs
it do
expect(perform).to(
be_success_service
.with_outputs(
id: "...",
full_name: "...",
# ...
)
)
endМатчер be_failure_service
it { expect(perform).to be_failure_service }it "returns expected failure" do
expect(perform).to(
be_failure_service
.with(ApplicationService::Exceptions::Failure)
.type(:base)
.message("Some error")
.meta(nil)
)
endwith
it "returns expected failure" do
expect(perform).to(
be_failure_service
.with(ApplicationService::Exceptions::Failure)
)
endtype
it "returns expected failure" do
expect(perform).to(
be_failure_service
.type(:base)
)
endmessage
it "returns expected failure" do
expect(perform).to(
be_failure_service
.message("Some error")
)
endmeta
it "returns expected failure" do
expect(perform).to(
be_failure_service
.meta(nil)
)
end