anima-core 0.3.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +4 -1
- data/app/channels/session_channel.rb +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -9
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +4 -0
- data/lib/anima/installer.rb +182 -6
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +8 -7
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +284 -2
- data/.mise.toml +0 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# RSpec Matchers: Error/Exception Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/raise_error.feature,
|
|
3
|
+
# throw_symbol.feature
|
|
4
|
+
|
|
5
|
+
# raise_error / raise_exception
|
|
6
|
+
RSpec.describe "raise_error matcher" do
|
|
7
|
+
describe "basic usage" do
|
|
8
|
+
it "expects any error" do
|
|
9
|
+
expect { raise StandardError }.to raise_error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "expects specific error class" do
|
|
13
|
+
expect { raise ArgumentError }.to raise_error(ArgumentError)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "expects error with message string" do
|
|
17
|
+
expect { raise "boom" }.to raise_error("boom")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "expects error with message regex" do
|
|
21
|
+
expect { raise "Something went wrong" }.to raise_error(/wrong/)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "expects class and message" do
|
|
25
|
+
expect { raise ArgumentError, "invalid" }.to raise_error(ArgumentError, "invalid")
|
|
26
|
+
expect { raise ArgumentError, "invalid" }.to raise_error(ArgumentError, /inv/)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "with_message chain" do
|
|
31
|
+
it "uses with_message for readability" do
|
|
32
|
+
expect { raise StandardError, "detailed error" }
|
|
33
|
+
.to raise_error(StandardError)
|
|
34
|
+
.with_message(/detailed/)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "block form for complex assertions" do
|
|
39
|
+
it "yields error for inspection" do
|
|
40
|
+
expect { raise ArgumentError, "bad input" }.to raise_error { |error|
|
|
41
|
+
expect(error).to be_an(ArgumentError)
|
|
42
|
+
expect(error.message).to include("bad")
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "composed matchers" do
|
|
48
|
+
it "uses matcher composition" do
|
|
49
|
+
expect { raise ArgumentError, "invalid value" }.to raise_error(
|
|
50
|
+
an_instance_of(ArgumentError).and(having_attributes(message: /invalid/))
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "negative expectation" do
|
|
56
|
+
it "expects no error" do
|
|
57
|
+
expect { 1 + 1 }.not_to raise_error
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# throw_symbol
|
|
63
|
+
RSpec.describe "throw_symbol matcher" do
|
|
64
|
+
describe "basic usage" do
|
|
65
|
+
it "expects any symbol thrown" do
|
|
66
|
+
expect { throw :done }.to throw_symbol
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "expects specific symbol" do
|
|
70
|
+
expect { throw :abort }.to throw_symbol(:abort)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "expects symbol with value" do
|
|
74
|
+
expect { throw :result, 42 }.to throw_symbol(:result, 42)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "negative expectations" do
|
|
79
|
+
it "expects nothing thrown" do
|
|
80
|
+
expect { 1 + 1 }.not_to throw_symbol
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "expects different symbol" do
|
|
84
|
+
expect { throw :foo }.not_to throw_symbol(:bar)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Practical examples
|
|
90
|
+
RSpec.describe PaymentService do
|
|
91
|
+
subject(:service) { build(:payment_service) }
|
|
92
|
+
|
|
93
|
+
describe "#process" do
|
|
94
|
+
context "with invalid amount" do
|
|
95
|
+
it "raises ArgumentError" do
|
|
96
|
+
expect { service.process(amount: -100) }
|
|
97
|
+
.to raise_error(ArgumentError, /positive/)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context "with invalid currency" do
|
|
102
|
+
it "raises UnsupportedCurrencyError" do
|
|
103
|
+
expect { service.process(amount: 100, currency: "XXX") }
|
|
104
|
+
.to raise_error(UnsupportedCurrencyError)
|
|
105
|
+
.with_message(/XXX/)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context "when gateway fails" do
|
|
110
|
+
before { allow(service).to receive(:gateway).and_raise(GatewayError) }
|
|
111
|
+
|
|
112
|
+
it "raises PaymentFailedError with cause" do
|
|
113
|
+
expect { service.process(amount: 100) }.to raise_error { |error|
|
|
114
|
+
expect(error).to be_a(PaymentFailedError)
|
|
115
|
+
expect(error.cause).to be_a(GatewayError)
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
RSpec.describe "early exit with throw/catch" do
|
|
123
|
+
describe "batch processor" do
|
|
124
|
+
subject(:processor) { build(:batch_processor) }
|
|
125
|
+
|
|
126
|
+
it "throws :halt on critical error" do
|
|
127
|
+
expect { processor.process_with_halt_on_error(bad_records) }
|
|
128
|
+
.to throw_symbol(:halt)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "throws with error details" do
|
|
132
|
+
expect { processor.process_with_halt_on_error(bad_records) }
|
|
133
|
+
.to throw_symbol(:halt, a_hash_including(reason: :validation_failed))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# RSpec Matchers: Output Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/output.feature
|
|
3
|
+
|
|
4
|
+
# output to stdout
|
|
5
|
+
RSpec.describe "output matcher - stdout" do
|
|
6
|
+
describe "any output" do
|
|
7
|
+
it "expects output to stdout" do
|
|
8
|
+
expect { print "hello" }.to output.to_stdout
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "expects no output" do
|
|
12
|
+
expect { 1 + 1 }.not_to output.to_stdout
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe "specific output" do
|
|
17
|
+
it "matches exact string" do
|
|
18
|
+
expect { print "hello" }.to output("hello").to_stdout
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "matches with regex" do
|
|
22
|
+
expect { puts "Hello, World!" }.to output(/World/).to_stdout
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# output to stderr
|
|
28
|
+
RSpec.describe "output matcher - stderr" do
|
|
29
|
+
it "captures stderr" do
|
|
30
|
+
expect { warn "danger" }.to output.to_stderr
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "matches warning message" do
|
|
34
|
+
expect { warn "danger" }.to output("danger\n").to_stderr
|
|
35
|
+
expect { warn "danger" }.to output(/danger/).to_stderr
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# output from subprocesses
|
|
40
|
+
RSpec.describe "output matcher - any process" do
|
|
41
|
+
it "captures subprocess stdout" do
|
|
42
|
+
expect { system("echo hello") }.to output("hello\n").to_stdout_from_any_process
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "captures subprocess stderr" do
|
|
46
|
+
expect { system("echo error >&2") }.to output("error\n").to_stderr_from_any_process
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Practical examples
|
|
51
|
+
RSpec.describe Logger do
|
|
52
|
+
subject(:logger) { build(:logger, output: $stdout) }
|
|
53
|
+
|
|
54
|
+
describe "#info" do
|
|
55
|
+
it "outputs formatted message to stdout" do
|
|
56
|
+
expect { logger.info("test message") }
|
|
57
|
+
.to output(/\[INFO\].*test message/).to_stdout
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "#error" do
|
|
62
|
+
it "outputs to stderr" do
|
|
63
|
+
expect { logger.error("critical failure") }
|
|
64
|
+
.to output(/critical failure/).to_stderr
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
RSpec.describe CLI do
|
|
70
|
+
subject(:cli) { build(:cli) }
|
|
71
|
+
|
|
72
|
+
describe "#run" do
|
|
73
|
+
context "with --help flag" do
|
|
74
|
+
it "prints usage to stdout" do
|
|
75
|
+
expect { cli.run(["--help"]) }
|
|
76
|
+
.to output(/Usage:/).to_stdout
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context "with invalid option" do
|
|
81
|
+
it "prints error to stderr" do
|
|
82
|
+
expect { cli.run(["--invalid"]) }
|
|
83
|
+
.to output(/unknown option/).to_stderr
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context "in verbose mode" do
|
|
88
|
+
it "prints progress information" do
|
|
89
|
+
expect { cli.run(["--verbose", "process"]) }
|
|
90
|
+
.to output(/Processing/).to_stdout
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
RSpec.describe RakeTask do
|
|
97
|
+
describe "external command execution" do
|
|
98
|
+
it "captures output from system calls" do
|
|
99
|
+
expect { system("rake db:migrate") }
|
|
100
|
+
.to output(/migrated/).to_stdout_from_any_process
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# RSpec Matchers: Predicate Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/predicates.feature
|
|
3
|
+
|
|
4
|
+
# be_* - converts to predicate method call
|
|
5
|
+
RSpec.describe "dynamic be_* matchers" do
|
|
6
|
+
it "calls predicate methods ending in ?" do
|
|
7
|
+
expect(0).to be_zero # 0.zero?
|
|
8
|
+
expect([]).to be_empty # [].empty?
|
|
9
|
+
expect(5).to be_positive # 5.positive?
|
|
10
|
+
expect(-3).to be_negative # (-3).negative?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "works with custom predicates" do
|
|
14
|
+
user = build(:user, status: :active)
|
|
15
|
+
|
|
16
|
+
expect(user).to be_active # user.active?
|
|
17
|
+
expect(user).not_to be_banned # !user.banned?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "passes arguments to predicate" do
|
|
21
|
+
expect(12).to be_multiple_of(3) # 12.multiple_of?(3)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# have_* - converts to has_*? method call
|
|
26
|
+
RSpec.describe "dynamic have_* matchers" do
|
|
27
|
+
it "calls has_*? methods" do
|
|
28
|
+
expect({ a: 1 }).to have_key(:a) # {a: 1}.has_key?(:a)
|
|
29
|
+
expect({ a: 1 }).to have_value(1) # {a: 1}.has_value?(1)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "works with custom has_*? methods" do
|
|
33
|
+
order = build(:order, :with_items)
|
|
34
|
+
|
|
35
|
+
expect(order).to have_items # order.has_items?
|
|
36
|
+
expect(order).to have_discount # order.has_discount?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Practical examples with Rails models
|
|
41
|
+
RSpec.describe User do
|
|
42
|
+
subject(:user) { build(:user) }
|
|
43
|
+
|
|
44
|
+
describe "validation predicates" do
|
|
45
|
+
context "with valid attributes" do
|
|
46
|
+
it { is_expected.to be_valid }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context "without email" do
|
|
50
|
+
subject(:user) { build(:user, email: nil) }
|
|
51
|
+
|
|
52
|
+
it { is_expected.not_to be_valid }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "state predicates" do
|
|
57
|
+
context "when active" do
|
|
58
|
+
subject(:user) { build(:user, :active) }
|
|
59
|
+
|
|
60
|
+
it { is_expected.to be_active }
|
|
61
|
+
it { is_expected.not_to be_suspended }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context "when suspended" do
|
|
65
|
+
subject(:user) { build(:user, :suspended) }
|
|
66
|
+
|
|
67
|
+
it { is_expected.to be_suspended }
|
|
68
|
+
it { is_expected.not_to be_active }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
RSpec.describe Order do
|
|
74
|
+
subject(:order) { build(:order) }
|
|
75
|
+
|
|
76
|
+
describe "collection predicates" do
|
|
77
|
+
context "with items" do
|
|
78
|
+
subject(:order) { build(:order, :with_items) }
|
|
79
|
+
|
|
80
|
+
it { is_expected.to have_items }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context "without items" do
|
|
84
|
+
it { is_expected.not_to have_items }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# RSpec Matchers: Truthiness Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/be.feature, exist.feature
|
|
3
|
+
|
|
4
|
+
# be_truthy - any truthy value
|
|
5
|
+
RSpec.describe "be_truthy matcher" do
|
|
6
|
+
it "passes for true" do
|
|
7
|
+
expect(true).to be_truthy
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "passes for truthy values" do
|
|
11
|
+
expect(7).to be_truthy
|
|
12
|
+
expect("foo").to be_truthy
|
|
13
|
+
expect([]).to be_truthy # Empty array is truthy
|
|
14
|
+
expect(0).to be_truthy # Zero is truthy in Ruby!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "fails for nil and false" do
|
|
18
|
+
expect(nil).not_to be_truthy
|
|
19
|
+
expect(false).not_to be_truthy
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# be_falsey / be_falsy - nil or false only
|
|
24
|
+
RSpec.describe "be_falsey matcher" do
|
|
25
|
+
it "passes for nil" do
|
|
26
|
+
expect(nil).to be_falsey
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "passes for false" do
|
|
30
|
+
expect(false).to be_falsey
|
|
31
|
+
expect(false).to be_falsy # Alias
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "fails for truthy values" do
|
|
35
|
+
expect(true).not_to be_falsey
|
|
36
|
+
expect(0).not_to be_falsey # 0 is truthy!
|
|
37
|
+
expect("").not_to be_falsey # Empty string is truthy
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# be_nil - exactly nil
|
|
42
|
+
RSpec.describe "be_nil matcher" do
|
|
43
|
+
it "passes only for nil" do
|
|
44
|
+
expect(nil).to be_nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "fails for false" do
|
|
48
|
+
expect(false).not_to be_nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# be true / be false - exact boolean
|
|
53
|
+
RSpec.describe "exact boolean matchers" do
|
|
54
|
+
it "requires exact true" do
|
|
55
|
+
expect(true).to be true
|
|
56
|
+
expect(1).not_to be true # Truthy but not true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "requires exact false" do
|
|
60
|
+
expect(false).to be false
|
|
61
|
+
expect(nil).not_to be false # Falsey but not false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# exist - calls exist? or exists?
|
|
66
|
+
RSpec.describe "exist matcher" do
|
|
67
|
+
it "checks existence" do
|
|
68
|
+
# For file-like objects
|
|
69
|
+
expect(File).to exist("/tmp")
|
|
70
|
+
expect(File).not_to exist("/nonexistent/path")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Practical example: model validations
|
|
75
|
+
RSpec.describe User do
|
|
76
|
+
subject(:user) { build(:user) }
|
|
77
|
+
|
|
78
|
+
describe "#admin?" do
|
|
79
|
+
context "when user is admin" do
|
|
80
|
+
subject(:user) { build(:user, :admin) }
|
|
81
|
+
|
|
82
|
+
it "returns true" do
|
|
83
|
+
expect(user.admin?).to be true # Exact boolean check
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context "when user is not admin" do
|
|
88
|
+
it "returns false" do
|
|
89
|
+
expect(user.admin?).to be false # Not just falsey
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "#deleted_at" do
|
|
95
|
+
context "when not deleted" do
|
|
96
|
+
it "is nil" do
|
|
97
|
+
expect(user.deleted_at).to be_nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# RSpec Matchers: Type/Class Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/types.feature, respond_to.feature
|
|
3
|
+
|
|
4
|
+
# be_instance_of - exact class match
|
|
5
|
+
RSpec.describe "be_instance_of matcher" do
|
|
6
|
+
it "matches exact class only" do
|
|
7
|
+
expect(17.0).to be_instance_of(Float)
|
|
8
|
+
expect(17.0).to be_an_instance_of(Float) # Alias
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "does NOT match superclass" do
|
|
12
|
+
expect(17.0).not_to be_instance_of(Numeric) # Float < Numeric
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# be_kind_of / be_a - class hierarchy match
|
|
17
|
+
RSpec.describe "be_kind_of matcher" do
|
|
18
|
+
it "matches actual class" do
|
|
19
|
+
expect(17.0).to be_kind_of(Float)
|
|
20
|
+
expect(17.0).to be_a(Float) # Alias
|
|
21
|
+
expect(17.0).to be_an(Integer) # Grammatical alias (fails - just for example)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "matches superclass" do
|
|
25
|
+
expect(17.0).to be_kind_of(Numeric)
|
|
26
|
+
expect(17.0).to be_a(Object)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "matches included modules" do
|
|
30
|
+
expect([1, 2, 3]).to be_kind_of(Enumerable)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# respond_to - interface checking
|
|
35
|
+
RSpec.describe "respond_to matcher" do
|
|
36
|
+
it "checks method existence" do
|
|
37
|
+
expect("string").to respond_to(:length)
|
|
38
|
+
expect("string").to respond_to(:upcase, :downcase) # Multiple
|
|
39
|
+
expect("string").not_to respond_to(:foo)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "checks argument count" do
|
|
43
|
+
expect(7).to respond_to(:zero?).with(0).arguments
|
|
44
|
+
expect(7).to respond_to(:between?).with(2).arguments
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "checks argument range" do
|
|
48
|
+
# Methods with optional arguments
|
|
49
|
+
expect([]).to respond_to(:first).with(0..1).arguments
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "checks keyword arguments" do
|
|
53
|
+
service = build(:payment_service)
|
|
54
|
+
|
|
55
|
+
expect(service).to respond_to(:process).with_keywords(:amount, :currency)
|
|
56
|
+
expect(service).to respond_to(:process).with(1).argument.and_keywords(:amount)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Practical example: duck typing
|
|
61
|
+
RSpec.describe "exportable object" do
|
|
62
|
+
subject(:exporter) { build(:csv_exporter) }
|
|
63
|
+
|
|
64
|
+
it "implements exportable interface" do
|
|
65
|
+
expect(exporter).to respond_to(:export).with(1).argument
|
|
66
|
+
expect(exporter).to respond_to(:headers)
|
|
67
|
+
expect(exporter).to respond_to(:rows)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Practical example: model type checking
|
|
72
|
+
RSpec.describe User do
|
|
73
|
+
subject(:user) { build(:user) }
|
|
74
|
+
|
|
75
|
+
it "is an ApplicationRecord" do
|
|
76
|
+
expect(user).to be_a(ApplicationRecord)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "includes Authenticatable" do
|
|
80
|
+
expect(user).to be_kind_of(Authenticatable)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# RSpec Matchers: Yield Examples
|
|
2
|
+
# Source: rspec-expectations gem features/built_in_matchers/yield.feature
|
|
3
|
+
|
|
4
|
+
# NOTE: All yield matchers require a block probe |b|
|
|
5
|
+
|
|
6
|
+
# yield_control - detects any yield
|
|
7
|
+
RSpec.describe "yield_control matcher" do
|
|
8
|
+
it "passes when block is yielded to" do
|
|
9
|
+
expect { |b| 5.tap(&b) }.to yield_control
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "fails when block is not yielded to" do
|
|
13
|
+
expect { |b| "no yield" }.not_to yield_control
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe "with counts" do
|
|
17
|
+
it "specifies exact yield count" do
|
|
18
|
+
expect { |b| 3.times(&b) }.to yield_control.exactly(3).times
|
|
19
|
+
expect { |b| 2.times(&b) }.to yield_control.twice
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "specifies minimum yields" do
|
|
23
|
+
expect { |b| 5.times(&b) }.to yield_control.at_least(3).times
|
|
24
|
+
expect { |b| 5.times(&b) }.to yield_control.at_least(:once)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "specifies maximum yields" do
|
|
28
|
+
expect { |b| 2.times(&b) }.to yield_control.at_most(5).times
|
|
29
|
+
expect { |b| 1.times(&b) }.to yield_control.at_most(:twice)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# yield_with_no_args - yield without arguments
|
|
35
|
+
RSpec.describe "yield_with_no_args matcher" do
|
|
36
|
+
it "passes when yielded without arguments" do
|
|
37
|
+
expect { |b| yield_nothing(&b) }.to yield_with_no_args
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def yield_nothing
|
|
41
|
+
yield
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "fails when yielded with arguments" do
|
|
45
|
+
expect { |b| yield_with_value(&b) }.not_to yield_with_no_args
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def yield_with_value
|
|
49
|
+
yield 42
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# yield_with_args - yield with specific arguments
|
|
54
|
+
RSpec.describe "yield_with_args matcher" do
|
|
55
|
+
it "passes for any arguments" do
|
|
56
|
+
expect { |b| "foo".tap(&b) }.to yield_with_args
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "matches exact values" do
|
|
60
|
+
expect { |b| yield_value(42, &b) }.to yield_with_args(42)
|
|
61
|
+
expect { |b| yield_values("a", "b", &b) }.to yield_with_args("a", "b")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "matches types (uses ===)" do
|
|
65
|
+
expect { |b| yield_value(42, &b) }.to yield_with_args(Integer)
|
|
66
|
+
expect { |b| yield_value("foo", &b) }.to yield_with_args(String)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "matches with regex" do
|
|
70
|
+
expect { |b| yield_value("foobar", &b) }.to yield_with_args(/bar/)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def yield_value(val)
|
|
74
|
+
yield val
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def yield_values(*vals)
|
|
78
|
+
yield(*vals)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# yield_successive_args - multiple yields with different args
|
|
83
|
+
RSpec.describe "yield_successive_args matcher" do
|
|
84
|
+
it "matches each yielded value in order" do
|
|
85
|
+
expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "works with hashes" do
|
|
89
|
+
expect { |b| { a: 1, b: 2 }.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "works with composed matchers" do
|
|
93
|
+
expect { |b| [1, 2, 3].each(&b) }
|
|
94
|
+
.to yield_successive_args(a_value < 2, 2, a_value > 2)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Practical examples
|
|
99
|
+
RSpec.describe File do
|
|
100
|
+
describe ".open with block" do
|
|
101
|
+
it "yields file handle" do
|
|
102
|
+
expect { |b| File.open("/tmp/test.txt", "w", &b) }
|
|
103
|
+
.to yield_with_args(an_instance_of(File))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
RSpec.describe Enumerator do
|
|
109
|
+
describe "#each_with_index" do
|
|
110
|
+
it "yields element and index pairs" do
|
|
111
|
+
enum = %w[a b c].each_with_index
|
|
112
|
+
|
|
113
|
+
expect { |b| enum.each(&b) }
|
|
114
|
+
.to yield_successive_args(["a", 0], ["b", 1], ["c", 2])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
RSpec.describe "Database transaction" do
|
|
120
|
+
describe ".transaction" do
|
|
121
|
+
it "yields control for block execution" do
|
|
122
|
+
expect { |b| ActiveRecord::Base.transaction(&b) }.to yield_control
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "yields without arguments" do
|
|
126
|
+
expect { |b| ActiveRecord::Base.transaction(&b) }.to yield_with_no_args
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
RSpec.describe BatchProcessor do
|
|
132
|
+
subject(:processor) { build(:batch_processor) }
|
|
133
|
+
|
|
134
|
+
describe "#process_each" do
|
|
135
|
+
let(:items) { build_list(:item, 3) }
|
|
136
|
+
|
|
137
|
+
it "yields each item" do
|
|
138
|
+
expect { |b| processor.process_each(items, &b) }
|
|
139
|
+
.to yield_successive_args(*items)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "yields expected number of times" do
|
|
143
|
+
expect { |b| processor.process_each(items, &b) }
|
|
144
|
+
.to yield_control.exactly(3).times
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|