メインコンテンツまでスキップ

失敗の集約

RSpec::Expectationsは、aggregate_failuresを提供しており、このAPIを使用すると、最初の失敗で中断するのではなく、一度にすべての失敗をグループ化して表示することができます。 RSpec::Coreは、この機能をいくつかの方法で改善しています。

  • RSpec::Coreは、コードスニペットとバックトレースをサブ失敗に追加することで、失敗の出力を大幅に向上させます。
  • RSpec::Coreは、この機能のためのメタデータの統合を提供します。:aggregate_failuresとタグ付けされた各例は、aggregate_failuresブロックでラップされます。また、config.define_derived_metadataを使用して、これをすべての例に自動的に適用することもできます。

メタデータ形式は非常に便利ですが、複数の異なるステップを持つエンドツーエンドテストには適していない場合があります。たとえば、(1)リクエストを行い、(2)リダイレクトを期待し、(3)リダイレクトに従い、(4)特定のレスポンスを期待するHTTPクライアントのワークフローの仕様を考えてみてください。もしexpect(response.status).to be_between(300, 399)の期待が失敗した場合、次のステップ(リダイレクトに従う)を実行することはできませんので、即座に中断したいと思うでしょう。このような状況では、テストワークフローの各ステップを表す期待をラップするために、aggregate_failuresブロック形式を使用することをお勧めします。

背景

以下の内容で "lib/client.rb" という名前のファイルがあるとします:

Response = Struct.new(:status, :headers, :body)

class Client
def self.make_request(url='/')
Response.new(404, { "Content-Type" => "text/plain" }, "Not Found")
end
end

aggregate_failuresブロック形式の使用

以下の内容で "spec/use_block_form_spec.rb" という名前のファイルがあるとします:

require 'client'

RSpec.describe Client do
after do
# this should be appended to failure list
expect(false).to be(true), "after hook failure"
end

around do |ex|
ex.run
# this should also be appended to failure list
expect(false).to be(true), "around hook failure"
end

it "returns a successful response" do
response = Client.make_request

aggregate_failures "testing response" do
expect(response.status).to eq(200)
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.body).to eq('{"message":"Success"}')
end
end
end

rspec spec/use_block_form_spec.rb を実行すると

すべての失敗が表示されるはずです:

Failures:

1) Client returns a successful response
Got 3 failures:

1.1) Got 3 failures from failure aggregation block "testing response".
# ./spec/use_block_form_spec.rb:18
# ./spec/use_block_form_spec.rb:10

1.1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/use_block_form_spec.rb:19

1.1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1 +1 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/use_block_form_spec.rb:20

1.1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/use_block_form_spec.rb:21

1.2) Failure/Error: expect(false).to be(true), "after hook failure"
after hook failure
# ./spec/use_block_form_spec.rb:6
# ./spec/use_block_form_spec.rb:10

1.3) Failure/Error: expect(false).to be(true), "around hook failure"
around hook failure
# ./spec/use_block_form_spec.rb:12

aggregate_failuresブロック形式の使用

以下の内容で "spec/use_block_form_spec.rb" という名前のファイルがあるとします:

require 'client'

RSpec.describe Client do
after do
# this should be appended to failure list
expect(false).to be(true), "after hook failure"
end

around do |ex|
ex.run
# this should also be appended to failure list
expect(false).to be(true), "around hook failure"
end

it "returns a successful response" do
response = Client.make_request

aggregate_failures "testing response" do
expect(response.status).to eq(200)
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.body).to eq('{"message":"Success"}')
end
end
end

rspec spec/use_block_form_spec.rb を実行すると

すべての失敗が表示されるはずです:

Failures:

1) Client returns a successful response
Got 3 failures:

1.1) Got 3 failures from failure aggregation block "testing response".
# ./spec/use_block_form_spec.rb:18
# ./spec/use_block_form_spec.rb:10

1.1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/use_block_form_spec.rb:19

1.1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1,2 +1,2 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/use_block_form_spec.rb:20

1.1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/use_block_form_spec.rb:21

1.2) Failure/Error: expect(false).to be(true), "after hook failure"
after hook failure
# ./spec/use_block_form_spec.rb:6
# ./spec/use_block_form_spec.rb:10

1.3) Failure/Error: expect(false).to be(true), "around hook failure"
around hook failure
# ./spec/use_block_form_spec.rb:12

:aggregate_failures メタデータを使用する

Given "spec/use_metadata_spec.rb" という名前のファイルがある場合、以下の内容である:

require 'client'

RSpec.describe Client do
it "follows a redirect", :aggregate_failures do
response = Client.make_request

expect(response.status).to eq(302)
expect(response.body).to eq('{"message":"Redirect"}')

redirect_response = Client.make_request(response.headers.fetch('Location'))

expect(redirect_response.status).to eq(200)
expect(redirect_response.body).to eq('{"message":"OK"}')
end
end

When rspec spec/use_metadata_spec.rb を実行すると

Then 失敗し、すべての失敗がリストされる:

Failures:

1) Client follows a redirect
Got 2 failures and 1 other error:

1.1) Failure/Error: expect(response.status).to eq(302)

expected: 302
got: 404

(compared using ==)
# ./spec/use_metadata_spec.rb:7

1.2) Failure/Error: expect(response.body).to eq('{"message":"Redirect"}')

expected: "{\"message\":\"Redirect\"}"
got: "Not Found"

(compared using ==)
# ./spec/use_metadata_spec.rb:8

1.3) Failure/Error: redirect_response = Client.make_request(response.headers.fetch('Location'))
KeyError:
key not found: "Location"
# ./spec/use_metadata_spec.rb:10
# ./spec/use_metadata_spec.rb:10

define_derived_metadata を使用してグローバルに失敗の集約を有効にする

Given "spec/enable_globally_spec.rb" という名前のファイルがある場合、以下の内容である:

require 'client'

RSpec.configure do |c|
c.define_derived_metadata do |meta|
meta[:aggregate_failures] = true
end
end

RSpec.describe Client do
it "returns a successful response" do
response = Client.make_request

expect(response.status).to eq(200)
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.body).to eq('{"message":"Success"}')
end
end

When rspec spec/enable_globally_spec.rb を実行すると

Then 失敗し、すべての失敗がリストされる:

Failures:

1) Client returns a successful response
Got 3 failures:

1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/enable_globally_spec.rb:13

1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1 +1 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/enable_globally_spec.rb:14

1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/enable_globally_spec.rb:15

define_derived_metadata を使用してグローバルに失敗の集約を有効にする

Given "spec/enable_globally_spec.rb" という名前のファイルがある場合、以下の内容である:

require 'client'

RSpec.configure do |c|
c.define_derived_metadata do |meta|
meta[:aggregate_failures] = true
end
end

RSpec.describe Client do
it "returns a successful response" do
response = Client.make_request

expect(response.status).to eq(200)
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.body).to eq('{"message":"Success"}')
end
end

When rspec spec/enable_globally_spec.rb を実行すると

Then 失敗し、すべての失敗がリストされる:

Failures:

1) Client returns a successful response
Got 3 failures:

1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/enable_globally_spec.rb:13

1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1,2 +1,2 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/enable_globally_spec.rb:14

1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/enable_globally_spec.rb:15

ネストされた失敗の集約が機能する

Given "spec/nested_failure_aggregation_spec.rb" という名前のファイルがある場合、以下の内容である:

require 'client'

RSpec.describe Client do
it "returns a successful response", :aggregate_failures do
response = Client.make_request

expect(response.status).to eq(200)

aggregate_failures "testing headers" do
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.headers).to include("Content-Length" => "21")
end

expect(response.body).to eq('{"message":"Success"}')
end
end

When rspec spec/nested_failure_aggregation_spec.rb を実行すると

Then 失敗し、すべての失敗がリストされる:

Failures:

1) Client returns a successful response
Got 3 failures:

1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/nested_failure_aggregation_spec.rb:7

1.2) Got 2 failures from failure aggregation block "testing headers".
# ./spec/nested_failure_aggregation_spec.rb:9

1.2.1) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1 +1 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/nested_failure_aggregation_spec.rb:10

1.2.2) Failure/Error: expect(response.headers).to include("Content-Length" => "21")
expected {"Content-Type" => "text/plain"} to include {"Content-Length" => "21"}
Diff:
@@ -1 +1 @@
-"Content-Length" => "21",
+"Content-Type" => "text/plain",
# ./spec/nested_failure_aggregation_spec.rb:11

1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/nested_failure_aggregation_spec.rb:14

ネストされた失敗の集約が機能する

Given "spec/nested_failure_aggregation_spec.rb" という名前のファイルがある場合、以下の内容である:

require 'client'

RSpec.describe Client do
it "returns a successful response", :aggregate_failures do
response = Client.make_request

expect(response.status).to eq(200)

aggregate_failures "testing headers" do
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.headers).to include("Content-Length" => "21")
end

expect(response.body).to eq('{"message":"Success"}')
end
end

When rspec spec/nested_failure_aggregation_spec.rb を実行すると

Then 失敗し、すべての失敗がリストされる:

Failures:

1) Client returns a successful response
Got 3 failures:

1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/nested_failure_aggregation_spec.rb:7

1.2) Got 2 failures from failure aggregation block "testing headers".
# ./spec/nested_failure_aggregation_spec.rb:9

1.2.1) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1,2 +1,2 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/nested_failure_aggregation_spec.rb:10

1.2.2) Failure/Error: expect(response.headers).to include("Content-Length" => "21")
expected {"Content-Type" => "text/plain"} to include {"Content-Length" => "21"}
Diff:
@@ -1,2 +1,2 @@
-"Content-Length" => "21",
+"Content-Type" => "text/plain",
# ./spec/nested_failure_aggregation_spec.rb:11

1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/nested_failure_aggregation_spec.rb:14

モックの期待失敗も集約される

Given "spec/mock_expectation_failure_spec.rb" という名前のファイルがある場合、以下の内容である:

require 'client'

RSpec.describe "Aggregating Failures", :aggregate_failures do
it "has a normal expectation failure and a message expectation failure" do
client = double("Client")
expect(client).to receive(:put).with("updated data")
allow(client).to receive(:get).and_return(Response.new(404, {}, "Not Found"))

response = client.get
expect(response.status).to eq(200)
end
end

When rspec spec/mock_expectation_failure_spec.rb を実行すると

Then 失敗し、すべての失敗がリストされる:

Failures:

1) Aggregating Failures has a normal expectation failure and a message expectation failure
Got 2 failures:

1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/mock_expectation_failure_spec.rb:10

1.2) Failure/Error: expect(client).to receive(:put).with("updated data")
(Double "Client").put("updated data")
expected: 1 time with arguments: ("updated data")
received: 0 times
# ./spec/mock_expectation_failure_spec.rb:6

保留中のテストは失敗の集約と正しく統合される

Given "spec/pending_spec.rb" という名前のファイルがある場合、以下の内容である:

require 'client'

RSpec.describe Client do
it "returns a successful response", :aggregate_failures do
pending "Not yet ready"
response = Client.make_request

expect(response.status).to eq(200)
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.body).to eq('{"message":"Success"}')
end
end

When rspec spec/pending_spec.rb を実行すると

Then パスし、すべての保留中の例がリストされる:

Pending: (Failures listed here are expected and do not affect your suite's status)

1) Client returns a successful response
# Not yet ready
Got 3 failures:

1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/pending_spec.rb:8

1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1 +1 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/pending_spec.rb:9

1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/pending_spec.rb:10

ペンディングは集約された失敗と正しく統合されます

Given ファイル名が "spec/pending_spec.rb" のファイルがあるとき:

require 'client'

RSpec.describe Client do
it "returns a successful response", :aggregate_failures do
pending "Not yet ready"
response = Client.make_request

expect(response.status).to eq(200)
expect(response.headers).to include("Content-Type" => "application/json")
expect(response.body).to eq('{"message":"Success"}')
end
end

When rspec spec/pending_spec.rb を実行すると

Then ペンディングの例がすべて表示され、パスするはずです:

Pending: (Failures listed here are expected and do not affect your suite's status)

1) Client returns a successful response
# Not yet ready
Got 3 failures:

1.1) Failure/Error: expect(response.status).to eq(200)

expected: 200
got: 404

(compared using ==)
# ./spec/pending_spec.rb:8

1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
Diff:
@@ -1,2 +1,2 @@
-"Content-Type" => "application/json",
+"Content-Type" => "text/plain",
# ./spec/pending_spec.rb:9

1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

expected: "{\"message\":\"Success\"}"
got: "Not Found"

(compared using ==)
# ./spec/pending_spec.rb:10