discourse/spec/support/service_matchers.rb
Loïc Guitaut 719457e430 DEV: Add a try step to services
This patch adds a new step to services named `try`.

It’s useful to rescue exceptions that some steps could raise. That way,
if an exception is caught, the service will stop its execution and can
be inspected like with any other steps.

Just wrap the steps that can raise with a `try` block:
```ruby
try do
  step :step_that_can_raise
  step :another_step_that_can_raise
end
```
By default, `try` will catch any exception inheriting from
`StandardError`, but we can specify what exceptions to catch:
```ruby
try(ArgumentError, RuntimeError) do
  step :will_raise
end
```

An outcome matcher has been added: `on_exceptions`. By default it will
be executed for any exception caught by the `try` step.
Here also, we can specify what exceptions to catch:
```ruby
on_exceptions(ArgumentError, RuntimeError) do |exception|
  …
end
```

Finally, an RSpec matcher has been added:
```ruby
  it { is_expected.to fail_with_exception }
  # or
  it { is_expected.to fail_with_exception(ArgumentError) }
```
2024-11-19 12:01:07 +01:00

223 lines
4.5 KiB
Ruby

# frozen_string_literal: true
module ServiceMatchers
class RunServiceSuccessfully
attr_reader :result
def matches?(result)
@result = result
result.success?
end
def failure_message
message = "Expected the service to succeed but it failed."
error_message_with_inspection(message)
end
def failure_message_when_negated
message = "Expected the service to fail but it succeeded."
error_message_with_inspection(message)
end
def description
"run the service successfully"
end
private
def error_message_with_inspection(message)
inspector = Service::StepsInspector.new(result)
"#{message}\n\n#{inspector.inspect}\n\n#{inspector.error}"
end
end
class FailStep
attr_reader :name, :result
def initialize(name)
@name = name
end
def matches?(result)
@result = result
step_exists? && step_failed? && service_failed?
end
def failure_message
set_unexpected_result
message =
if !step_exists?
step_not_existing_message
elsif !step_failed?
step_failed_message
else
"expected the service to fail but it succeeded."
end
error_message_with_inspection(message)
end
def failure_message_when_negated
set_unexpected_result
error_message_with_inspection(negated_message)
end
def description
"fail a #{type} named '#{name}'"
end
private
def step_exists?
result[step].present?
end
def step_failed?
result[step].failure?
end
def service_failed?
result.failure?
end
def type
self.class.name.split("::").last.sub("Fail", "").downcase
end
def step
"result.#{type}.#{name}"
end
def error_message_with_inspection(message)
inspector = Service::StepsInspector.new(result)
"#{message}\n\n#{inspector.inspect}\n\n#{inspector.error}"
end
def set_unexpected_result
return unless result[step]
result[step]["spec.unexpected_result"] = true
end
def step_not_existing_message
"Expected #{type} '#{name}' (key: '#{step}') was not found in the result object."
end
def step_failed_message
"Expected #{type} '#{name}' (key: '#{step}') to fail but it succeeded."
end
def negated_message
"Expected #{type} '#{name}' (key: '#{step}') to succeed but it failed."
end
end
class FailContract < FailStep
end
class FailPolicy < FailStep
end
class FailToFindModel < FailStep
def type
"model"
end
def description
"fail to find a model named '#{name}'"
end
def step_failed?
super && result[step].not_found
end
end
class FailWithInvalidModel < FailStep
def type
"model"
end
def description
"fail to have a valid model named '#{name}'"
end
def step_failed?
super && result[step].invalid
end
end
class FailWithException < FailStep
attr_reader :exception
def initialize(exception)
@exception = exception
@name = "default"
end
def type
"try"
end
def description
"fail with an exception (#{exception})"
end
def step_failed?
super && result[step].exception.is_a?(exception)
end
def step_not_existing_message
"Expected try block (key: '#{step}') was not found in the result object."
end
def step_failed_message
message =
"Expected try block (key: '#{step}') to fail with an exception of type '#{exception}'"
message +=
if result[step].exception.blank?
" but it succeeded."
else
" but it failed with an exception of type '#{result[step].exception.class}'"
end
end
def negated_message
"Expected try block (key: '#{step}') to succeed but it failed."
end
end
def fail_a_policy(name)
FailPolicy.new(name)
end
def fail_a_contract(name = "default")
FailContract.new(name)
end
def fail_to_find_a_model(name = "model")
FailToFindModel.new(name)
end
def fail_with_an_invalid_model(name = "model")
FailWithInvalidModel.new(name)
end
def fail_with_exception(exception = StandardError)
FailWithException.new(exception)
end
def fail_a_step(name = "model")
FailStep.new(name)
end
def run_successfully
RunServiceSuccessfully.new
end
def inspect_steps(result)
inspector = Service::StepsInspector.new(result)
puts "Steps:"
puts inspector.inspect
puts "\nFirst error:"
puts inspector.error
end
end