anima-core 0.3.0 → 1.0.0
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 +3 -0
- 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 +168 -0
- 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 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# RSpec Mocks: any_instance Examples
|
|
2
|
+
# Source: rspec-mocks gem features/working_with_legacy_code/any_instance.feature
|
|
3
|
+
|
|
4
|
+
# WARNING: any_instance is discouraged. It often indicates:
|
|
5
|
+
# - Missing dependency injection
|
|
6
|
+
# - Tight coupling in design
|
|
7
|
+
# - Need for refactoring
|
|
8
|
+
#
|
|
9
|
+
# Prefer instance_double with explicit injection.
|
|
10
|
+
# Use any_instance only for legacy code you can't easily refactor.
|
|
11
|
+
|
|
12
|
+
# allow_any_instance_of - stub method on all instances
|
|
13
|
+
RSpec.describe "allow_any_instance_of" do
|
|
14
|
+
describe "basic usage" do
|
|
15
|
+
it "stubs method on any instance" do
|
|
16
|
+
allow_any_instance_of(Widget).to receive(:name).and_return("Stubbed")
|
|
17
|
+
|
|
18
|
+
widget1 = Widget.new
|
|
19
|
+
widget2 = Widget.new
|
|
20
|
+
|
|
21
|
+
expect(widget1.name).to eq("Stubbed")
|
|
22
|
+
expect(widget2.name).to eq("Stubbed")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "with receive_messages" do
|
|
27
|
+
it "stubs multiple methods" do
|
|
28
|
+
allow_any_instance_of(Widget).to receive_messages(
|
|
29
|
+
name: "Stubbed",
|
|
30
|
+
price: 100
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
widget = Widget.new
|
|
34
|
+
expect(widget.name).to eq("Stubbed")
|
|
35
|
+
expect(widget.price).to eq(100)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "with arguments" do
|
|
40
|
+
it "matches specific arguments" do
|
|
41
|
+
allow_any_instance_of(Calculator).to receive(:add).with(1, 2).and_return(100)
|
|
42
|
+
|
|
43
|
+
calc = Calculator.new
|
|
44
|
+
expect(calc.add(1, 2)).to eq(100)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe "block receives instance" do
|
|
49
|
+
it "passes instance as first argument to block" do
|
|
50
|
+
allow_any_instance_of(String).to receive(:slice) do |instance, start, length|
|
|
51
|
+
"Instance: #{instance[start, length]}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
expect("hello world".slice(0, 5)).to eq("Instance: hello")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "consecutive return values" do
|
|
59
|
+
it "applies per instance, not globally" do
|
|
60
|
+
allow_any_instance_of(Counter).to receive(:value).and_return(1, 2, 3)
|
|
61
|
+
|
|
62
|
+
first = Counter.new
|
|
63
|
+
second = Counter.new
|
|
64
|
+
|
|
65
|
+
# Each instance gets its own sequence
|
|
66
|
+
expect(first.value).to eq(1)
|
|
67
|
+
expect(first.value).to eq(2)
|
|
68
|
+
expect(second.value).to eq(1) # New instance, new sequence
|
|
69
|
+
expect(first.value).to eq(3)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# expect_any_instance_of - expect at least one instance receives message
|
|
75
|
+
RSpec.describe "expect_any_instance_of" do
|
|
76
|
+
describe "basic expectation" do
|
|
77
|
+
it "passes if any instance receives the message" do
|
|
78
|
+
expect_any_instance_of(Widget).to receive(:save)
|
|
79
|
+
|
|
80
|
+
widget = Widget.new
|
|
81
|
+
widget.save
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe "with return value" do
|
|
86
|
+
it "returns specified value and verifies call" do
|
|
87
|
+
expect_any_instance_of(Widget).to receive(:save).and_return(true)
|
|
88
|
+
|
|
89
|
+
widget = Widget.new
|
|
90
|
+
expect(widget.save).to be(true)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Legacy code example - why any_instance exists
|
|
96
|
+
RSpec.describe "legacy code scenario" do
|
|
97
|
+
# Imagine this service creates its own dependencies internally
|
|
98
|
+
# and we can't easily inject them
|
|
99
|
+
class LegacyOrderService
|
|
100
|
+
def process(order_id)
|
|
101
|
+
order = Order.find(order_id) # Creates Order internally
|
|
102
|
+
order.process
|
|
103
|
+
order.save
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe LegacyOrderService do
|
|
108
|
+
subject(:service) { LegacyOrderService.new }
|
|
109
|
+
|
|
110
|
+
# Using any_instance because we can't inject Order
|
|
111
|
+
it "processes and saves the order" do
|
|
112
|
+
allow(Order).to receive(:find).and_return(build(:order))
|
|
113
|
+
expect_any_instance_of(Order).to receive(:process)
|
|
114
|
+
expect_any_instance_of(Order).to receive(:save)
|
|
115
|
+
|
|
116
|
+
service.process(1)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# BETTER: Refactor to use dependency injection
|
|
121
|
+
class RefactoredOrderService
|
|
122
|
+
def initialize(order_repository: Order)
|
|
123
|
+
@order_repository = order_repository
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def process(order_id)
|
|
127
|
+
order = @order_repository.find(order_id)
|
|
128
|
+
order.process
|
|
129
|
+
order.save
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
attr_reader :order_repository
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe RefactoredOrderService do
|
|
138
|
+
subject(:service) { RefactoredOrderService.new(order_repository:) }
|
|
139
|
+
|
|
140
|
+
let(:order_repository) { class_double("Order") }
|
|
141
|
+
let(:order) { instance_double("Order") }
|
|
142
|
+
|
|
143
|
+
it "processes and saves the order" do
|
|
144
|
+
allow(order_repository).to receive(:find).and_return(order)
|
|
145
|
+
expect(order).to receive(:process)
|
|
146
|
+
expect(order).to receive(:save)
|
|
147
|
+
|
|
148
|
+
service.process(1)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# When any_instance might be acceptable
|
|
154
|
+
RSpec.describe "acceptable any_instance usage" do
|
|
155
|
+
describe "testing framework extensions" do
|
|
156
|
+
# Testing behavior added to core classes
|
|
157
|
+
it "stubs String extension method" do
|
|
158
|
+
allow_any_instance_of(String).to receive(:custom_method).and_return("extended")
|
|
159
|
+
|
|
160
|
+
expect("test".custom_method).to eq("extended")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
describe "testing monkey patches in legacy code" do
|
|
165
|
+
it "verifies behavior without refactoring" do
|
|
166
|
+
expect_any_instance_of(LegacyModel).to receive(:legacy_callback)
|
|
167
|
+
|
|
168
|
+
LegacyModel.create(name: "test")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# RSpec Mocks: Argument Matchers Examples
|
|
2
|
+
# Source: rspec-mocks gem features/setting_constraints/matching_arguments.feature
|
|
3
|
+
|
|
4
|
+
# Built-in argument matchers
|
|
5
|
+
RSpec.describe "argument matchers" do
|
|
6
|
+
describe "anything" do
|
|
7
|
+
it "matches any single argument" do
|
|
8
|
+
dbl = double("collaborator")
|
|
9
|
+
expect(dbl).to receive(:foo).with(anything)
|
|
10
|
+
dbl.foo("whatever")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "matches at specific positions" do
|
|
14
|
+
dbl = double("collaborator")
|
|
15
|
+
expect(dbl).to receive(:foo).with(1, anything, 3)
|
|
16
|
+
dbl.foo(1, "anything goes here", 3)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "any_args" do
|
|
21
|
+
it "matches any number of arguments" do
|
|
22
|
+
dbl = double("collaborator")
|
|
23
|
+
expect(dbl).to receive(:foo).with(any_args)
|
|
24
|
+
dbl.foo(1, 2, 3, 4, 5)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "no_args" do
|
|
29
|
+
it "matches zero arguments" do
|
|
30
|
+
dbl = double("collaborator")
|
|
31
|
+
expect(dbl).to receive(:foo).with(no_args)
|
|
32
|
+
dbl.foo
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "type matchers" do
|
|
37
|
+
it "matches by kind_of" do
|
|
38
|
+
dbl = double("collaborator")
|
|
39
|
+
expect(dbl).to receive(:foo).with(kind_of(Numeric))
|
|
40
|
+
dbl.foo(42)
|
|
41
|
+
# Also matches floats, BigDecimal, etc.
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "matches by instance_of (exact class)" do
|
|
45
|
+
dbl = double("collaborator")
|
|
46
|
+
expect(dbl).to receive(:foo).with(instance_of(Integer))
|
|
47
|
+
dbl.foo(42)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "duck_type" do
|
|
52
|
+
it "matches by method presence" do
|
|
53
|
+
dbl = double("collaborator")
|
|
54
|
+
expect(dbl).to receive(:foo).with(duck_type(:to_s, :length))
|
|
55
|
+
|
|
56
|
+
dbl.foo("a string") # Has both methods
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe "boolean" do
|
|
61
|
+
it "matches true or false" do
|
|
62
|
+
dbl = double("collaborator")
|
|
63
|
+
allow(dbl).to receive(:foo).with(boolean)
|
|
64
|
+
|
|
65
|
+
dbl.foo(true)
|
|
66
|
+
dbl.foo(false)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "hash_including" do
|
|
71
|
+
it "matches partial hash" do
|
|
72
|
+
dbl = double("collaborator")
|
|
73
|
+
expect(dbl).to receive(:foo).with(hash_including(a: 1))
|
|
74
|
+
dbl.foo(a: 1, b: 2, c: 3)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "matches nested structure" do
|
|
78
|
+
dbl = double("collaborator")
|
|
79
|
+
expect(dbl).to receive(:foo).with(
|
|
80
|
+
hash_including(user: hash_including(name: "Alice"))
|
|
81
|
+
)
|
|
82
|
+
dbl.foo(user: { name: "Alice", email: "alice@example.com" })
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "hash_excluding" do
|
|
87
|
+
it "matches hash without specified keys" do
|
|
88
|
+
dbl = double("collaborator")
|
|
89
|
+
expect(dbl).to receive(:foo).with(hash_excluding(:admin))
|
|
90
|
+
dbl.foo(name: "Alice", role: "user")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "array_including" do
|
|
95
|
+
it "matches array containing items" do
|
|
96
|
+
dbl = double("collaborator")
|
|
97
|
+
expect(dbl).to receive(:foo).with(array_including(1, 2))
|
|
98
|
+
dbl.foo([1, 2, 3, 4])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe "array_excluding" do
|
|
103
|
+
it "matches array without specified items" do
|
|
104
|
+
dbl = double("collaborator")
|
|
105
|
+
expect(dbl).to receive(:foo).with(array_excluding(:admin))
|
|
106
|
+
dbl.foo([:user, :guest])
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "regex matching" do
|
|
111
|
+
it "matches strings with regex" do
|
|
112
|
+
dbl = double("collaborator")
|
|
113
|
+
expect(dbl).to receive(:foo).with(/bar/)
|
|
114
|
+
dbl.foo("foobar")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "RSpec matchers" do
|
|
119
|
+
it "uses collection matchers" do
|
|
120
|
+
dbl = double("collaborator")
|
|
121
|
+
expect(dbl).to receive(:foo).with(a_collection_containing_exactly(1, 2, 3))
|
|
122
|
+
dbl.foo([3, 1, 2])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "uses string matchers" do
|
|
126
|
+
dbl = double("collaborator")
|
|
127
|
+
expect(dbl).to receive(:foo).with(a_string_starting_with("Hello"))
|
|
128
|
+
dbl.foo("Hello, World!")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "uses comparison matchers" do
|
|
132
|
+
dbl = double("collaborator")
|
|
133
|
+
expect(dbl).to receive(:foo).with(a_value > 10)
|
|
134
|
+
dbl.foo(15)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe "satisfy" do
|
|
139
|
+
it "matches with custom predicate" do
|
|
140
|
+
dbl = double("collaborator")
|
|
141
|
+
expect(dbl).to receive(:foo).with(
|
|
142
|
+
satisfy { |x| x[:a][:b][:c] == 5 }
|
|
143
|
+
)
|
|
144
|
+
dbl.foo(a: { b: { c: 5 } })
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe "having_attributes" do
|
|
149
|
+
it "matches object with attributes" do
|
|
150
|
+
dbl = double("collaborator")
|
|
151
|
+
user = build(:user, name: "Alice", email: "alice@example.com")
|
|
152
|
+
|
|
153
|
+
expect(dbl).to receive(:process).with(
|
|
154
|
+
having_attributes(name: "Alice")
|
|
155
|
+
)
|
|
156
|
+
dbl.process(user)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Argument-dependent responses
|
|
162
|
+
RSpec.describe "conditional stubs" do
|
|
163
|
+
describe "different responses by argument" do
|
|
164
|
+
it "returns specific values for specific arguments" do
|
|
165
|
+
dbl = double("collaborator")
|
|
166
|
+
allow(dbl).to receive(:foo).and_return(:default)
|
|
167
|
+
allow(dbl).to receive(:foo).with(1).and_return(:one)
|
|
168
|
+
allow(dbl).to receive(:foo).with(2).and_return(:two)
|
|
169
|
+
|
|
170
|
+
expect(dbl.foo(0)).to eq(:default)
|
|
171
|
+
expect(dbl.foo(1)).to eq(:one)
|
|
172
|
+
expect(dbl.foo(2)).to eq(:two)
|
|
173
|
+
expect(dbl.foo(99)).to eq(:default)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Practical example
|
|
179
|
+
RSpec.describe UserRepository do
|
|
180
|
+
subject(:repository) { build(:user_repository, database:) }
|
|
181
|
+
|
|
182
|
+
let(:database) { instance_double("Database") }
|
|
183
|
+
|
|
184
|
+
describe "#find_by_attributes" do
|
|
185
|
+
before do
|
|
186
|
+
allow(database).to receive(:query)
|
|
187
|
+
.with(hash_including(active: true))
|
|
188
|
+
.and_return([build(:user, :active)])
|
|
189
|
+
|
|
190
|
+
allow(database).to receive(:query)
|
|
191
|
+
.with(hash_including(admin: true))
|
|
192
|
+
.and_return([build(:user, :admin)])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "queries active users" do
|
|
196
|
+
result = repository.find_by_attributes(active: true, name: "Alice")
|
|
197
|
+
expect(result.first).to be_active
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "queries admin users" do
|
|
201
|
+
result = repository.find_by_attributes(admin: true, department: "IT")
|
|
202
|
+
expect(result.first).to be_admin
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# RSpec Mocks: Constant Stubbing Examples
|
|
2
|
+
# Source: rspec-mocks gem features/mutating_constants/*.feature
|
|
3
|
+
|
|
4
|
+
# stub_const - temporarily replace constant values
|
|
5
|
+
RSpec.describe "stub_const" do
|
|
6
|
+
describe "top-level constants" do
|
|
7
|
+
it "stubs constant for duration of example" do
|
|
8
|
+
stub_const("FOO", 5)
|
|
9
|
+
expect(FOO).to eq(5)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "restores original value after example" do
|
|
13
|
+
original = FOO rescue nil
|
|
14
|
+
stub_const("FOO", 999)
|
|
15
|
+
# After this example, FOO returns to original
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "nested constants" do
|
|
20
|
+
it "stubs nested class constants" do
|
|
21
|
+
stub_const("MyGem::SomeClass::PER_PAGE", 100)
|
|
22
|
+
expect(MyGem::SomeClass::PER_PAGE).to eq(100)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "stubs module constants" do
|
|
26
|
+
stub_const("Rails::VERSION", "7.0.0")
|
|
27
|
+
expect(Rails::VERSION).to eq("7.0.0")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "replacing classes" do
|
|
32
|
+
it "stubs with fake class" do
|
|
33
|
+
fake_class = Class.new do
|
|
34
|
+
def self.perform
|
|
35
|
+
:fake_result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
stub_const("HeavyWorker", fake_class)
|
|
40
|
+
expect(HeavyWorker.perform).to eq(:fake_result)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "transfer_nested_constants" do
|
|
45
|
+
it "transfers all nested constants" do
|
|
46
|
+
fake_deck = Class.new
|
|
47
|
+
|
|
48
|
+
stub_const("CardDeck", fake_deck, transfer_nested_constants: true)
|
|
49
|
+
|
|
50
|
+
# Original CardDeck::SUITS is now available on fake_deck
|
|
51
|
+
expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "transfers selected constants" do
|
|
55
|
+
fake_deck = Class.new
|
|
56
|
+
|
|
57
|
+
stub_const("CardDeck", fake_deck, transfer_nested_constants: [:SUITS])
|
|
58
|
+
|
|
59
|
+
expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
|
|
60
|
+
# Other constants like RANKS are not transferred
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe "undefined constants" do
|
|
65
|
+
it "can stub constants that don't exist yet" do
|
|
66
|
+
stub_const("FUTURE_FEATURE_FLAG", true)
|
|
67
|
+
expect(FUTURE_FEATURE_FLAG).to be(true)
|
|
68
|
+
# Constant is removed after example
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# hide_const - temporarily make constant undefined
|
|
74
|
+
RSpec.describe "hide_const" do
|
|
75
|
+
describe "hiding defined constants" do
|
|
76
|
+
it "makes constant undefined" do
|
|
77
|
+
hide_const("SomeClass")
|
|
78
|
+
expect { SomeClass }.to raise_error(NameError)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "restores constant after example" do
|
|
82
|
+
hide_const("SomeClass")
|
|
83
|
+
# After this example, SomeClass is available again
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "hiding nested constants" do
|
|
88
|
+
it "hides nested constant" do
|
|
89
|
+
hide_const("MyGem::SomeClass::TIMEOUT")
|
|
90
|
+
expect { MyGem::SomeClass::TIMEOUT }.to raise_error(NameError)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "hiding undefined constants" do
|
|
95
|
+
it "does nothing for undefined constants" do
|
|
96
|
+
# Safe to call - no error raised
|
|
97
|
+
hide_const("DEFINITELY_NOT_DEFINED")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Practical examples
|
|
103
|
+
RSpec.describe "configuration testing" do
|
|
104
|
+
describe FeatureToggle do
|
|
105
|
+
describe ".enabled?" do
|
|
106
|
+
context "when feature is enabled" do
|
|
107
|
+
before { stub_const("FeatureToggle::FEATURES", { dark_mode: true }) }
|
|
108
|
+
|
|
109
|
+
it "returns true" do
|
|
110
|
+
expect(FeatureToggle.enabled?(:dark_mode)).to be(true)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
context "when feature is disabled" do
|
|
115
|
+
before { stub_const("FeatureToggle::FEATURES", { dark_mode: false }) }
|
|
116
|
+
|
|
117
|
+
it "returns false" do
|
|
118
|
+
expect(FeatureToggle.enabled?(:dark_mode)).to be(false)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
RSpec.describe "environment-dependent code" do
|
|
126
|
+
describe ApiClient do
|
|
127
|
+
context "in production" do
|
|
128
|
+
before { stub_const("Rails.env", ActiveSupport::StringInquirer.new("production")) }
|
|
129
|
+
|
|
130
|
+
it "uses production URL" do
|
|
131
|
+
expect(ApiClient.base_url).to eq("https://api.example.com")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
context "in development" do
|
|
136
|
+
before { stub_const("Rails.env", ActiveSupport::StringInquirer.new("development")) }
|
|
137
|
+
|
|
138
|
+
it "uses localhost URL" do
|
|
139
|
+
expect(ApiClient.base_url).to eq("http://localhost:3000")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
RSpec.describe "testing error handling for missing dependencies" do
|
|
146
|
+
describe ExternalServiceClient do
|
|
147
|
+
context "when gem is not loaded" do
|
|
148
|
+
before { hide_const("ExternalGem") }
|
|
149
|
+
|
|
150
|
+
it "raises descriptive error" do
|
|
151
|
+
expect { ExternalServiceClient.connect }
|
|
152
|
+
.to raise_error("ExternalGem is required. Add it to your Gemfile.")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
RSpec.describe "pagination configuration" do
|
|
159
|
+
describe UserListController do
|
|
160
|
+
subject(:controller) { build(:user_list_controller) }
|
|
161
|
+
|
|
162
|
+
context "with custom page size" do
|
|
163
|
+
before { stub_const("UserListController::DEFAULT_PAGE_SIZE", 50) }
|
|
164
|
+
|
|
165
|
+
it "uses custom page size" do
|
|
166
|
+
expect(controller.default_page_size).to eq(50)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
context "with default page size" do
|
|
171
|
+
it "uses standard page size" do
|
|
172
|
+
expect(controller.default_page_size).to eq(25)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# RSpec Mocks: Test Doubles Examples
|
|
2
|
+
# Source: rspec-mocks gem features/basics/test_doubles.feature,
|
|
3
|
+
# verifying_doubles/*.feature
|
|
4
|
+
|
|
5
|
+
# Basic double - strict, raises on unexpected messages
|
|
6
|
+
RSpec.describe "basic double" do
|
|
7
|
+
it "raises on unexpected messages" do
|
|
8
|
+
dbl = double("collaborator")
|
|
9
|
+
expect { dbl.foo }.to raise_error(RSpec::Mocks::MockExpectationError)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "can be created with predefined stubs" do
|
|
13
|
+
dbl = double("collaborator", foo: 3, bar: 4)
|
|
14
|
+
expect(dbl.foo).to eq(3)
|
|
15
|
+
expect(dbl.bar).to eq(4)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "can be anonymous with stubs" do
|
|
19
|
+
dbl = double(foo: "bar", baz: "qux")
|
|
20
|
+
expect(dbl.foo).to eq("bar")
|
|
21
|
+
expect(dbl.baz).to eq("qux")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# instance_double - verifies against instance methods
|
|
26
|
+
RSpec.describe "instance_double" do
|
|
27
|
+
describe "verification" do
|
|
28
|
+
it "allows stubbing existing methods" do
|
|
29
|
+
notifier = instance_double("ConsoleNotifier", notify: true)
|
|
30
|
+
expect(notifier.notify).to be(true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "raises if stubbing non-existent method" do
|
|
34
|
+
notifier = instance_double("ConsoleNotifier")
|
|
35
|
+
expect {
|
|
36
|
+
allow(notifier).to receive(:non_existent_method)
|
|
37
|
+
}.to raise_error(RSpec::Mocks::MockExpectationError, /does not implement/)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "verifies argument arity" do
|
|
41
|
+
calculator = instance_double("Calculator")
|
|
42
|
+
expect {
|
|
43
|
+
allow(calculator).to receive(:add).with(1, 2, 3, 4, 5)
|
|
44
|
+
}.to raise_error(RSpec::Mocks::MockExpectationError, /wrong number of arguments/)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Practical example with instance_double
|
|
50
|
+
RSpec.describe UserNotificationService do
|
|
51
|
+
subject(:service) { build(:user_notification_service, notifier:) }
|
|
52
|
+
|
|
53
|
+
let(:notifier) { instance_double("ConsoleNotifier") }
|
|
54
|
+
let(:user) { build(:user) }
|
|
55
|
+
|
|
56
|
+
describe "#notify" do
|
|
57
|
+
it "delegates to the notifier" do
|
|
58
|
+
expect(notifier).to receive(:notify).with(user.email, "Welcome!")
|
|
59
|
+
service.notify(user, "Welcome!")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# class_double - verifies against class methods
|
|
65
|
+
RSpec.describe "class_double" do
|
|
66
|
+
describe "replacing constants" do
|
|
67
|
+
it "can replace the class constant with as_stubbed_const" do
|
|
68
|
+
fake_mailer = class_double("UserMailer").as_stubbed_const
|
|
69
|
+
allow(fake_mailer).to receive(:send_welcome)
|
|
70
|
+
|
|
71
|
+
# Now UserMailer refers to fake_mailer within this example
|
|
72
|
+
UserMailer.send_welcome
|
|
73
|
+
expect(fake_mailer).to have_received(:send_welcome)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "transfers nested constants when requested" do
|
|
77
|
+
fake_class = class_double("CardDeck").as_stubbed_const(
|
|
78
|
+
transfer_nested_constants: true
|
|
79
|
+
)
|
|
80
|
+
# CardDeck::SUITS and other constants are now available
|
|
81
|
+
expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "can selectively transfer constants" do
|
|
85
|
+
fake_class = class_double("CardDeck").as_stubbed_const(
|
|
86
|
+
transfer_nested_constants: [:SUITS]
|
|
87
|
+
)
|
|
88
|
+
# Only SUITS is transferred
|
|
89
|
+
expect(CardDeck::SUITS).to eq(%w[hearts diamonds clubs spades])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Practical example with class_double
|
|
95
|
+
RSpec.describe OrderProcessor do
|
|
96
|
+
subject(:processor) { build(:order_processor) }
|
|
97
|
+
|
|
98
|
+
let(:payment_gateway) { class_double("PaymentGateway").as_stubbed_const }
|
|
99
|
+
|
|
100
|
+
describe "#process" do
|
|
101
|
+
let(:order) { build(:order, amount: 100) }
|
|
102
|
+
|
|
103
|
+
it "charges the payment gateway" do
|
|
104
|
+
expect(payment_gateway).to receive(:charge).with(100).and_return(true)
|
|
105
|
+
processor.process(order)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# object_double - doubles an existing object instance
|
|
111
|
+
RSpec.describe "object_double" do
|
|
112
|
+
describe "doubling real objects" do
|
|
113
|
+
it "avoids side effects while verifying methods" do
|
|
114
|
+
real_user = User.new
|
|
115
|
+
user_double = object_double(real_user, save: true)
|
|
116
|
+
|
|
117
|
+
expect(user_double.save).to be(true)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "verifies methods exist on the real object" do
|
|
121
|
+
real_user = User.new
|
|
122
|
+
user_double = object_double(real_user)
|
|
123
|
+
|
|
124
|
+
expect {
|
|
125
|
+
allow(user_double).to receive(:non_existent)
|
|
126
|
+
}.to raise_error(RSpec::Mocks::MockExpectationError)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "doubling constant objects" do
|
|
131
|
+
it "can replace global logger" do
|
|
132
|
+
logger = object_double("MyApp::LOGGER", info: nil, error: nil).as_stubbed_const
|
|
133
|
+
|
|
134
|
+
MyApp::LOGGER.info("test message")
|
|
135
|
+
expect(logger).to have_received(:info).with("test message")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|