anima-core 0.2.1 → 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 +19 -0
- data/README.md +213 -43
- 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 +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -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 -6
- 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 +5 -40
- 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/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -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 +11 -20
- 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 +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- 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/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- 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 +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rspec
|
|
3
|
+
description: "RSpec testing with FactoryBot — matchers, test doubles, shared examples. Activate when writing specs, fixing failing tests, working with describe/it/expect blocks, editing *_spec.rb files, planning test strategy, or discussing unit/integration tests."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RSpec Testing
|
|
7
|
+
|
|
8
|
+
This skill provides comprehensive guidance for writing effective RSpec tests in Ruby and Rails applications. Use for writing new specs, fixing failing tests, understanding matchers, using test doubles, and following RSpec best practices.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
### Basic Structure
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
RSpec.describe Order do
|
|
16
|
+
subject(:order) { described_class.new(items) }
|
|
17
|
+
let(:items) { [item1, item2] }
|
|
18
|
+
let(:item1) { double("item", price: 10) }
|
|
19
|
+
let(:item2) { double("item", price: 20) }
|
|
20
|
+
|
|
21
|
+
describe "#total" do
|
|
22
|
+
it "sums item prices" do
|
|
23
|
+
expect(order.total).to eq(30)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context "with discount" do
|
|
28
|
+
let(:order) { described_class.new(items, discount: 5) }
|
|
29
|
+
|
|
30
|
+
it "applies discount" do
|
|
31
|
+
expect(order.total).to eq(25)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Key Concepts
|
|
38
|
+
|
|
39
|
+
| Concept | Purpose |
|
|
40
|
+
|---------|---------|
|
|
41
|
+
| `describe` / `context` | Group related examples |
|
|
42
|
+
| `it` / `specify` | Define individual test cases |
|
|
43
|
+
| `let` | Lazy-evaluated, memoized helper |
|
|
44
|
+
| `let!` | Eager-evaluated helper (runs before each example) |
|
|
45
|
+
| `subject` | Primary object under test |
|
|
46
|
+
| `before` / `after` | Setup and teardown hooks |
|
|
47
|
+
| `expect` | Make assertions |
|
|
48
|
+
|
|
49
|
+
## Writing Good Specs
|
|
50
|
+
|
|
51
|
+
### Use Named Subject for Method Tests
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
describe "#calculate_total" do
|
|
55
|
+
subject(:total) { order.calculate_total }
|
|
56
|
+
|
|
57
|
+
it "returns sum of items" do
|
|
58
|
+
expect(total).to eq(100)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Context Blocks for Different States
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
describe "#withdraw" do
|
|
67
|
+
context "with sufficient funds" do
|
|
68
|
+
let(:account) { build(:account, balance: 100) }
|
|
69
|
+
|
|
70
|
+
it "reduces balance" do
|
|
71
|
+
expect { account.withdraw(50) }.to change(account, :balance).by(-50)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context "with insufficient funds" do
|
|
76
|
+
let(:account) { build(:account, balance: 10) }
|
|
77
|
+
|
|
78
|
+
it "raises error" do
|
|
79
|
+
expect { account.withdraw(50) }.to raise_error(InsufficientFunds)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Common Matchers
|
|
86
|
+
|
|
87
|
+
### Equality
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
expect(x).to eq(y) # ==
|
|
91
|
+
expect(x).to eql(y) # eql? (type-sensitive)
|
|
92
|
+
expect(x).to be(y) # equal? (identity)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Truthiness
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
expect(x).to be_truthy # not nil or false
|
|
99
|
+
expect(x).to be_falsey # nil or false
|
|
100
|
+
expect(x).to be_nil
|
|
101
|
+
expect(x).to be true # exactly true
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Comparisons
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
expect(x).to be > 3
|
|
108
|
+
expect(x).to be_between(1, 10).inclusive
|
|
109
|
+
expect(x).to be_within(0.1).of(3.14)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Collections
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
expect(arr).to include(1, 2)
|
|
116
|
+
expect(arr).to contain_exactly(3, 2, 1) # order-independent
|
|
117
|
+
expect(arr).to all(be_positive)
|
|
118
|
+
expect(str).to start_with("hello")
|
|
119
|
+
expect(hash).to have_key(:name)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Changes
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
expect { x += 1 }.to change { x }.by(1)
|
|
126
|
+
expect { x += 1 }.to change { x }.from(0).to(1)
|
|
127
|
+
expect { user.save }.to change(User, :count).by(1)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Errors
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
expect { raise "boom" }.to raise_error
|
|
134
|
+
expect { raise ArgumentError, "bad" }.to raise_error(ArgumentError, /bad/)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Predicates (Dynamic)
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
expect([]).to be_empty # [].empty?
|
|
141
|
+
expect(user).to be_valid # user.valid?
|
|
142
|
+
expect(hash).to have_key(k) # hash.has_key?(k)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Test Doubles
|
|
146
|
+
|
|
147
|
+
### Types
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# Basic double (strict)
|
|
151
|
+
user = double("user", name: "Bob")
|
|
152
|
+
|
|
153
|
+
# Verifying doubles (recommended)
|
|
154
|
+
user = instance_double("User", name: "Bob") # validates instance methods
|
|
155
|
+
api = class_double("Api", fetch: data) # validates class methods
|
|
156
|
+
logger = object_double(Rails.logger) # validates object methods
|
|
157
|
+
|
|
158
|
+
# Spy (null object for after-the-fact verification)
|
|
159
|
+
notifier = spy("notifier")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Stubbing
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
allow(user).to receive(:name).and_return("Bob")
|
|
166
|
+
allow(Api).to receive(:fetch).and_return(data)
|
|
167
|
+
allow(obj).to receive(:method) { computed_value }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Expectations
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
expect(user).to receive(:save).and_return(true)
|
|
174
|
+
expect(Api).to receive(:post).with(hash_including(id: 1))
|
|
175
|
+
|
|
176
|
+
# Spy pattern (verify after action)
|
|
177
|
+
notifier = spy("notifier")
|
|
178
|
+
service.call(notifier)
|
|
179
|
+
expect(notifier).to have_received(:notify).with("done")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Argument Matchers
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
expect(obj).to receive(:call).with(anything)
|
|
186
|
+
expect(obj).to receive(:call).with(kind_of(Integer))
|
|
187
|
+
expect(obj).to receive(:call).with(hash_including(a: 1))
|
|
188
|
+
expect(obj).to receive(:call).with(array_including(1, 2))
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Rails Specs
|
|
192
|
+
|
|
193
|
+
| Spec Type | Use For | Key Helpers |
|
|
194
|
+
|-----------|---------|-------------|
|
|
195
|
+
| `type: :model` | Business logic, scopes, validations | `build`, `create`, associations |
|
|
196
|
+
| `type: :request` | Controller actions (preferred) | `get`, `post`, `response`, `have_http_status` |
|
|
197
|
+
| `type: :system` | Browser/UI testing | `visit`, `fill_in`, `click_button`, `have_text` |
|
|
198
|
+
| `type: :job` | Background jobs | `have_enqueued_job`, `perform_now` |
|
|
199
|
+
| `type: :mailer` | Email delivery | `have_enqueued_mail`, `deliver_now` |
|
|
200
|
+
| `type: :routing` | Route resolution | `route_to`, `be_routable` |
|
|
201
|
+
|
|
202
|
+
See `examples/rails/` for complete spec templates.
|
|
203
|
+
|
|
204
|
+
## Best Practices
|
|
205
|
+
|
|
206
|
+
### Do
|
|
207
|
+
|
|
208
|
+
- Use `described_class` instead of hardcoding class name
|
|
209
|
+
- Use `let` for test data, `let!` when database records must exist before test
|
|
210
|
+
- Use **named subject** when referencing in tests: `subject(:user) { ... }`
|
|
211
|
+
- Use context blocks to organize different scenarios
|
|
212
|
+
- Use verifying doubles (`instance_double`) over plain `double`
|
|
213
|
+
- Name examples with verbs: `it "creates user"` not `it "should create user"`
|
|
214
|
+
- Keep examples focused on one behavior
|
|
215
|
+
- Use factories over fixtures for flexible test data
|
|
216
|
+
- Prefer `build_stubbed` or `build` over `create` when database not needed
|
|
217
|
+
|
|
218
|
+
### Don't
|
|
219
|
+
|
|
220
|
+
- Don't use instance variables (`@user`) - use `let` for type safety
|
|
221
|
+
- Don't use `before` just to trigger `let` evaluation - use `let!` instead:
|
|
222
|
+
```ruby
|
|
223
|
+
# BAD - before just to initialize
|
|
224
|
+
let(:user) { create(:user) }
|
|
225
|
+
before { user }
|
|
226
|
+
|
|
227
|
+
# GOOD - let! for eager evaluation
|
|
228
|
+
let!(:user) { create(:user) }
|
|
229
|
+
```
|
|
230
|
+
Use `before` for side-effects like `sign_in(user)` or `driven_by(:rack_test)`
|
|
231
|
+
- Don't use `let!` when `let` suffices (wastes resources)
|
|
232
|
+
- Don't test Rails framework (validations work, focus on business logic)
|
|
233
|
+
- Don't stub the object under test
|
|
234
|
+
- Avoid `any_instance_of` - prefer stubbing `ClassName.new` to return a double (see `references/mocks.md`)
|
|
235
|
+
- Don't use `receive_message_chain` (violates Law of Demeter)
|
|
236
|
+
- Don't write examples without descriptions
|
|
237
|
+
- Shared examples work best for testing concerns across including classes. For unique behaviors, prefer repetition over abstraction
|
|
238
|
+
|
|
239
|
+
### Factory Bot
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
# Build strategies
|
|
243
|
+
user = build(:user) # In-memory, not persisted
|
|
244
|
+
user = create(:user) # Persisted to database
|
|
245
|
+
user = build_stubbed(:user) # Fake persisted (fastest)
|
|
246
|
+
attrs = attributes_for(:user) # Hash of attributes
|
|
247
|
+
|
|
248
|
+
# With traits and attributes
|
|
249
|
+
user = create(:user, :admin, :verified, name: "Bob")
|
|
250
|
+
|
|
251
|
+
# Lists
|
|
252
|
+
users = create_list(:user, 5, :admin)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Strategy Selection**:
|
|
256
|
+
- `build_stubbed` - Unit tests without database (fastest)
|
|
257
|
+
- `build` - Validation tests, method tests
|
|
258
|
+
- `create` - Database queries, scopes, associations
|
|
259
|
+
- `attributes_for` - Controller params
|
|
260
|
+
|
|
261
|
+
See `references/factory_bot.md` for traits, sequences, associations, and callbacks.
|
|
262
|
+
|
|
263
|
+
## Configuration
|
|
264
|
+
|
|
265
|
+
Essential settings for `spec_helper.rb` and `rails_helper.rb`:
|
|
266
|
+
|
|
267
|
+
| Setting | Purpose |
|
|
268
|
+
|---------|---------|
|
|
269
|
+
| `verify_partial_doubles = true` | Validates stubbed methods exist |
|
|
270
|
+
| `filter_run_when_matching :focus` | Run only focused specs (`fit`, `fdescribe`) |
|
|
271
|
+
| `order = :random` | Randomize spec order to catch dependencies |
|
|
272
|
+
| `use_transactional_fixtures = true` | Rollback database after each spec |
|
|
273
|
+
| `infer_spec_type_from_file_location!` | Auto-detect spec type from path |
|
|
274
|
+
|
|
275
|
+
See `examples/core/configuration.rb` for complete setup.
|
|
276
|
+
|
|
277
|
+
## Before You Write
|
|
278
|
+
|
|
279
|
+
**What are you about to do?**
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
├── Creating or modifying a factory?
|
|
283
|
+
│ └── Read `references/factory_bot.md`
|
|
284
|
+
│
|
|
285
|
+
├── Writing a new spec file?
|
|
286
|
+
│ ├── Model/service/PORO → Read `references/core.md`
|
|
287
|
+
│ ├── Request/controller → Read `references/rails.md`
|
|
288
|
+
│ └── System/feature/job/mailer → Read `references/rails.md`
|
|
289
|
+
│
|
|
290
|
+
├── Using test doubles, stubs, or mocks?
|
|
291
|
+
│ └── Read `references/mocks.md`
|
|
292
|
+
│
|
|
293
|
+
├── Writing custom or complex matchers?
|
|
294
|
+
│ └── Read `references/matchers.md`
|
|
295
|
+
│
|
|
296
|
+
└── Fixing a failing spec?
|
|
297
|
+
├── Factory-related error → Read `references/factory_bot.md`
|
|
298
|
+
├── Mock/stub error → Read `references/mocks.md`
|
|
299
|
+
├── Matcher error → Read `references/matchers.md`
|
|
300
|
+
└── Rails-specific error → Read `references/rails.md`
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Need code examples?**
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
├── Basic spec structure, hooks, shared examples
|
|
307
|
+
│ └── See `examples/core/`
|
|
308
|
+
│
|
|
309
|
+
├── Matcher usage patterns
|
|
310
|
+
│ └── See `examples/matchers/`
|
|
311
|
+
│
|
|
312
|
+
├── Test doubles and stubbing
|
|
313
|
+
│ └── See `examples/mocks/`
|
|
314
|
+
│
|
|
315
|
+
├── Rails spec templates (model, request, system, job, mailer)
|
|
316
|
+
│ └── See `examples/rails/`
|
|
317
|
+
│
|
|
318
|
+
└── Factory definitions with traits and associations
|
|
319
|
+
└── See `examples/factory_bot/`
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Running Specs
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
rspec # all specs
|
|
326
|
+
rspec spec/models # directory
|
|
327
|
+
rspec spec/user_spec.rb # file
|
|
328
|
+
rspec spec/user_spec.rb:23 # line
|
|
329
|
+
rspec --format doc # documentation format
|
|
330
|
+
rspec --only-failures # re-run failures
|
|
331
|
+
rspec --profile 10 # show slowest
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Debugging
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
rspec --seed 12345 # reproduce random order
|
|
338
|
+
rspec --fail-fast # stop on first failure
|
|
339
|
+
rspec --backtrace # full backtrace
|
|
340
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# RSpec Core: Basic Structure Examples
|
|
2
|
+
# Source: rspec-core gem features/example_groups/basic_structure.feature
|
|
3
|
+
|
|
4
|
+
# Basic describe/context/it structure
|
|
5
|
+
RSpec.describe Order do
|
|
6
|
+
context "with no items" do
|
|
7
|
+
let(:order) { build(:order) }
|
|
8
|
+
|
|
9
|
+
it "has zero total" do
|
|
10
|
+
expect(order.total).to eq(0)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context "with one item" do
|
|
15
|
+
let(:order) { build(:order) }
|
|
16
|
+
let(:item) { build(:item, price: 10) }
|
|
17
|
+
|
|
18
|
+
before { order.add_item(item) }
|
|
19
|
+
|
|
20
|
+
it "has item price as total" do
|
|
21
|
+
expect(order.total).to eq(10)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Method-focused describe blocks
|
|
27
|
+
RSpec.describe Calculator do
|
|
28
|
+
subject(:calculator) { build(:calculator) }
|
|
29
|
+
|
|
30
|
+
describe "#add" do
|
|
31
|
+
subject(:result) { calculator.add(2, 3) }
|
|
32
|
+
|
|
33
|
+
it "sums two numbers" do
|
|
34
|
+
expect(result).to eq(5)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe ".from_string" do
|
|
39
|
+
subject(:calc) { described_class.from_string("2+3") }
|
|
40
|
+
|
|
41
|
+
it "parses expression" do
|
|
42
|
+
expect(calc.result).to eq(5)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Nested contexts for state variations
|
|
48
|
+
RSpec.describe BankAccount do
|
|
49
|
+
describe "#withdraw" do
|
|
50
|
+
subject(:account) { build(:bank_account, balance:) }
|
|
51
|
+
|
|
52
|
+
context "with sufficient funds" do
|
|
53
|
+
let(:balance) { 100 }
|
|
54
|
+
|
|
55
|
+
it "reduces balance" do
|
|
56
|
+
account.withdraw(50)
|
|
57
|
+
expect(account.balance).to eq(50)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context "with insufficient funds" do
|
|
62
|
+
let(:balance) { 10 }
|
|
63
|
+
|
|
64
|
+
it "raises InsufficientFundsError" do
|
|
65
|
+
expect { account.withdraw(50) }.to raise_error(InsufficientFundsError)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# RSpec Core: Configuration Examples
|
|
2
|
+
# Source: rspec-core gem spec/spec_helper.rb, features/configuration/*.feature
|
|
3
|
+
|
|
4
|
+
# Full spec_helper.rb configuration
|
|
5
|
+
RSpec.configure do |config|
|
|
6
|
+
# Expectations configuration
|
|
7
|
+
config.expect_with :rspec do |expectations|
|
|
8
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
9
|
+
expectations.max_formatted_output_length = 1000
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Mocks configuration
|
|
13
|
+
config.mock_with :rspec do |mocks|
|
|
14
|
+
mocks.verify_partial_doubles = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Execution order
|
|
18
|
+
config.order = :random
|
|
19
|
+
Kernel.srand config.seed
|
|
20
|
+
|
|
21
|
+
# Failure handling
|
|
22
|
+
config.fail_fast = false # or number like 3
|
|
23
|
+
|
|
24
|
+
# Focus filtering - run only focused tests when any exist
|
|
25
|
+
config.filter_run_when_matching :focus
|
|
26
|
+
|
|
27
|
+
# Persist example status for --only-failures
|
|
28
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
|
29
|
+
|
|
30
|
+
# Output formatting
|
|
31
|
+
config.default_formatter = "doc" if config.files_to_run.one?
|
|
32
|
+
|
|
33
|
+
# Profile slow examples
|
|
34
|
+
config.profile_examples = 10
|
|
35
|
+
|
|
36
|
+
# Disable monkey patching (no should syntax)
|
|
37
|
+
config.disable_monkey_patching!
|
|
38
|
+
|
|
39
|
+
# Warnings
|
|
40
|
+
config.raise_errors_for_deprecations!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Including modules conditionally
|
|
44
|
+
RSpec.configure do |config|
|
|
45
|
+
# Include everywhere
|
|
46
|
+
config.include FactoryBot::Syntax::Methods
|
|
47
|
+
|
|
48
|
+
# Include only in specific types
|
|
49
|
+
config.include Devise::Test::ControllerHelpers, type: :controller
|
|
50
|
+
config.include Devise::Test::IntegrationHelpers, type: :request
|
|
51
|
+
config.include Capybara::DSL, type: :feature
|
|
52
|
+
|
|
53
|
+
# Include based on metadata
|
|
54
|
+
config.include ApiHelpers, :api
|
|
55
|
+
config.include AuthHelpers, :authorized
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Extending example groups
|
|
59
|
+
RSpec.configure do |config|
|
|
60
|
+
config.extend ControllerMacros, type: :controller
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Custom type inference
|
|
64
|
+
RSpec.configure do |config|
|
|
65
|
+
config.infer_spec_type_from_file_location!
|
|
66
|
+
|
|
67
|
+
config.define_derived_metadata(file_path: %r{/spec/api/}) do |metadata|
|
|
68
|
+
metadata[:type] = :request
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Shared context auto-inclusion
|
|
73
|
+
RSpec.configure do |config|
|
|
74
|
+
config.include_context "authenticated user", :authenticated
|
|
75
|
+
config.include_context "with admin", :admin
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Filter by Ruby version
|
|
79
|
+
RSpec.configure do |config|
|
|
80
|
+
config.filter_run_excluding ruby: ->(version) {
|
|
81
|
+
case version.to_s
|
|
82
|
+
when "!jruby"
|
|
83
|
+
RUBY_ENGINE == "jruby"
|
|
84
|
+
when /^> (.*)/
|
|
85
|
+
!(RUBY_VERSION.to_s > $1)
|
|
86
|
+
else
|
|
87
|
+
!(RUBY_VERSION.to_s =~ /^#{version}/)
|
|
88
|
+
end
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Example:
|
|
93
|
+
it "uses Ruby 3.2 feature", ruby: "> 3.2" do
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Alias it_behaves_like for readability
|
|
97
|
+
RSpec.configure do |config|
|
|
98
|
+
config.alias_it_behaves_like_to :it_has_behavior
|
|
99
|
+
config.alias_it_behaves_like_to :it_should_behave_like
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Around hooks for specific metadata
|
|
103
|
+
RSpec.configure do |config|
|
|
104
|
+
config.around(:example, :freeze_time) do |example|
|
|
105
|
+
travel_to(Time.zone.local(2024, 1, 1)) do
|
|
106
|
+
example.run
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
config.around(:example, :isolated_directory) do |example|
|
|
111
|
+
Dir.mktmpdir do |dir|
|
|
112
|
+
Dir.chdir(dir) { example.run }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# .rspec file example
|
|
118
|
+
# --format documentation
|
|
119
|
+
# --color
|
|
120
|
+
# --require spec_helper
|
|
121
|
+
# --order random
|
|
122
|
+
# --profile 10
|
|
123
|
+
|
|
124
|
+
# .rspec-local (gitignored, personal preferences)
|
|
125
|
+
# --fail-fast
|
|
126
|
+
# --format progress
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# RSpec Core: Hooks Examples
|
|
2
|
+
# Source: rspec-core gem features/hooks/before_and_after_hooks.feature
|
|
3
|
+
|
|
4
|
+
# before(:example) - runs before each example
|
|
5
|
+
# NOTE: Use let/let! instead of before + instance variables
|
|
6
|
+
RSpec.describe Thing do
|
|
7
|
+
let(:thing) { build(:thing) }
|
|
8
|
+
|
|
9
|
+
it "has 0 widgets initially" do
|
|
10
|
+
expect(thing.widgets.count).to eq(0)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "can accept widgets" do
|
|
14
|
+
thing.widgets << build(:widget)
|
|
15
|
+
expect(thing.widgets.count).to eq(1)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "does not share state across examples" do
|
|
19
|
+
expect(thing.widgets.count).to eq(0) # Fresh thing via let
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Hook scopes - configuration level
|
|
24
|
+
# NOTE: Suite/context level hooks are configured globally
|
|
25
|
+
RSpec.configure do |config|
|
|
26
|
+
config.before(:suite) do
|
|
27
|
+
# Run once before all specs (database setup, etc.)
|
|
28
|
+
DatabaseCleaner.strategy = :transaction
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
config.after(:suite) do
|
|
32
|
+
# Run once after all specs (cleanup)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Conditional hooks with metadata
|
|
37
|
+
RSpec.configure do |config|
|
|
38
|
+
config.before(:example, :authorized) do
|
|
39
|
+
sign_in_as(:authorized_user)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
config.before(:example, db: :clean) do
|
|
43
|
+
DatabaseCleaner.clean
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
RSpec.describe AdminController, :authorized do
|
|
48
|
+
let(:admin) { create(:user, :admin) }
|
|
49
|
+
|
|
50
|
+
it "allows access" do # sign_in_as runs automatically
|
|
51
|
+
get :index
|
|
52
|
+
expect(response).to be_successful
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# around hooks - wrap example execution
|
|
57
|
+
RSpec.describe "Database transaction" do
|
|
58
|
+
around(:example) do |example|
|
|
59
|
+
DatabaseCleaner.cleaning do
|
|
60
|
+
example.run
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
let(:user) { create(:user) }
|
|
65
|
+
|
|
66
|
+
it "runs inside transaction" do
|
|
67
|
+
expect(user).to be_persisted
|
|
68
|
+
# Transaction rolled back after example
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# around with before/after - execution order
|
|
73
|
+
RSpec.describe "Hook order" do
|
|
74
|
+
# Output order:
|
|
75
|
+
# 1. around: before
|
|
76
|
+
# 2. before
|
|
77
|
+
# 3. example
|
|
78
|
+
# 4. after
|
|
79
|
+
# 5. around: after
|
|
80
|
+
|
|
81
|
+
around(:example) do |example|
|
|
82
|
+
puts "around: before"
|
|
83
|
+
example.run
|
|
84
|
+
puts "around: after"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
before(:example) { puts "before" }
|
|
88
|
+
after(:example) { puts "after" }
|
|
89
|
+
|
|
90
|
+
it "runs in order" do
|
|
91
|
+
puts "example"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# before(:context) - shared expensive setup
|
|
96
|
+
# NOTE: let/subject/mocks NOT available in before(:context)
|
|
97
|
+
# Use instance variables ONLY when before(:context) is required
|
|
98
|
+
RSpec.describe "Expensive shared setup" do
|
|
99
|
+
before(:context) do
|
|
100
|
+
# This is the ONE exception where instance variables are acceptable
|
|
101
|
+
# because let is not supported in before(:context)
|
|
102
|
+
@shared_resource = ExpensiveResource.create
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
after(:context) do
|
|
106
|
+
@shared_resource.cleanup
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "uses shared resource" do
|
|
110
|
+
expect(@shared_resource).to be_ready
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "reuses same resource" do
|
|
114
|
+
expect(@shared_resource).to be_ready
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Preferred alternative: use let! with memoization at context level
|
|
119
|
+
RSpec.describe "Preferred shared setup" do
|
|
120
|
+
# If possible, restructure to avoid before(:context)
|
|
121
|
+
let!(:resource) { create(:expensive_resource) }
|
|
122
|
+
|
|
123
|
+
it "uses resource" do
|
|
124
|
+
expect(resource).to be_ready
|
|
125
|
+
end
|
|
126
|
+
end
|