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) }
```
This commit is contained in:
Loïc Guitaut
2024-11-08 17:24:40 +01:00
committed by Loïc Guitaut
parent 682e8df007
commit 719457e430
6 changed files with 295 additions and 74 deletions

View File

@ -20,6 +20,7 @@ RSpec.describe Service::StepsInspector do
step :in_transaction_step_1
step :in_transaction_step_2
end
try { step :might_raise }
step :final_step
end
@ -30,9 +31,14 @@ RSpec.describe Service::StepsInspector do
before do
class DummyService
%i[fetch_model policy in_transaction_step_1 in_transaction_step_2 final_step].each do |name|
define_method(name) { true }
end
%i[
fetch_model
policy
in_transaction_step_1
in_transaction_step_2
might_raise
final_step
].each { |name| define_method(name) { true } }
end
end
@ -42,14 +48,16 @@ RSpec.describe Service::StepsInspector do
context "when service runs without error" do
it "outputs all the steps of the service" do
expect(output).to eq <<~OUTPUT.chomp
[1/8] [options] 'default'
[2/8] [model] 'model'
[3/8] [policy] 'policy'
[4/8] [params] 'default'
[5/8] [transaction]
[6/8] [step] 'in_transaction_step_1'
[7/8] [step] 'in_transaction_step_2'
[8/8] [step] 'final_step'
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy'
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise'
[10/10] [step] 'final_step'
OUTPUT
end
end
@ -65,14 +73,16 @@ RSpec.describe Service::StepsInspector do
it "shows the failing step" do
expect(output).to eq <<~OUTPUT.chomp
[1/8] [options] 'default'
[2/8] [model] 'model'
[3/8] [policy] 'policy'
[4/8] [params] 'default'
[5/8] [transaction]
[6/8] [step] 'in_transaction_step_1'
[7/8] [step] 'in_transaction_step_2'
[8/8] [step] 'final_step'
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy'
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise'
[10/10] [step] 'final_step'
OUTPUT
end
end
@ -88,14 +98,16 @@ RSpec.describe Service::StepsInspector do
it "shows the failing step" do
expect(output).to eq <<~OUTPUT.chomp
[1/8] [options] 'default'
[2/8] [model] 'model'
[3/8] [policy] 'policy'
[4/8] [params] 'default'
[5/8] [transaction]
[6/8] [step] 'in_transaction_step_1'
[7/8] [step] 'in_transaction_step_2'
[8/8] [step] 'final_step'
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy'
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise'
[10/10] [step] 'final_step'
OUTPUT
end
end
@ -105,14 +117,16 @@ RSpec.describe Service::StepsInspector do
it "shows the failing step" do
expect(output).to eq <<~OUTPUT.chomp
[1/8] [options] 'default'
[2/8] [model] 'model'
[3/8] [policy] 'policy'
[4/8] [params] 'default'
[5/8] [transaction]
[6/8] [step] 'in_transaction_step_1'
[7/8] [step] 'in_transaction_step_2'
[8/8] [step] 'final_step'
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy'
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise'
[10/10] [step] 'final_step'
OUTPUT
end
end
@ -128,14 +142,41 @@ RSpec.describe Service::StepsInspector do
it "shows the failing step" do
expect(output).to eq <<~OUTPUT.chomp
[1/8] [options] 'default'
[2/8] [model] 'model'
[3/8] [policy] 'policy'
[4/8] [params] 'default'
[5/8] [transaction]
[6/8] [step] 'in_transaction_step_1'
[7/8] [step] 'in_transaction_step_2'
[8/8] [step] 'final_step'
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy'
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise'
[10/10] [step] 'final_step'
OUTPUT
end
end
context "when a step raises an exception inside the 'try' block" do
before do
class DummyService
def might_raise
raise "BOOM"
end
end
end
it "shows the failing step" do
expect(output).to eq <<~OUTPUT.chomp
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy'
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise' 💥
[10/10] [step] 'final_step'
OUTPUT
end
end
@ -146,14 +187,16 @@ RSpec.describe Service::StepsInspector do
it "adapts its output accordingly" do
expect(output).to eq <<~OUTPUT.chomp
[1/8] [options] 'default'
[2/8] [model] 'model'
[3/8] [policy] 'policy' <= expected to return false but got true instead
[4/8] [params] 'default'
[5/8] [transaction]
[6/8] [step] 'in_transaction_step_1'
[7/8] [step] 'in_transaction_step_2'
[8/8] [step] 'final_step'
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy' <= expected to return false but got true instead
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise'
[10/10] [step] 'final_step'
OUTPUT
end
end
@ -170,14 +213,16 @@ RSpec.describe Service::StepsInspector do
it "adapts its output accordingly" do
expect(output).to eq <<~OUTPUT.chomp
[1/8] [options] 'default'
[2/8] [model] 'model'
[3/8] [policy] 'policy' <= expected to return true but got false instead
[4/8] [params] 'default'
[5/8] [transaction]
[6/8] [step] 'in_transaction_step_1'
[7/8] [step] 'in_transaction_step_2'
[8/8] [step] 'final_step'
[ 1/10] [options] 'default'
[ 2/10] [model] 'model'
[ 3/10] [policy] 'policy' <= expected to return true but got false instead
[ 4/10] [params] 'default'
[ 5/10] [transaction]
[ 6/10] [step] 'in_transaction_step_1'
[ 7/10] [step] 'in_transaction_step_2'
[ 8/10] [try]
[ 9/10] [step] 'might_raise'
[10/10] [step] 'final_step'
OUTPUT
end
end
@ -266,5 +311,19 @@ RSpec.describe Service::StepsInspector do
expect(error).to eq("my error")
end
end
context "when an exception occurred inside the 'try' block" do
before do
class DummyService
def might_raise
raise "BOOM"
end
end
end
it "returns an error related to the exception" do
expect(error).to match(/RuntimeError: BOOM/)
end
end
end
end