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,816 @@
|
|
|
1
|
+
# RSpec Core Reference
|
|
2
|
+
|
|
3
|
+
Comprehensive reference for RSpec's core DSL, configuration, and structure.
|
|
4
|
+
|
|
5
|
+
## Example Groups and Examples
|
|
6
|
+
|
|
7
|
+
### describe / context
|
|
8
|
+
|
|
9
|
+
Example groups are classes that inherit from `RSpec::Core::ExampleGroup`:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
RSpec.describe Order do
|
|
13
|
+
context "with no items" do
|
|
14
|
+
# nested context - creates a subclass
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `describe` and `context` are aliases
|
|
20
|
+
- Blocks are evaluated eagerly when spec file loads
|
|
21
|
+
- Nested groups inherit from parent groups
|
|
22
|
+
|
|
23
|
+
### it / specify / example
|
|
24
|
+
|
|
25
|
+
Individual test cases:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
it "sums the prices" do
|
|
29
|
+
expect(order.total).to eq(5.55)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Variants with metadata:
|
|
33
|
+
fit "focused example" # :focus => true
|
|
34
|
+
xit "skipped example" # :skip => 'Temporarily skipped'
|
|
35
|
+
pending "pending example" # :pending => true
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Hooks
|
|
39
|
+
|
|
40
|
+
### before / after
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
before(:example) { } # runs before each example (alias: :each)
|
|
44
|
+
before(:context) { } # runs once before all examples (alias: :all)
|
|
45
|
+
before(:suite) { } # runs once before entire suite (only in RSpec.configure)
|
|
46
|
+
|
|
47
|
+
after(:example) { } # runs after each example
|
|
48
|
+
after(:context) { } # runs after all examples in group
|
|
49
|
+
after(:suite) { } # runs after entire suite
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Execution Order
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
before(:suite) # RSpec.configure
|
|
56
|
+
before(:context) # RSpec.configure
|
|
57
|
+
before(:context) # parent group
|
|
58
|
+
before(:context) # current group
|
|
59
|
+
before(:example) # RSpec.configure
|
|
60
|
+
before(:example) # parent group
|
|
61
|
+
before(:example) # current group
|
|
62
|
+
# example runs
|
|
63
|
+
after(:example) # reverse order
|
|
64
|
+
after(:context) # reverse order
|
|
65
|
+
after(:suite) # reverse order
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Conditional Hooks
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
RSpec.configure do |config|
|
|
72
|
+
config.before(:example, :authorized => true) do
|
|
73
|
+
log_in_as :authorized_user
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
RSpec.describe Something, :authorized => true do
|
|
78
|
+
# before hook runs here
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Constraints
|
|
83
|
+
|
|
84
|
+
- `before(:context)` shares state via instance variables (ordering dependencies risk)
|
|
85
|
+
- `let`, `subject`, mocks/stubs NOT supported in `before(:context)`
|
|
86
|
+
- Database transactions expect `:example` scope
|
|
87
|
+
|
|
88
|
+
### around
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
around(:example) do |example|
|
|
92
|
+
DatabaseCleaner.cleaning do
|
|
93
|
+
example.run
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Memoized Helpers
|
|
99
|
+
|
|
100
|
+
### Why let Over Instance Variables
|
|
101
|
+
|
|
102
|
+
Instance variables in RSpec create critical problems:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# BAD - typo goes unnoticed, @usre returns nil silently
|
|
106
|
+
before { @user = create(:user) }
|
|
107
|
+
it "does something" do
|
|
108
|
+
expect(@usre.name).to eq("Alice") # nil.name raises NoMethodError
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# GOOD - typo raises NameError immediately
|
|
112
|
+
let(:user) { create(:user) }
|
|
113
|
+
it "does something" do
|
|
114
|
+
expect(usre.name).to eq("Alice") # NameError: undefined local variable
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Problems with instance variables:
|
|
119
|
+
- **Silent failures**: Undefined instance variables return `nil` without errors
|
|
120
|
+
- **State leakage**: Can leak between test files and examples
|
|
121
|
+
- **No lazy evaluation**: Always executed in before blocks
|
|
122
|
+
|
|
123
|
+
### Why let Over Helper Methods
|
|
124
|
+
|
|
125
|
+
Always prefer `let`:
|
|
126
|
+
- **Memoization**: Cached within example, prevents redundant queries
|
|
127
|
+
- **Lazy evaluation**: Only executes when referenced
|
|
128
|
+
- **Overridable**: Redefine in nested contexts with `super()`
|
|
129
|
+
- **Type safety**: Typos raise `NameError` immediately
|
|
130
|
+
|
|
131
|
+
Use helper methods only when:
|
|
132
|
+
- A `let` would only be used inside another `let` definition (chain of lets)
|
|
133
|
+
- Setup needs parameters that vary per call within the same example
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# Always use let for test dependencies
|
|
137
|
+
let(:user) { create(:user) }
|
|
138
|
+
let(:post) { create(:post, author: user) }
|
|
139
|
+
let(:comment) { create(:comment, post:, author: user) }
|
|
140
|
+
|
|
141
|
+
# Helper method: parameterized setup called multiple times in one example
|
|
142
|
+
def create_order_with_items(count)
|
|
143
|
+
order = Order.new
|
|
144
|
+
count.times { order.add_item(build(:item)) }
|
|
145
|
+
order
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "compares orders of different sizes" do
|
|
149
|
+
small_order = create_order_with_items(2)
|
|
150
|
+
large_order = create_order_with_items(10)
|
|
151
|
+
expect(large_order.total).to be > small_order.total
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### let
|
|
156
|
+
|
|
157
|
+
Lazily-evaluated, memoized helper:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
let(:user) { create(:user) }
|
|
161
|
+
|
|
162
|
+
it "uses user" do
|
|
163
|
+
user # evaluated here
|
|
164
|
+
user # returns same instance
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Characteristics:
|
|
169
|
+
- Lazy evaluation: not invoked until first reference
|
|
170
|
+
- Memoized within an example (multiple calls return same value)
|
|
171
|
+
- NOT cached across examples (fresh for each example)
|
|
172
|
+
- Thread-safe by default
|
|
173
|
+
|
|
174
|
+
### let!
|
|
175
|
+
|
|
176
|
+
Same as `let` but evaluated in implicit `before` hook:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
let!(:user) { create(:user) } # evaluated before each example
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Use when:
|
|
183
|
+
- Side effects needed before example runs
|
|
184
|
+
- Data must exist for database queries/scopes
|
|
185
|
+
- Records needed for associations
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# let! needed - testing scope that queries database
|
|
189
|
+
let!(:active_user) { create(:user, status: :active) }
|
|
190
|
+
let!(:inactive_user) { create(:user, status: :inactive) }
|
|
191
|
+
|
|
192
|
+
it "finds only active users" do
|
|
193
|
+
expect(User.active).to contain_exactly(active_user)
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### let vs let! Decision Guide
|
|
198
|
+
|
|
199
|
+
| Use `let` when | Use `let!` when |
|
|
200
|
+
|----------------|-----------------|
|
|
201
|
+
| Value used in test body | Side effects needed before test |
|
|
202
|
+
| Not all examples use value | Testing database scopes/queries |
|
|
203
|
+
| Performance matters | Records must exist for count checks |
|
|
204
|
+
|
|
205
|
+
**Anti-pattern**: Using `let!` when `let` suffices:
|
|
206
|
+
```ruby
|
|
207
|
+
# Bad - creates data unnecessarily
|
|
208
|
+
let!(:admin) { create(:user, :admin) }
|
|
209
|
+
|
|
210
|
+
it "validates email format" do
|
|
211
|
+
user = build(:user, email: "invalid")
|
|
212
|
+
expect(user).not_to be_valid
|
|
213
|
+
# admin created but never used!
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Overriding let in Nested Contexts
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
let(:discount) { 0 }
|
|
221
|
+
|
|
222
|
+
it "calculates full price" do
|
|
223
|
+
expect(cart.total).to eq(100)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
context "with discount" do
|
|
227
|
+
let(:discount) { 20 } # Overrides parent
|
|
228
|
+
|
|
229
|
+
it "applies discount" do
|
|
230
|
+
expect(cart.total).to eq(80)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Using super() to extend parent values**:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
let(:params) { { name: "Item", price: 10 } }
|
|
239
|
+
|
|
240
|
+
context "with discount" do
|
|
241
|
+
let(:params) { super().merge(discount: 2) } # Must use super() with parens
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### subject
|
|
246
|
+
|
|
247
|
+
Implicit or explicit test target:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# Implicit - creates instance of described class
|
|
251
|
+
RSpec.describe Array do
|
|
252
|
+
it { is_expected.to be_empty } # subject is Array.new
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Explicit
|
|
256
|
+
subject { [1, 2, 3] }
|
|
257
|
+
|
|
258
|
+
# Named subject (creates both subject and named method)
|
|
259
|
+
subject(:account) { CheckingAccount.new(50) }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
One-liner syntax:
|
|
263
|
+
- `is_expected` wraps subject in `expect(subject)`
|
|
264
|
+
- `should` / `should_not` (legacy syntax)
|
|
265
|
+
|
|
266
|
+
### Named Subject Best Practices
|
|
267
|
+
|
|
268
|
+
Always use named subject when referencing in tests:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# BAD - what is "subject"?
|
|
272
|
+
describe Article do
|
|
273
|
+
subject { Article.new }
|
|
274
|
+
it "validates presence of title" do
|
|
275
|
+
expect(subject).not_to be_valid # Requires scrolling to understand
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# GOOD - intention-revealing name
|
|
280
|
+
describe Article do
|
|
281
|
+
subject(:article) { Article.new }
|
|
282
|
+
it "validates presence of title" do
|
|
283
|
+
expect(article).not_to be_valid
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Subject Anti-Patterns
|
|
289
|
+
|
|
290
|
+
**1. At class level, subject should be the object under test**:
|
|
291
|
+
|
|
292
|
+
At the top-level `describe`, subject represents the object being tested:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
# BAD - at class level, subject should be the object, not a method result
|
|
296
|
+
RSpec.describe MyService do
|
|
297
|
+
subject(:response) { described_class.new.call } # Wrong: this is a result
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# GOOD - subject is the object instance
|
|
301
|
+
RSpec.describe MyService do
|
|
302
|
+
subject(:service) { described_class.new }
|
|
303
|
+
|
|
304
|
+
it "returns success" do
|
|
305
|
+
expect(service.call).to be_success
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Note**: Inside a `describe "#method"` block, subject CAN be the method result.
|
|
311
|
+
This is valid because the method IS what's being tested in that scope:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
RSpec.describe Order do
|
|
315
|
+
subject(:order) { described_class.new(items) } # Class-level: the object
|
|
316
|
+
|
|
317
|
+
describe "#total" do
|
|
318
|
+
subject(:total) { order.total } # Method-level: the result is OK
|
|
319
|
+
|
|
320
|
+
it "sums item prices" do
|
|
321
|
+
expect(total).to eq(100)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**2. Multiple subjects**:
|
|
328
|
+
```ruby
|
|
329
|
+
# BAD - ambiguous which is THE subject
|
|
330
|
+
subject(:user) { User.new }
|
|
331
|
+
subject(:admin) { Admin.new }
|
|
332
|
+
|
|
333
|
+
# GOOD - one subject, rest as let
|
|
334
|
+
subject(:user) { User.new }
|
|
335
|
+
let(:admin) { Admin.new }
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**3. Missing subject**:
|
|
339
|
+
```ruby
|
|
340
|
+
# BAD - repeated instantiation
|
|
341
|
+
it "returns 200" do
|
|
342
|
+
expect(Pinger.new.call).to eq(200)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# GOOD - define subject once
|
|
346
|
+
subject(:pinger) { Pinger.new }
|
|
347
|
+
it "returns 200" do
|
|
348
|
+
expect(pinger.call).to eq(200)
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Subject Placement
|
|
353
|
+
|
|
354
|
+
Subject must be first declaration in example group:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
describe UserSerializer do
|
|
358
|
+
subject(:serializer) { described_class.new(user) } # First
|
|
359
|
+
let(:user) { create(:user) } # After subject
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Configuration
|
|
364
|
+
|
|
365
|
+
### RSpec.configure
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
RSpec.configure do |config|
|
|
369
|
+
# Expectations
|
|
370
|
+
config.expect_with :rspec do |expectations|
|
|
371
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
372
|
+
expectations.max_formatted_output_length = 1000
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Mocks
|
|
376
|
+
config.mock_with :rspec do |mocks|
|
|
377
|
+
mocks.verify_partial_doubles = true
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Execution
|
|
381
|
+
config.order = :random
|
|
382
|
+
config.fail_fast = true # or number like 3
|
|
383
|
+
|
|
384
|
+
# Filtering
|
|
385
|
+
config.filter_run_when_matching :focus
|
|
386
|
+
config.filter_run_excluding :slow
|
|
387
|
+
|
|
388
|
+
# Persistence
|
|
389
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
|
390
|
+
|
|
391
|
+
# Output
|
|
392
|
+
config.default_formatter = "doc"
|
|
393
|
+
config.profile_examples = 10
|
|
394
|
+
|
|
395
|
+
# Include helpers
|
|
396
|
+
config.include MyHelpers
|
|
397
|
+
config.include AuthHelpers, type: :request
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Configuration Files
|
|
402
|
+
|
|
403
|
+
Precedence (lowest to highest):
|
|
404
|
+
1. `$XDG_CONFIG_HOME/rspec/options` or `~/.rspec`
|
|
405
|
+
2. `./.rspec`
|
|
406
|
+
3. `./.rspec-local`
|
|
407
|
+
4. Command-line options
|
|
408
|
+
5. `SPEC_OPTS` environment variable
|
|
409
|
+
|
|
410
|
+
## Metadata
|
|
411
|
+
|
|
412
|
+
### Adding Metadata
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
it "does something", :slow, :ui => true do
|
|
416
|
+
# metadata[:slow] = true
|
|
417
|
+
# metadata[:ui] = true
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
RSpec.describe "Group", :integration do
|
|
421
|
+
# metadata[:integration] = true
|
|
422
|
+
end
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Accessing Metadata
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
it "does something" do |example|
|
|
429
|
+
example.metadata[:description] # => "does something"
|
|
430
|
+
example.metadata[:file_path] # => "/path/to/spec.rb"
|
|
431
|
+
end
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### described_class
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
RSpec.describe Widget do
|
|
438
|
+
it "creates instance" do
|
|
439
|
+
widget = described_class.new
|
|
440
|
+
expect(widget).to be_a(Widget)
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Filtering
|
|
446
|
+
|
|
447
|
+
### By Tag
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
rspec --tag slow:true
|
|
451
|
+
rspec --tag ~slow # exclude
|
|
452
|
+
|
|
453
|
+
# In configure:
|
|
454
|
+
config.filter_run_including :foo => :bar
|
|
455
|
+
config.filter_run_excluding :foo => :bar
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### By Description
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
rspec --example "Homepage when logged in"
|
|
462
|
+
rspec -e "Homepage" -e "User" # multiple patterns
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### By Location
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
rspec spec/homepage_spec.rb:14 spec/widgets_spec.rb:40
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Focus Filtering
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
RSpec.configure do |config|
|
|
475
|
+
config.filter_run_when_matching :focus
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
fit "focused example" do # runs only this
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Shared Examples
|
|
483
|
+
|
|
484
|
+
### What Shared Examples Are
|
|
485
|
+
|
|
486
|
+
Shared examples store test assertions (`it` blocks) for reuse across multiple test contexts. Content is only executed when included in another example group.
|
|
487
|
+
|
|
488
|
+
### shared_examples vs shared_context
|
|
489
|
+
|
|
490
|
+
| Feature | `shared_examples` | `shared_context` |
|
|
491
|
+
|---------|-------------------|------------------|
|
|
492
|
+
| Contains | Test assertions (`it` blocks) | Setup (`let`, `before`, helpers) |
|
|
493
|
+
| Purpose | Share behavior tests | Share configuration |
|
|
494
|
+
| Use when | Same behavior across classes | Same initial state needed |
|
|
495
|
+
|
|
496
|
+
### Definition
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
RSpec.shared_examples "a collection" do
|
|
500
|
+
it "responds to each" do
|
|
501
|
+
expect(subject).to respond_to(:each)
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
RSpec.shared_examples "a container" do |item|
|
|
506
|
+
it "contains #{item}" do
|
|
507
|
+
expect(subject).to include(item)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Passing Data INTO Shared Examples
|
|
513
|
+
|
|
514
|
+
**Method 1: Positional Parameters** (compile-time):
|
|
515
|
+
```ruby
|
|
516
|
+
RSpec.shared_examples "measurable" do |expected_size|
|
|
517
|
+
it "has size #{expected_size}" do
|
|
518
|
+
expect(subject.size).to eq(expected_size)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
describe Array do
|
|
523
|
+
subject { [1, 2, 3] }
|
|
524
|
+
it_behaves_like "measurable", 3
|
|
525
|
+
end
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Method 2: Keyword Arguments** (recommended):
|
|
529
|
+
```ruby
|
|
530
|
+
shared_examples "configurable" do |defaults: {}, required: []|
|
|
531
|
+
required.each do |attr|
|
|
532
|
+
it "requires #{attr}" do
|
|
533
|
+
expect(subject.public_send(attr)).to be_present
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
describe Settings do
|
|
539
|
+
it_behaves_like "configurable", required: [:timeout, :retries]
|
|
540
|
+
end
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Method 3: Block for Runtime Context**:
|
|
544
|
+
```ruby
|
|
545
|
+
RSpec.shared_examples "a collection" do
|
|
546
|
+
it "is not empty" do
|
|
547
|
+
expect(collection).not_to be_empty
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
describe Array do
|
|
552
|
+
it_behaves_like "a collection" do
|
|
553
|
+
let(:collection) { [1, 2, 3] } # Defined at runtime
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Accessing Context FROM WITHIN Shared Examples
|
|
559
|
+
|
|
560
|
+
Shared examples can access from including context:
|
|
561
|
+
- `described_class`
|
|
562
|
+
- `subject`
|
|
563
|
+
- `let` definitions
|
|
564
|
+
- Metadata
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
RSpec.shared_examples "timestamped" do
|
|
568
|
+
it "responds to created_at" do
|
|
569
|
+
expect(subject).to respond_to(:created_at)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
it "is instance of described class" do
|
|
573
|
+
expect(subject).to be_a(described_class)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### it_behaves_like vs include_examples
|
|
579
|
+
|
|
580
|
+
| Method | Context | Safety | Output |
|
|
581
|
+
|--------|---------|--------|--------|
|
|
582
|
+
| `it_behaves_like` | Creates nested context | Safe | `behaves like X` |
|
|
583
|
+
| `include_examples` | Merges into current | Risky | Flat |
|
|
584
|
+
|
|
585
|
+
**Use `it_behaves_like`** (default choice):
|
|
586
|
+
```ruby
|
|
587
|
+
describe Array do
|
|
588
|
+
it_behaves_like "a collection" # Creates nested context
|
|
589
|
+
end
|
|
590
|
+
# Output: Array > behaves like a collection > responds to each
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
**Avoid `include_examples`** multiple times:
|
|
594
|
+
```ruby
|
|
595
|
+
# BAD - method conflicts, last let wins
|
|
596
|
+
describe Controller do
|
|
597
|
+
include_examples "user actions", :admin
|
|
598
|
+
include_examples "user actions", :regular # Overrides admin let!
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# GOOD - isolated contexts
|
|
602
|
+
describe Controller do
|
|
603
|
+
it_behaves_like "user actions", :admin
|
|
604
|
+
it_behaves_like "user actions", :regular
|
|
605
|
+
end
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Shared Context
|
|
609
|
+
|
|
610
|
+
Use for shared setup (no assertions):
|
|
611
|
+
|
|
612
|
+
```ruby
|
|
613
|
+
RSpec.shared_context "authenticated user" do
|
|
614
|
+
let(:current_user) { create(:user) }
|
|
615
|
+
before { sign_in(current_user) }
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
RSpec.describe DashboardController do
|
|
619
|
+
include_context "authenticated user"
|
|
620
|
+
|
|
621
|
+
it "shows dashboard" do
|
|
622
|
+
get :index
|
|
623
|
+
expect(response).to be_successful
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Anti-Patterns to Avoid
|
|
629
|
+
|
|
630
|
+
**1. Over-abstraction**:
|
|
631
|
+
```ruby
|
|
632
|
+
# BAD - too much indirection
|
|
633
|
+
shared_context "with user" do
|
|
634
|
+
let(:user) { create(:user, role: role) }
|
|
635
|
+
let(:role) { :member }
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
shared_examples "authorized" do
|
|
639
|
+
it "allows access" do
|
|
640
|
+
expect(user.can_access?).to be true # Where did user come from?
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# GOOD - some repetition is okay for clarity
|
|
645
|
+
describe Admin do
|
|
646
|
+
let(:admin) { create(:user, :admin) }
|
|
647
|
+
|
|
648
|
+
it "allows access" do
|
|
649
|
+
expect(admin.can_access?).to be true
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**2. The Mystery Guest** (RSpec DSL Puzzle):
|
|
655
|
+
```ruby
|
|
656
|
+
# BAD - readers must hunt for definitions
|
|
657
|
+
describe BillingService do
|
|
658
|
+
include_context "user setup"
|
|
659
|
+
include_context "subscription setup"
|
|
660
|
+
|
|
661
|
+
it "charges user" do
|
|
662
|
+
BillingService.process(user) # Where is user defined?
|
|
663
|
+
expect(user.charged?).to be true
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**3. Hidden Dependencies**:
|
|
669
|
+
```ruby
|
|
670
|
+
# BAD - shared example requires specific let names
|
|
671
|
+
shared_examples "sortable" do
|
|
672
|
+
it "sorts items" do
|
|
673
|
+
expect(items.sort).to eq(sorted_items) # Must define items AND sorted_items
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**4. Spec Explosion** (n*m problem):
|
|
679
|
+
Avoid including heavy shared examples in many places. If shared example has 10 assertions and included in 8 classes, you run 80 specs instead of 10+8.
|
|
680
|
+
|
|
681
|
+
### Organization
|
|
682
|
+
|
|
683
|
+
**Same file** (isolated use):
|
|
684
|
+
```ruby
|
|
685
|
+
# spec/models/user_spec.rb
|
|
686
|
+
RSpec.shared_examples "has timestamps" do
|
|
687
|
+
it { is_expected.to respond_to(:created_at) }
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
RSpec.describe User do
|
|
691
|
+
it_behaves_like "has timestamps"
|
|
692
|
+
end
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
**Support directory** (widespread use):
|
|
696
|
+
```
|
|
697
|
+
spec/support/
|
|
698
|
+
shared_contexts/
|
|
699
|
+
authenticated_user.rb
|
|
700
|
+
shared_examples/
|
|
701
|
+
timestampable.rb
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Load in `rails_helper.rb`:
|
|
705
|
+
```ruby
|
|
706
|
+
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### When to Use Shared Examples
|
|
710
|
+
|
|
711
|
+
**Good use cases**:
|
|
712
|
+
- Controller authentication (same auth checks across actions)
|
|
713
|
+
- Interface compliance testing
|
|
714
|
+
- Shared model behaviors (e.g., soft delete)
|
|
715
|
+
|
|
716
|
+
**Avoid when**:
|
|
717
|
+
- Models have unique behaviors
|
|
718
|
+
- Setup complexity exceeds benefit
|
|
719
|
+
- Tests become hard to understand
|
|
720
|
+
|
|
721
|
+
## CLI Options
|
|
722
|
+
|
|
723
|
+
### Common Options
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
rspec # run all in spec/
|
|
727
|
+
rspec spec/models # specific directory
|
|
728
|
+
rspec spec/user_spec.rb # specific file
|
|
729
|
+
rspec spec/user_spec.rb:23 # specific line
|
|
730
|
+
|
|
731
|
+
# Formatting
|
|
732
|
+
rspec --format doc # documentation format
|
|
733
|
+
rspec --format progress # dots (default)
|
|
734
|
+
rspec --format json --out results.json
|
|
735
|
+
|
|
736
|
+
# Execution
|
|
737
|
+
rspec --fail-fast # stop on first failure
|
|
738
|
+
rspec --fail-fast=3 # stop after 3 failures
|
|
739
|
+
rspec --only-failures # re-run only failures
|
|
740
|
+
rspec --next-failure # run next failure
|
|
741
|
+
rspec --order random # randomize
|
|
742
|
+
rspec --seed 1234 # specific seed
|
|
743
|
+
rspec --profile 10 # show 10 slowest
|
|
744
|
+
rspec --dry-run # list without running
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### .rspec File
|
|
748
|
+
|
|
749
|
+
```
|
|
750
|
+
--format documentation
|
|
751
|
+
--color
|
|
752
|
+
--require spec_helper
|
|
753
|
+
--order random
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
## Best Practice Patterns
|
|
757
|
+
|
|
758
|
+
### Context Blocks for States
|
|
759
|
+
|
|
760
|
+
```ruby
|
|
761
|
+
describe "#withdraw" do
|
|
762
|
+
context "with sufficient funds" do
|
|
763
|
+
it "reduces balance" do
|
|
764
|
+
# ...
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
context "with insufficient funds" do
|
|
769
|
+
it "raises error" do
|
|
770
|
+
# ...
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Named Subject in Method Describe
|
|
777
|
+
|
|
778
|
+
```ruby
|
|
779
|
+
describe "#calculate_total" do
|
|
780
|
+
subject(:total) { order.calculate_total }
|
|
781
|
+
|
|
782
|
+
it "sums items" do
|
|
783
|
+
expect(total).to eq(100)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Helper Methods
|
|
789
|
+
|
|
790
|
+
```ruby
|
|
791
|
+
RSpec.describe Order do
|
|
792
|
+
def create_order_with_items(count)
|
|
793
|
+
order = Order.new
|
|
794
|
+
count.times { order.add_item(Item.new) }
|
|
795
|
+
order
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
it "calculates total" do
|
|
799
|
+
order = create_order_with_items(3)
|
|
800
|
+
expect(order.total).to eq(30)
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### Shared Examples for Reusable Behaviors
|
|
806
|
+
|
|
807
|
+
```ruby
|
|
808
|
+
shared_examples "a timestamped model" do
|
|
809
|
+
it { is_expected.to respond_to(:created_at) }
|
|
810
|
+
it { is_expected.to respond_to(:updated_at) }
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
RSpec.describe User do
|
|
814
|
+
it_behaves_like "a timestamped model"
|
|
815
|
+
end
|
|
816
|
+
```
|