rubyn-code 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +620 -0
- data/db/migrations/000_create_schema_migrations.sql +4 -0
- data/db/migrations/001_create_sessions.sql +16 -0
- data/db/migrations/002_create_messages.sql +16 -0
- data/db/migrations/003_create_tasks.sql +17 -0
- data/db/migrations/004_create_task_dependencies.sql +8 -0
- data/db/migrations/005_create_memories.sql +44 -0
- data/db/migrations/006_create_cost_records.sql +16 -0
- data/db/migrations/007_create_hooks.sql +12 -0
- data/db/migrations/008_create_skills_cache.sql +8 -0
- data/db/migrations/009_create_teams.sql +27 -0
- data/db/migrations/010_create_instincts.sql +15 -0
- data/exe/rubyn-code +6 -0
- data/lib/rubyn_code/agent/conversation.rb +193 -0
- data/lib/rubyn_code/agent/loop.rb +517 -0
- data/lib/rubyn_code/agent/loop_detector.rb +78 -0
- data/lib/rubyn_code/auth/oauth.rb +174 -0
- data/lib/rubyn_code/auth/server.rb +126 -0
- data/lib/rubyn_code/auth/token_store.rb +153 -0
- data/lib/rubyn_code/autonomous/daemon.rb +233 -0
- data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
- data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
- data/lib/rubyn_code/background/job.rb +19 -0
- data/lib/rubyn_code/background/notifier.rb +44 -0
- data/lib/rubyn_code/background/worker.rb +146 -0
- data/lib/rubyn_code/cli/app.rb +118 -0
- data/lib/rubyn_code/cli/input_handler.rb +79 -0
- data/lib/rubyn_code/cli/renderer.rb +205 -0
- data/lib/rubyn_code/cli/repl.rb +519 -0
- data/lib/rubyn_code/cli/spinner.rb +100 -0
- data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
- data/lib/rubyn_code/config/defaults.rb +43 -0
- data/lib/rubyn_code/config/project_config.rb +120 -0
- data/lib/rubyn_code/config/settings.rb +127 -0
- data/lib/rubyn_code/context/auto_compact.rb +81 -0
- data/lib/rubyn_code/context/compactor.rb +89 -0
- data/lib/rubyn_code/context/manager.rb +91 -0
- data/lib/rubyn_code/context/manual_compact.rb +87 -0
- data/lib/rubyn_code/context/micro_compact.rb +135 -0
- data/lib/rubyn_code/db/connection.rb +176 -0
- data/lib/rubyn_code/db/migrator.rb +146 -0
- data/lib/rubyn_code/db/schema.rb +106 -0
- data/lib/rubyn_code/hooks/built_in.rb +124 -0
- data/lib/rubyn_code/hooks/registry.rb +99 -0
- data/lib/rubyn_code/hooks/runner.rb +88 -0
- data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
- data/lib/rubyn_code/learning/extractor.rb +191 -0
- data/lib/rubyn_code/learning/injector.rb +138 -0
- data/lib/rubyn_code/learning/instinct.rb +172 -0
- data/lib/rubyn_code/llm/client.rb +218 -0
- data/lib/rubyn_code/llm/message_builder.rb +116 -0
- data/lib/rubyn_code/llm/streaming.rb +203 -0
- data/lib/rubyn_code/mcp/client.rb +139 -0
- data/lib/rubyn_code/mcp/config.rb +83 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
- data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
- data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
- data/lib/rubyn_code/memory/models.rb +62 -0
- data/lib/rubyn_code/memory/search.rb +181 -0
- data/lib/rubyn_code/memory/session_persistence.rb +194 -0
- data/lib/rubyn_code/memory/store.rb +199 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
- data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
- data/lib/rubyn_code/observability/models.rb +29 -0
- data/lib/rubyn_code/observability/token_counter.rb +42 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
- data/lib/rubyn_code/output/diff_renderer.rb +212 -0
- data/lib/rubyn_code/output/formatter.rb +120 -0
- data/lib/rubyn_code/permissions/deny_list.rb +49 -0
- data/lib/rubyn_code/permissions/policy.rb +59 -0
- data/lib/rubyn_code/permissions/prompter.rb +80 -0
- data/lib/rubyn_code/permissions/tier.rb +22 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
- data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
- data/lib/rubyn_code/skills/catalog.rb +70 -0
- data/lib/rubyn_code/skills/document.rb +80 -0
- data/lib/rubyn_code/skills/loader.rb +57 -0
- data/lib/rubyn_code/sub_agents/runner.rb +168 -0
- data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
- data/lib/rubyn_code/tasks/dag.rb +208 -0
- data/lib/rubyn_code/tasks/manager.rb +212 -0
- data/lib/rubyn_code/tasks/models.rb +31 -0
- data/lib/rubyn_code/teams/mailbox.rb +128 -0
- data/lib/rubyn_code/teams/manager.rb +175 -0
- data/lib/rubyn_code/teams/teammate.rb +38 -0
- data/lib/rubyn_code/tools/background_run.rb +41 -0
- data/lib/rubyn_code/tools/base.rb +84 -0
- data/lib/rubyn_code/tools/bash.rb +81 -0
- data/lib/rubyn_code/tools/bundle_add.rb +53 -0
- data/lib/rubyn_code/tools/bundle_install.rb +41 -0
- data/lib/rubyn_code/tools/compact.rb +57 -0
- data/lib/rubyn_code/tools/db_migrate.rb +52 -0
- data/lib/rubyn_code/tools/edit_file.rb +49 -0
- data/lib/rubyn_code/tools/executor.rb +62 -0
- data/lib/rubyn_code/tools/git_commit.rb +97 -0
- data/lib/rubyn_code/tools/git_diff.rb +61 -0
- data/lib/rubyn_code/tools/git_log.rb +59 -0
- data/lib/rubyn_code/tools/git_status.rb +59 -0
- data/lib/rubyn_code/tools/glob.rb +44 -0
- data/lib/rubyn_code/tools/grep.rb +81 -0
- data/lib/rubyn_code/tools/load_skill.rb +41 -0
- data/lib/rubyn_code/tools/memory_search.rb +77 -0
- data/lib/rubyn_code/tools/memory_write.rb +52 -0
- data/lib/rubyn_code/tools/rails_generate.rb +54 -0
- data/lib/rubyn_code/tools/read_file.rb +38 -0
- data/lib/rubyn_code/tools/read_inbox.rb +64 -0
- data/lib/rubyn_code/tools/registry.rb +48 -0
- data/lib/rubyn_code/tools/review_pr.rb +145 -0
- data/lib/rubyn_code/tools/run_specs.rb +75 -0
- data/lib/rubyn_code/tools/schema.rb +59 -0
- data/lib/rubyn_code/tools/send_message.rb +53 -0
- data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
- data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
- data/lib/rubyn_code/tools/task.rb +148 -0
- data/lib/rubyn_code/tools/web_fetch.rb +108 -0
- data/lib/rubyn_code/tools/web_search.rb +196 -0
- data/lib/rubyn_code/tools/write_file.rb +30 -0
- data/lib/rubyn_code/version.rb +5 -0
- data/lib/rubyn_code.rb +203 -0
- data/skills/code_quality/fits_in_your_head.md +189 -0
- data/skills/code_quality/naming_conventions.md +213 -0
- data/skills/code_quality/null_object.md +205 -0
- data/skills/code_quality/technical_debt.md +135 -0
- data/skills/code_quality/value_objects.md +216 -0
- data/skills/code_quality/yagni.md +176 -0
- data/skills/design_patterns/adapter.md +191 -0
- data/skills/design_patterns/bridge_memento_visitor.md +254 -0
- data/skills/design_patterns/builder.md +158 -0
- data/skills/design_patterns/command.md +126 -0
- data/skills/design_patterns/composite.md +147 -0
- data/skills/design_patterns/decorator.md +204 -0
- data/skills/design_patterns/facade.md +133 -0
- data/skills/design_patterns/factory_method.md +169 -0
- data/skills/design_patterns/iterator.md +116 -0
- data/skills/design_patterns/mediator.md +133 -0
- data/skills/design_patterns/observer.md +177 -0
- data/skills/design_patterns/proxy.md +140 -0
- data/skills/design_patterns/singleton.md +124 -0
- data/skills/design_patterns/state.md +207 -0
- data/skills/design_patterns/strategy.md +127 -0
- data/skills/design_patterns/template_method.md +173 -0
- data/skills/gems/devise.md +365 -0
- data/skills/gems/dry_rb.md +186 -0
- data/skills/gems/factory_bot.md +268 -0
- data/skills/gems/faraday.md +263 -0
- data/skills/gems/graphql_ruby.md +514 -0
- data/skills/gems/pundit.md +446 -0
- data/skills/gems/redis.md +219 -0
- data/skills/gems/rubocop.md +257 -0
- data/skills/gems/sidekiq.md +360 -0
- data/skills/gems/stripe.md +224 -0
- data/skills/minitest/assertions.md +185 -0
- data/skills/minitest/fixtures.md +238 -0
- data/skills/minitest/integration_tests.md +210 -0
- data/skills/minitest/mailers_and_jobs.md +218 -0
- data/skills/minitest/mocking_stubbing.md +202 -0
- data/skills/minitest/service_tests_and_performance.md +246 -0
- data/skills/minitest/structure_and_conventions.md +169 -0
- data/skills/minitest/system_tests.md +237 -0
- data/skills/rails/action_cable.md +160 -0
- data/skills/rails/active_record_basics.md +174 -0
- data/skills/rails/active_storage.md +242 -0
- data/skills/rails/api_design.md +212 -0
- data/skills/rails/associations.md +182 -0
- data/skills/rails/background_jobs.md +212 -0
- data/skills/rails/caching.md +158 -0
- data/skills/rails/callbacks.md +135 -0
- data/skills/rails/concerns_controllers.md +218 -0
- data/skills/rails/concerns_models.md +280 -0
- data/skills/rails/controllers.md +190 -0
- data/skills/rails/engines.md +201 -0
- data/skills/rails/form_objects.md +168 -0
- data/skills/rails/hotwire.md +229 -0
- data/skills/rails/internationalization.md +192 -0
- data/skills/rails/logging.md +198 -0
- data/skills/rails/mailers.md +180 -0
- data/skills/rails/migrations.md +200 -0
- data/skills/rails/multitenancy.md +207 -0
- data/skills/rails/n_plus_one.md +151 -0
- data/skills/rails/presenters.md +244 -0
- data/skills/rails/query_objects.md +177 -0
- data/skills/rails/routing.md +194 -0
- data/skills/rails/scopes.md +187 -0
- data/skills/rails/security.md +233 -0
- data/skills/rails/serializers.md +243 -0
- data/skills/rails/service_objects.md +184 -0
- data/skills/rails/testing_strategy.md +258 -0
- data/skills/rails/validations.md +206 -0
- data/skills/refactoring/code_smells.md +251 -0
- data/skills/refactoring/command_query_separation.md +166 -0
- data/skills/refactoring/encapsulate_collection.md +125 -0
- data/skills/refactoring/extract_class.md +138 -0
- data/skills/refactoring/extract_method.md +185 -0
- data/skills/refactoring/replace_conditional.md +211 -0
- data/skills/refactoring/value_objects.md +246 -0
- data/skills/rspec/build_stubbed.md +199 -0
- data/skills/rspec/factory_design.md +206 -0
- data/skills/rspec/let_vs_let_bang.md +161 -0
- data/skills/rspec/mocking_stubbing.md +209 -0
- data/skills/rspec/request_specs.md +212 -0
- data/skills/rspec/service_specs.md +262 -0
- data/skills/rspec/shared_examples.md +244 -0
- data/skills/rspec/system_specs.md +286 -0
- data/skills/rspec/test_performance.md +215 -0
- data/skills/ruby/blocks_procs_lambdas.md +204 -0
- data/skills/ruby/classes.md +155 -0
- data/skills/ruby/concurrency.md +194 -0
- data/skills/ruby/data_struct_openstruct.md +158 -0
- data/skills/ruby/debugging_profiling.md +204 -0
- data/skills/ruby/enumerable_patterns.md +168 -0
- data/skills/ruby/exception_handling.md +199 -0
- data/skills/ruby/file_io.md +217 -0
- data/skills/ruby/hashes.md +195 -0
- data/skills/ruby/metaprogramming.md +170 -0
- data/skills/ruby/modules.md +210 -0
- data/skills/ruby/pattern_matching.md +177 -0
- data/skills/ruby/regular_expressions.md +166 -0
- data/skills/ruby/result_objects.md +200 -0
- data/skills/ruby/strings.md +177 -0
- data/skills/ruby_project/bundler_dependencies.md +181 -0
- data/skills/ruby_project/cli_tools.md +224 -0
- data/skills/ruby_project/rake_tasks.md +146 -0
- data/skills/ruby_project/structure.md +261 -0
- data/skills/sinatra/application_structure.md +241 -0
- data/skills/sinatra/middleware_and_deployment.md +221 -0
- data/skills/sinatra/testing.md +233 -0
- data/skills/solid/dependency_inversion.md +195 -0
- data/skills/solid/interface_segregation.md +237 -0
- data/skills/solid/liskov_substitution.md +263 -0
- data/skills/solid/open_closed.md +212 -0
- data/skills/solid/single_responsibility.md +183 -0
- metadata +397 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# RSpec: build_stubbed vs build vs create
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Choose the cheapest factory strategy that satisfies the test. Default to `build_stubbed`, fall back to `build`, use `create` only when the test genuinely needs a persisted database record.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
RSpec.describe Order do
|
|
9
|
+
# FASTEST: No database, no save, generates a fake id
|
|
10
|
+
# Use for: testing methods that don't touch the DB
|
|
11
|
+
let(:order) { build_stubbed(:order, total: 100.00) }
|
|
12
|
+
|
|
13
|
+
describe "#discounted_total" do
|
|
14
|
+
it "applies 10% discount" do
|
|
15
|
+
expect(order.discounted_total).to eq(90.00)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "#high_value?" do
|
|
20
|
+
let(:cheap_order) { build_stubbed(:order, total: 50) }
|
|
21
|
+
let(:expensive_order) { build_stubbed(:order, total: 500) }
|
|
22
|
+
|
|
23
|
+
it "returns true for orders over 200" do
|
|
24
|
+
expect(expensive_order.high_value?).to be true
|
|
25
|
+
expect(cheap_order.high_value?).to be false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
RSpec.describe Order do
|
|
33
|
+
# MEDIUM: In-memory object, not saved. Has valid attributes.
|
|
34
|
+
# Use for: testing validations, or when you need to call .save yourself
|
|
35
|
+
let(:order) { build(:order) }
|
|
36
|
+
|
|
37
|
+
describe "validations" do
|
|
38
|
+
it "requires a shipping address" do
|
|
39
|
+
order.shipping_address = nil
|
|
40
|
+
expect(order).not_to be_valid
|
|
41
|
+
expect(order.errors[:shipping_address]).to include("can't be blank")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "is valid with all required attributes" do
|
|
45
|
+
expect(order).to be_valid
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
RSpec.describe Order do
|
|
53
|
+
# SLOWEST: Writes to database. Generates real id, timestamps, etc.
|
|
54
|
+
# Use for: testing scopes, queries, uniqueness, DB constraints, and associations that query
|
|
55
|
+
let(:user) { create(:user) }
|
|
56
|
+
|
|
57
|
+
describe ".recent" do
|
|
58
|
+
let!(:new_order) { create(:order, user: user, created_at: 1.day.ago) }
|
|
59
|
+
let!(:old_order) { create(:order, user: user, created_at: 1.year.ago) }
|
|
60
|
+
|
|
61
|
+
it "returns orders from the last 30 days" do
|
|
62
|
+
expect(user.orders.recent).to eq([new_order])
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe ".total_revenue" do
|
|
67
|
+
before do
|
|
68
|
+
create(:order, user: user, total: 100)
|
|
69
|
+
create(:order, user: user, total: 250)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "sums all order totals" do
|
|
73
|
+
expect(user.orders.total_revenue).to eq(350)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The decision tree:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Does the test need the record in the database?
|
|
83
|
+
├── YES (scopes, queries, uniqueness, associations that query) → create
|
|
84
|
+
└── NO
|
|
85
|
+
├── Does the test call .save, .valid?, or .errors? → build
|
|
86
|
+
└── NO (testing return values, calculations, formatting) → build_stubbed
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Why This Is Good
|
|
90
|
+
|
|
91
|
+
- **Speed.** `build_stubbed` is 10-50x faster than `create`. It skips database writes, transactions, callbacks, and index updates. In a 2,000-spec suite, choosing the right strategy can save 5-10 minutes of run time.
|
|
92
|
+
- **Isolation.** `build_stubbed` tests logic in complete isolation from the database. If the test passes, the method works regardless of database state.
|
|
93
|
+
- **Clearer intent.** When a test uses `create`, it signals "this test depends on the database." When it uses `build_stubbed`, it signals "this test is about pure logic." Readers immediately understand the scope.
|
|
94
|
+
- **Less factory overhead.** `build_stubbed` doesn't trigger `after_create` callbacks or cascade through association chains. A stubbed order doesn't create a real user, real line items, and real products.
|
|
95
|
+
|
|
96
|
+
## Anti-Pattern
|
|
97
|
+
|
|
98
|
+
Using `create` for everything because "it's easier" or "just in case":
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
RSpec.describe Order do
|
|
102
|
+
# SLOW: Every test creates 3+ database records
|
|
103
|
+
let(:user) { create(:user) }
|
|
104
|
+
let(:product) { create(:product) }
|
|
105
|
+
let(:order) { create(:order, user: user) }
|
|
106
|
+
let(:line_item) { create(:line_item, order: order, product: product) }
|
|
107
|
+
|
|
108
|
+
describe "#high_value?" do
|
|
109
|
+
it "returns true over 200" do
|
|
110
|
+
# This test only checks a comparison: total > 200
|
|
111
|
+
# It does NOT need any database records
|
|
112
|
+
order.total = 500
|
|
113
|
+
expect(order.high_value?).to be true
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe "#formatted_total" do
|
|
118
|
+
it "formats as currency" do
|
|
119
|
+
# This test only checks string formatting
|
|
120
|
+
# 4 database records created for zero reason
|
|
121
|
+
expect(order.formatted_total).to eq("$100.00")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Why This Is Bad
|
|
128
|
+
|
|
129
|
+
- **4 INSERT statements for a string formatting test.** The `formatted_total` test checks `sprintf` behavior. It needs zero database interaction, yet it creates a user, product, order, and line item.
|
|
130
|
+
- **Factory chain cascades.** `create(:order)` triggers `create(:user)` via the association. `create(:line_item)` triggers `create(:product)` and `create(:order)`. One `create` can cascade into 5+ INSERTs.
|
|
131
|
+
- **Slower test suite.** Across hundreds of tests, unnecessary `create` calls add up to minutes. A test suite that should run in 30 seconds takes 3 minutes.
|
|
132
|
+
- **Fragile.** Database-backed tests can fail for reasons unrelated to the behavior being tested — unique constraint violations from other test data, unexpected callbacks, or association validation errors.
|
|
133
|
+
|
|
134
|
+
## When To Apply
|
|
135
|
+
|
|
136
|
+
**Use `build_stubbed` when:**
|
|
137
|
+
- Testing instance methods that compute, format, or return values (`#total`, `#display_name`, `#high_value?`)
|
|
138
|
+
- Testing methods that check object state without querying (`#pending?`, `#can_cancel?`)
|
|
139
|
+
- Building objects to pass into service objects or other units under test
|
|
140
|
+
- You need an object with an `id` but don't need it in the database
|
|
141
|
+
|
|
142
|
+
**Use `build` when:**
|
|
143
|
+
- Testing model validations (`.valid?`, `.errors`)
|
|
144
|
+
- Testing `before_validation` or `before_save` callbacks
|
|
145
|
+
- You need to call `.save` in the test and check the result
|
|
146
|
+
- Building an object that will be passed to `create` or `save` explicitly
|
|
147
|
+
|
|
148
|
+
**Use `create` when:**
|
|
149
|
+
- Testing database scopes and queries (`.where`, `.recent`, `.active`)
|
|
150
|
+
- Testing uniqueness validations (need a real record to conflict with)
|
|
151
|
+
- Testing `has_many` / `belongs_to` associations that are loaded via query
|
|
152
|
+
- Testing `after_create` or `after_commit` callbacks
|
|
153
|
+
- Testing code that calls `.reload`
|
|
154
|
+
- Testing counter caches or database-computed columns
|
|
155
|
+
|
|
156
|
+
## When NOT To Apply
|
|
157
|
+
|
|
158
|
+
- Don't overthink it for one-off tests. If a test file has 3 examples and they all need `create`, just use `create`. The optimization matters at scale — hundreds of tests, not three.
|
|
159
|
+
- Don't `build_stubbed` when the method under test calls `.reload`, `.save`, or queries the database. It will raise or return stale data.
|
|
160
|
+
|
|
161
|
+
## Edge Cases
|
|
162
|
+
|
|
163
|
+
**`build_stubbed` and associations:**
|
|
164
|
+
Stubbed associations work for `belongs_to` (the foreign key is set). They don't work for `has_many` queries (no database to query against).
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
order = build_stubbed(:order)
|
|
168
|
+
order.user # Works — returns a stubbed user
|
|
169
|
+
order.line_items # Returns empty collection — no DB to query
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If you need associations, stub them manually or use `build_stubbed` with inline assignment:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
items = build_stubbed_list(:line_item, 3)
|
|
176
|
+
order = build_stubbed(:order)
|
|
177
|
+
allow(order).to receive(:line_items).and_return(items)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**`build_stubbed` and `.persisted?`:**
|
|
181
|
+
Stubbed objects return `true` for `.persisted?` and have a fake `id`. This makes them behave like saved records in most contexts — useful for testing path helpers, serializers, and view rendering.
|
|
182
|
+
|
|
183
|
+
**Testing both validation and persistence:**
|
|
184
|
+
Split into two tests. Validation test uses `build`. Persistence test uses `create`.
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
describe "email" do
|
|
188
|
+
it "validates format" do
|
|
189
|
+
user = build(:user, email: "invalid")
|
|
190
|
+
expect(user).not_to be_valid
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "enforces uniqueness in database" do
|
|
194
|
+
create(:user, email: "taken@example.com")
|
|
195
|
+
duplicate = build(:user, email: "taken@example.com")
|
|
196
|
+
expect(duplicate).not_to be_valid
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
```
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# RSpec: Factory Design
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Design factories to produce valid records with the minimum possible attributes. Use traits for variations. Use sequences for unique fields. Avoid deeply nested association chains and never put business logic in factories.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# spec/factories/users.rb
|
|
9
|
+
FactoryBot.define do
|
|
10
|
+
factory :user do
|
|
11
|
+
sequence(:email) { |n| "user#{n}@example.com" }
|
|
12
|
+
password { "password123" }
|
|
13
|
+
name { "Jane Doe" }
|
|
14
|
+
role { :user }
|
|
15
|
+
plan { :free }
|
|
16
|
+
|
|
17
|
+
trait :admin do
|
|
18
|
+
role { :admin }
|
|
19
|
+
sequence(:email) { |n| "admin#{n}@example.com" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
trait :pro do
|
|
23
|
+
plan { :pro }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
trait :with_api_key do
|
|
27
|
+
after(:create) do |user|
|
|
28
|
+
create(:api_key, user: user)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# spec/factories/orders.rb
|
|
37
|
+
FactoryBot.define do
|
|
38
|
+
factory :order do
|
|
39
|
+
user
|
|
40
|
+
sequence(:reference) { |n| "ORD-#{n.to_s.rjust(6, '0')}" }
|
|
41
|
+
shipping_address { "123 Main St" }
|
|
42
|
+
status { :pending }
|
|
43
|
+
|
|
44
|
+
trait :with_line_items do
|
|
45
|
+
transient do
|
|
46
|
+
item_count { 2 }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
after(:create) do |order, evaluator|
|
|
50
|
+
create_list(:line_item, evaluator.item_count, order: order)
|
|
51
|
+
order.reload
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
trait :shipped do
|
|
56
|
+
status { :shipped }
|
|
57
|
+
shipped_at { 1.day.ago }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
trait :cancelled do
|
|
61
|
+
status { :cancelled }
|
|
62
|
+
cancelled_at { 1.hour.ago }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
trait :high_value do
|
|
66
|
+
total { 500.00 }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# spec/factories/line_items.rb
|
|
74
|
+
FactoryBot.define do
|
|
75
|
+
factory :line_item do
|
|
76
|
+
order
|
|
77
|
+
product
|
|
78
|
+
quantity { 1 }
|
|
79
|
+
unit_price { 10.00 }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Usage in tests:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Minimal — just what the test needs
|
|
88
|
+
user = build_stubbed(:user)
|
|
89
|
+
admin = build_stubbed(:user, :admin)
|
|
90
|
+
pro_user = create(:user, :pro)
|
|
91
|
+
|
|
92
|
+
# Compose traits
|
|
93
|
+
order = create(:order, :shipped, :high_value)
|
|
94
|
+
|
|
95
|
+
# Override specific attributes
|
|
96
|
+
order = create(:order, total: 99.99, user: user)
|
|
97
|
+
|
|
98
|
+
# Use transient attributes
|
|
99
|
+
order = create(:order, :with_line_items, item_count: 5)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Why This Is Good
|
|
103
|
+
|
|
104
|
+
- **Minimal by default.** The base factory creates a valid record with nothing extra. Tests that need specific attributes override them explicitly, making dependencies visible.
|
|
105
|
+
- **Traits are composable.** `:shipped`, `:cancelled`, `:high_value` can be mixed and matched. No need for separate factories like `shipped_order`, `cancelled_order`, `shipped_high_value_order`.
|
|
106
|
+
- **Sequences prevent collisions.** Unique fields use sequences, so tests never fail due to duplicate emails or reference numbers regardless of run order.
|
|
107
|
+
- **Transient attributes control association creation.** `item_count: 5` is clearer than creating 5 line items manually. The complexity is in the factory, not in every test.
|
|
108
|
+
- **Readable test code.** `create(:order, :shipped, :high_value)` reads like a description of what you need. No setup noise.
|
|
109
|
+
|
|
110
|
+
## Anti-Pattern
|
|
111
|
+
|
|
112
|
+
Factories with heavy defaults, deep association chains, and business logic:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
FactoryBot.define do
|
|
116
|
+
factory :order do
|
|
117
|
+
user
|
|
118
|
+
shipping_address { Faker::Address.full_address }
|
|
119
|
+
billing_address { Faker::Address.full_address }
|
|
120
|
+
status { :pending }
|
|
121
|
+
notes { Faker::Lorem.paragraph }
|
|
122
|
+
reference { "ORD-#{SecureRandom.hex(6)}" }
|
|
123
|
+
currency { "USD" }
|
|
124
|
+
tax_rate { 0.08 }
|
|
125
|
+
discount_code { nil }
|
|
126
|
+
ip_address { Faker::Internet.ip_v4_address }
|
|
127
|
+
user_agent { Faker::Internet.user_agent }
|
|
128
|
+
|
|
129
|
+
after(:create) do |order|
|
|
130
|
+
create_list(:line_item, 3, order: order)
|
|
131
|
+
order.update!(
|
|
132
|
+
subtotal: order.line_items.sum(&:total),
|
|
133
|
+
tax: order.line_items.sum(&:total) * 0.08,
|
|
134
|
+
total: order.line_items.sum(&:total) * 1.08
|
|
135
|
+
)
|
|
136
|
+
create(:shipment, order: order)
|
|
137
|
+
create(:payment, order: order, amount: order.total)
|
|
138
|
+
OrderMailer.confirmation(order).deliver_now
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Why This Is Bad
|
|
145
|
+
|
|
146
|
+
- **Every `create(:order)` creates 8+ records.** The order, a user, 3 line items, 3 products (via line items), a shipment, and a payment. A test that just needs an order object now waits for 8+ INSERTs.
|
|
147
|
+
- **Side effects in factories.** `OrderMailer.confirmation(order).deliver_now` runs in tests. Every test that creates an order sends an email. Tests become slow and flaky.
|
|
148
|
+
- **Unnecessary data.** `Faker::Lorem.paragraph` for notes, `Faker::Internet.ip_v4_address` for IP — these slow down factory execution with random generation, and the test almost certainly doesn't care about these values.
|
|
149
|
+
- **Hidden coupling.** The factory calculates subtotal, tax, and total. If the calculation logic changes, the factory breaks — or worse, silently produces wrong data that makes tests pass incorrectly.
|
|
150
|
+
- **Can't use `build_stubbed`.** Heavy `after(:create)` callbacks mean this factory only works with `create`. You're forced into database hits even for tests that don't need them.
|
|
151
|
+
|
|
152
|
+
## When To Apply
|
|
153
|
+
|
|
154
|
+
Always. Every project using FactoryBot should follow these principles from the start:
|
|
155
|
+
|
|
156
|
+
- **Base factory has required fields only.** If the model validates presence of `email` and `name`, the factory sets `email` and `name`. Nothing else gets a default unless it's required for validity.
|
|
157
|
+
- **Use traits for every variation.** Don't add optional fields to the base factory. A shipped order is `create(:order, :shipped)`, not a factory that always sets `shipped_at`.
|
|
158
|
+
- **Sequences for every unique field.** Email, reference numbers, slugs, usernames — anything with a uniqueness validation.
|
|
159
|
+
- **Transient attributes for controlled association creation.** Don't create associations in the base factory. Use traits like `:with_line_items` that are opt-in.
|
|
160
|
+
- **No business logic in factories.** Don't calculate totals, send emails, or trigger service objects. Factories create data — that's it.
|
|
161
|
+
|
|
162
|
+
## When NOT To Apply
|
|
163
|
+
|
|
164
|
+
- **Seed data is different from factories.** `db/seeds.rb` can and should create rich, interconnected data for development. That's not a factory — different purpose, different rules.
|
|
165
|
+
- **Complex setup for integration/system tests.** System tests may need a fully populated order with line items, shipments, and payments. Use a dedicated factory trait or a setup helper — don't bloat the base factory.
|
|
166
|
+
|
|
167
|
+
## Edge Cases
|
|
168
|
+
|
|
169
|
+
**Circular associations:**
|
|
170
|
+
If Order belongs_to User and User has_many Orders, the factory chain can loop. Break the cycle by not auto-creating associations in both directions:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
factory :user do
|
|
174
|
+
# Don't create orders here
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
factory :order do
|
|
178
|
+
user # Creates user, but user factory doesn't create orders
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Factories for STI (Single Table Inheritance):**
|
|
183
|
+
Use inheritance in factories too:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
factory :notification do
|
|
187
|
+
user
|
|
188
|
+
message { "Something happened" }
|
|
189
|
+
|
|
190
|
+
factory :email_notification, class: "EmailNotification" do
|
|
191
|
+
trait :sent do
|
|
192
|
+
sent_at { 1.hour.ago }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
factory :sms_notification, class: "SmsNotification" do
|
|
197
|
+
phone_number { "+15551234567" }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Faker vs static values:**
|
|
203
|
+
Use static values in factories (`name { "Jane Doe" }`), not Faker (`name { Faker::Name.name }`). Faker adds execution time, produces random data that makes test output inconsistent, and occasionally generates values that fail validations (too long, invalid characters). Save Faker for seed data.
|
|
204
|
+
|
|
205
|
+
**Association strategy mismatch:**
|
|
206
|
+
When using `build_stubbed(:order)`, FactoryBot also stubs the `user` association. But if you `create(:line_item)`, it will `create` (not stub) the associated order. Be deliberate about which strategy you use at each level.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# RSpec: let vs let!
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Use `let` (lazy) by default. Use `let!` (eager) only when the record must exist in the database before the example runs, and no example in the group references it directly.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
RSpec.describe Order do
|
|
9
|
+
# CORRECT: Lazy — only created when an example calls `user`
|
|
10
|
+
let(:user) { create(:user) }
|
|
11
|
+
|
|
12
|
+
# CORRECT: Lazy — only created when an example calls `order`
|
|
13
|
+
let(:order) { create(:order, user: user) }
|
|
14
|
+
|
|
15
|
+
describe "#total" do
|
|
16
|
+
# These line items are only created for examples that call `line_items`
|
|
17
|
+
let(:line_items) do
|
|
18
|
+
[
|
|
19
|
+
create(:line_item, order: order, quantity: 2, unit_price: 10.00),
|
|
20
|
+
create(:line_item, order: order, quantity: 1, unit_price: 25.00)
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "calculates from line items" do
|
|
25
|
+
line_items # Trigger creation
|
|
26
|
+
expect(order.reload.total).to eq(45.00)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe ".recent" do
|
|
31
|
+
# CORRECT use of let! — these must exist BEFORE the scope query runs.
|
|
32
|
+
# No example references `old_order` directly, but it must be in the DB
|
|
33
|
+
# for the scope to correctly exclude it.
|
|
34
|
+
let!(:recent_order) { create(:order, created_at: 1.day.ago) }
|
|
35
|
+
let!(:old_order) { create(:order, created_at: 1.year.ago) }
|
|
36
|
+
|
|
37
|
+
it "returns orders from the last 30 days" do
|
|
38
|
+
expect(Order.recent).to include(recent_order)
|
|
39
|
+
expect(Order.recent).not_to include(old_order)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Why This Is Good
|
|
46
|
+
|
|
47
|
+
- **Lazy `let` avoids unnecessary DB hits.** If an example doesn't reference a `let` variable, the record is never created. In a describe block with 10 examples where only 3 need a specific record, you save 7 unnecessary INSERT statements.
|
|
48
|
+
- **Each example is self-documenting.** When you see `let(:order)` used in an example, you know that example needs an order. With `let!`, you have to mentally track "which records exist before every example runs?" even in examples that don't use them.
|
|
49
|
+
- **Faster test suite.** Lazy evaluation means the minimum number of records are created per example. In a large test suite, this compounds into minutes saved.
|
|
50
|
+
- **Memoized per example.** `let` evaluates once per example and caches. Calling `user` three times in one example hits the database once. No need for instance variables.
|
|
51
|
+
|
|
52
|
+
## Anti-Pattern
|
|
53
|
+
|
|
54
|
+
Using `let!` everywhere "just to be safe":
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
RSpec.describe Order do
|
|
58
|
+
let!(:user) { create(:user) }
|
|
59
|
+
let!(:admin) { create(:user, role: :admin) }
|
|
60
|
+
let!(:product) { create(:product) }
|
|
61
|
+
let!(:category) { create(:category) }
|
|
62
|
+
let!(:order) { create(:order, user: user) }
|
|
63
|
+
let!(:line_item) { create(:line_item, order: order, product: product) }
|
|
64
|
+
let!(:shipping_rate) { create(:shipping_rate) }
|
|
65
|
+
|
|
66
|
+
describe "#total" do
|
|
67
|
+
it "calculates correctly" do
|
|
68
|
+
# Only needs order and line_item, but ALL 7 records are created
|
|
69
|
+
expect(order.total).to eq(line_item.quantity * line_item.unit_price)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "#shipped?" do
|
|
74
|
+
it "returns false when pending" do
|
|
75
|
+
# Only needs order, but ALL 7 records are created
|
|
76
|
+
expect(order.shipped?).to be false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Why This Is Bad
|
|
83
|
+
|
|
84
|
+
- **Every example pays for every record.** 7 INSERT statements run before every single example, even if the example only needs 1 record. With 20 examples in this describe block, that's 140 INSERTs instead of ~40.
|
|
85
|
+
- **Hides dependencies.** When everything is `let!`, you can't tell which records an example actually needs by reading it. The implicit "everything exists" makes the test harder to understand and maintain.
|
|
86
|
+
- **Masks missing associations.** If an example works only because `let!(:product)` happens to exist, removing it later breaks the test in a confusing way. With `let`, the dependency is explicit — the example calls `product` or it doesn't.
|
|
87
|
+
- **Factory chain explosion.** If `create(:order)` creates a user, and `create(:line_item)` creates a product and a category, `let!` on all of them creates duplicate records you never asked for.
|
|
88
|
+
|
|
89
|
+
## When To Apply
|
|
90
|
+
|
|
91
|
+
Use `let!` ONLY when ALL of these are true:
|
|
92
|
+
|
|
93
|
+
1. The record must exist in the database BEFORE the example runs
|
|
94
|
+
2. The example doesn't reference the variable directly — it's testing a query/scope that should find (or exclude) the record
|
|
95
|
+
3. There's no other way to trigger the creation
|
|
96
|
+
|
|
97
|
+
The classic case is **testing scopes and queries**:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
describe ".active" do
|
|
101
|
+
let!(:active_user) { create(:user, active: true) }
|
|
102
|
+
let!(:inactive_user) { create(:user, active: false) }
|
|
103
|
+
|
|
104
|
+
it "returns only active users" do
|
|
105
|
+
# Both must exist in DB before User.active runs
|
|
106
|
+
# The example references active_user for the assertion but
|
|
107
|
+
# inactive_user must exist to prove exclusion
|
|
108
|
+
expect(User.active).to eq([active_user])
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## When NOT To Apply
|
|
114
|
+
|
|
115
|
+
- **The example references the variable directly.** If the example calls `order`, use `let` — it will be created on first reference.
|
|
116
|
+
- **You're "not sure if it needs to exist first."** Default to `let`. If the test fails because the record doesn't exist, then switch to `let!` for that specific variable. Don't preemptively use `let!`.
|
|
117
|
+
- **You're setting up context for a single example.** Use inline `create` inside the example instead:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
it "rejects duplicate emails" do
|
|
121
|
+
create(:user, email: "taken@example.com")
|
|
122
|
+
duplicate = build(:user, email: "taken@example.com")
|
|
123
|
+
expect(duplicate).not_to be_valid
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Edge Cases
|
|
128
|
+
|
|
129
|
+
**`let` inside a `before` block:**
|
|
130
|
+
Don't. If you need something to exist before all examples, use `let!` or a `before` block with `create` directly. Calling `let` variables inside `before` works but obscures intent.
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Clear intent
|
|
134
|
+
let!(:admin) { create(:user, role: :admin) }
|
|
135
|
+
|
|
136
|
+
# Also clear
|
|
137
|
+
before { create(:user, role: :admin) }
|
|
138
|
+
|
|
139
|
+
# Confusing — looks lazy but is eager because before forces evaluation
|
|
140
|
+
let(:admin) { create(:user, role: :admin) }
|
|
141
|
+
before { admin }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**`let` with `build_stubbed`:**
|
|
145
|
+
Always prefer `let(:user) { build_stubbed(:user) }` when the test doesn't need a database record. This is even faster than lazy `let` with `create` because no DB hit ever occurs.
|
|
146
|
+
|
|
147
|
+
**Nested `describe` blocks with `let`:**
|
|
148
|
+
Inner `let` overrides outer `let` with the same name. This is useful for testing variations:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
let(:user) { create(:user, plan: :free) }
|
|
152
|
+
|
|
153
|
+
context "with pro plan" do
|
|
154
|
+
let(:user) { create(:user, plan: :pro) }
|
|
155
|
+
it { expect(user.can_export?).to be true }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
context "with free plan" do
|
|
159
|
+
it { expect(user.can_export?).to be false }
|
|
160
|
+
end
|
|
161
|
+
```
|