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,246 @@
|
|
|
1
|
+
# Minitest: Testing Service Objects
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Test service objects by calling `.call` with real or fixture data. Assert on the result object, side effects (DB changes, jobs enqueued, emails sent), and error conditions. Inject doubles for external dependencies.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# test/services/orders/create_service_test.rb
|
|
9
|
+
require "test_helper"
|
|
10
|
+
|
|
11
|
+
class Orders::CreateServiceTest < ActiveSupport::TestCase
|
|
12
|
+
setup do
|
|
13
|
+
@user = users(:alice)
|
|
14
|
+
@product = products(:widget)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
test "creates an order with valid params" do
|
|
18
|
+
result = Orders::CreateService.call(valid_params, @user)
|
|
19
|
+
|
|
20
|
+
assert result.success?
|
|
21
|
+
assert_instance_of Order, result.order
|
|
22
|
+
assert_equal "pending", result.order.status
|
|
23
|
+
assert_equal @user, result.order.user
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test "creates a database record" do
|
|
27
|
+
assert_difference "Order.count", 1 do
|
|
28
|
+
Orders::CreateService.call(valid_params, @user)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test "sends confirmation email" do
|
|
33
|
+
assert_emails 1 do
|
|
34
|
+
Orders::CreateService.call(valid_params, @user)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test "enqueues warehouse notification" do
|
|
39
|
+
assert_enqueued_with(job: WarehouseNotificationJob) do
|
|
40
|
+
Orders::CreateService.call(valid_params, @user)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
test "returns failure for invalid params" do
|
|
45
|
+
result = Orders::CreateService.call({ shipping_address: "" }, @user)
|
|
46
|
+
|
|
47
|
+
refute result.success?
|
|
48
|
+
assert result.order.errors[:shipping_address].any?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test "does not create record on failure" do
|
|
52
|
+
assert_no_difference "Order.count" do
|
|
53
|
+
Orders::CreateService.call({ shipping_address: "" }, @user)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
test "does not send email on failure" do
|
|
58
|
+
assert_no_emails do
|
|
59
|
+
Orders::CreateService.call({ shipping_address: "" }, @user)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def valid_params
|
|
66
|
+
{
|
|
67
|
+
shipping_address: "123 Main St",
|
|
68
|
+
line_items_attributes: [
|
|
69
|
+
{ product_id: @product.id, quantity: 2 }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Testing services with external dependencies:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# test/services/embeddings/codebase_indexer_test.rb
|
|
80
|
+
require "test_helper"
|
|
81
|
+
|
|
82
|
+
class Embeddings::CodebaseIndexerTest < ActiveSupport::TestCase
|
|
83
|
+
setup do
|
|
84
|
+
@project = projects(:rubyn_project)
|
|
85
|
+
@fake_embedder = FakeEmbedder.new
|
|
86
|
+
@indexer = Embeddings::CodebaseIndexer.new(embedding_client: @fake_embedder)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
test "creates code embeddings for each chunk" do
|
|
90
|
+
file_content = <<~RUBY
|
|
91
|
+
class Order < ApplicationRecord
|
|
92
|
+
def total
|
|
93
|
+
line_items.sum(&:subtotal)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
RUBY
|
|
97
|
+
|
|
98
|
+
assert_difference "@project.code_embeddings.count" do
|
|
99
|
+
@indexer.index_file(@project, "app/models/order.rb", file_content)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
test "stores the file path on each embedding" do
|
|
104
|
+
@indexer.index_file(@project, "app/models/order.rb", "class Order; end")
|
|
105
|
+
|
|
106
|
+
embedding = @project.code_embeddings.last
|
|
107
|
+
assert_equal "app/models/order.rb", embedding.file_path
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
test "stores a file hash for change detection" do
|
|
111
|
+
content = "class Order; end"
|
|
112
|
+
@indexer.index_file(@project, "app/models/order.rb", content)
|
|
113
|
+
|
|
114
|
+
embedding = @project.code_embeddings.last
|
|
115
|
+
assert_equal Digest::SHA256.hexdigest(content), embedding.file_hash
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test "skips unchanged files" do
|
|
119
|
+
content = "class Order; end"
|
|
120
|
+
@indexer.index_file(@project, "app/models/order.rb", content)
|
|
121
|
+
|
|
122
|
+
assert_no_difference "@project.code_embeddings.count" do
|
|
123
|
+
@indexer.index_file(@project, "app/models/order.rb", content)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# test/support/fake_embedder.rb
|
|
129
|
+
class FakeEmbedder
|
|
130
|
+
DIMENSIONS = 1024
|
|
131
|
+
|
|
132
|
+
def embed(texts)
|
|
133
|
+
texts.map { Array.new(DIMENSIONS) { rand(-1.0..1.0) } }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def embed_query(text)
|
|
137
|
+
Array.new(DIMENSIONS) { rand(-1.0..1.0) }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
# Minitest: Test Performance
|
|
143
|
+
|
|
144
|
+
## Pattern
|
|
145
|
+
|
|
146
|
+
Keep the suite fast: use fixtures, parallelize, avoid unnecessary DB writes, and profile regularly.
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# test/test_helper.rb
|
|
150
|
+
class ActiveSupport::TestCase
|
|
151
|
+
# Run tests in parallel across CPU cores
|
|
152
|
+
parallelize(workers: :number_of_processors)
|
|
153
|
+
|
|
154
|
+
# Use transactions (default) — fastest cleanup strategy
|
|
155
|
+
# Each test rolls back, no data persists between tests
|
|
156
|
+
self.use_transactional_tests = true
|
|
157
|
+
|
|
158
|
+
fixtures :all
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Profile slow tests
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Find the 10 slowest tests
|
|
166
|
+
bundle exec rails test --verbose 2>&1 | sort -t= -k2 -rn | head -10
|
|
167
|
+
|
|
168
|
+
# Or use minitest-reporters for detailed timing
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# Gemfile
|
|
173
|
+
group :test do
|
|
174
|
+
gem "minitest-reporters"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# test/test_helper.rb
|
|
178
|
+
require "minitest/reporters"
|
|
179
|
+
Minitest::Reporters.use! [
|
|
180
|
+
Minitest::Reporters::DefaultReporter.new,
|
|
181
|
+
Minitest::Reporters::MeanTimeReporter.new # Tracks average test times
|
|
182
|
+
]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Speed tips
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# FAST: Use fixtures (zero per-test cost)
|
|
189
|
+
test "something with alice" do
|
|
190
|
+
assert users(:alice).valid?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# SLOW: Creating records per test
|
|
194
|
+
test "something with a user" do
|
|
195
|
+
user = User.create!(email: "test@example.com", name: "Test", password: "password")
|
|
196
|
+
assert user.valid?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# FAST: Test pure logic without DB
|
|
200
|
+
test "money arithmetic" do
|
|
201
|
+
price = Money.new(10_00)
|
|
202
|
+
tax = price * 0.08
|
|
203
|
+
|
|
204
|
+
assert_equal Money.new(80), tax
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# FAST: Build without saving when testing validations
|
|
208
|
+
test "requires email" do
|
|
209
|
+
user = User.new(email: nil)
|
|
210
|
+
refute user.valid?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# FAST: Stub slow external calls
|
|
214
|
+
test "handles API timeout" do
|
|
215
|
+
WarehouseApi.stub(:notify, ->(*) { raise Faraday::TimeoutError }) do
|
|
216
|
+
result = Orders::ShipService.call(orders(:pending_order))
|
|
217
|
+
refute result.success?
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Parallel test configuration
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# For system tests or tests that need separate DB state
|
|
226
|
+
class ActiveSupport::TestCase
|
|
227
|
+
parallelize(workers: :number_of_processors)
|
|
228
|
+
|
|
229
|
+
# If parallel tests have DB issues, use:
|
|
230
|
+
parallelize_setup do |worker|
|
|
231
|
+
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{worker}"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Speed Hierarchy
|
|
237
|
+
|
|
238
|
+
1. **Pure Ruby assertions** (no DB) — microseconds
|
|
239
|
+
2. **Fixture reads** — microseconds (data already loaded)
|
|
240
|
+
3. **`User.new` + `.valid?`** — milliseconds (no DB write)
|
|
241
|
+
4. **Single `create!`** — ~1-5ms
|
|
242
|
+
5. **Factory with associations** — ~5-50ms (cascading creates)
|
|
243
|
+
6. **Integration test** — ~10-100ms (full request cycle)
|
|
244
|
+
7. **System test with browser** — ~500ms-5s
|
|
245
|
+
|
|
246
|
+
Optimize by pushing tests as high on this list as possible. Don't use an integration test when a unit test will do. Don't create records when fixtures exist.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Minitest: Test Structure and Conventions
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Minitest ships with Ruby — no extra gems needed. It provides two styles: `Minitest::Test` (classic xUnit) and `Minitest::Spec` (describe/it blocks). Both are fast, simple, and explicit. Choose one style per project and stick with it.
|
|
6
|
+
|
|
7
|
+
### Classic Style (Minitest::Test)
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# test/models/order_test.rb
|
|
11
|
+
require "test_helper"
|
|
12
|
+
|
|
13
|
+
class OrderTest < ActiveSupport::TestCase
|
|
14
|
+
setup do
|
|
15
|
+
@user = users(:alice)
|
|
16
|
+
@order = orders(:pending_order)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
test "calculates total from line items" do
|
|
20
|
+
line_item = LineItem.create!(order: @order, product: products(:widget), quantity: 2, unit_price: 10_00)
|
|
21
|
+
|
|
22
|
+
assert_equal 20_00, @order.reload.total
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
test "requires a shipping address" do
|
|
26
|
+
order = Order.new(user: @user, shipping_address: nil)
|
|
27
|
+
|
|
28
|
+
assert_not order.valid?
|
|
29
|
+
assert_includes order.errors[:shipping_address], "can't be blank"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test "pending? returns true for pending orders" do
|
|
33
|
+
assert_predicate @order, :pending?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "pending? returns false for shipped orders" do
|
|
37
|
+
@order.update!(status: :shipped)
|
|
38
|
+
|
|
39
|
+
refute_predicate @order, :pending?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
test ".recent returns orders from the last 30 days" do
|
|
43
|
+
old_order = Order.create!(user: @user, shipping_address: "123 Main", created_at: 60.days.ago)
|
|
44
|
+
|
|
45
|
+
recent = Order.recent
|
|
46
|
+
|
|
47
|
+
assert_includes recent, @order
|
|
48
|
+
refute_includes recent, old_order
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Spec Style (Minitest::Spec)
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# test/models/order_spec.rb
|
|
57
|
+
require "test_helper"
|
|
58
|
+
|
|
59
|
+
describe Order do
|
|
60
|
+
let(:user) { users(:alice) }
|
|
61
|
+
let(:order) { orders(:pending_order) }
|
|
62
|
+
|
|
63
|
+
describe "#total" do
|
|
64
|
+
it "calculates from line items" do
|
|
65
|
+
LineItem.create!(order: order, product: products(:widget), quantity: 2, unit_price: 10_00)
|
|
66
|
+
|
|
67
|
+
_(order.reload.total).must_equal 20_00
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe "validations" do
|
|
72
|
+
it "requires shipping address" do
|
|
73
|
+
order = Order.new(user: user, shipping_address: nil)
|
|
74
|
+
|
|
75
|
+
_(order).wont_be :valid?
|
|
76
|
+
_(order.errors[:shipping_address]).must_include "can't be blank"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe ".recent" do
|
|
81
|
+
it "excludes orders older than 30 days" do
|
|
82
|
+
old_order = Order.create!(user: user, shipping_address: "123 Main", created_at: 60.days.ago)
|
|
83
|
+
|
|
84
|
+
_(Order.recent).must_include order
|
|
85
|
+
_(Order.recent).wont_include old_order
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### The test_helper.rb
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# test/test_helper.rb
|
|
95
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
96
|
+
require_relative "../config/environment"
|
|
97
|
+
require "rails/test_help"
|
|
98
|
+
require "minitest/autorun"
|
|
99
|
+
require "minitest/pride" # Colorful output
|
|
100
|
+
require "webmock/minitest" # Stub HTTP requests
|
|
101
|
+
|
|
102
|
+
class ActiveSupport::TestCase
|
|
103
|
+
# Run tests in parallel
|
|
104
|
+
parallelize(workers: :number_of_processors)
|
|
105
|
+
|
|
106
|
+
# Use fixtures
|
|
107
|
+
fixtures :all
|
|
108
|
+
|
|
109
|
+
# Shared helpers available in all tests
|
|
110
|
+
def sign_in(user)
|
|
111
|
+
post login_path, params: { email: user.email, password: "password" }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def json_response
|
|
115
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class ActionDispatch::IntegrationTest
|
|
120
|
+
# Helpers for integration tests
|
|
121
|
+
def auth_headers(user)
|
|
122
|
+
{ "Authorization" => "Bearer #{user.api_keys.first.raw_key}" }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Why This Is Good
|
|
128
|
+
|
|
129
|
+
- **Ships with Ruby.** No Gemfile additions, no version conflicts, no `bundle install` waiting. It's always there.
|
|
130
|
+
- **Fast by default.** Minitest is ~5x faster to boot than RSpec. On a 500-test suite, this can save 3-5 seconds per run.
|
|
131
|
+
- **Plain Ruby.** Tests are classes with methods. No DSL magic, no `let` memoization surprises, no hidden context. Everything is explicit.
|
|
132
|
+
- **Fixtures over factories.** Rails fixtures load once, wrap in transactions, and are instant. No N factory creates per test.
|
|
133
|
+
- **Parallel by default.** `parallelize(workers: :number_of_processors)` runs tests across CPU cores out of the box.
|
|
134
|
+
|
|
135
|
+
## Anti-Pattern
|
|
136
|
+
|
|
137
|
+
Overly complex test setup that mimics RSpec patterns instead of using Minitest idioms:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# BAD: Fighting Minitest to write RSpec-style tests
|
|
141
|
+
class OrderTest < ActiveSupport::TestCase
|
|
142
|
+
# Don't try to replicate RSpec's let/subject/context nesting
|
|
143
|
+
def setup
|
|
144
|
+
@company = Company.create!(name: "Acme")
|
|
145
|
+
@user = User.create!(email: "test@example.com", company: @company)
|
|
146
|
+
@product = Product.create!(name: "Widget", price: 10_00, company: @company)
|
|
147
|
+
@order = Order.create!(user: @user, shipping_address: "123 Main")
|
|
148
|
+
@line_item = LineItem.create!(order: @order, product: @product, quantity: 2, unit_price: 10_00)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Every test pays for all 5 creates even if it only needs @user
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Why This Is Bad
|
|
156
|
+
|
|
157
|
+
- **Setup creates everything for every test.** A validation test that only needs an Order still creates Company, User, Product, and LineItem.
|
|
158
|
+
- **Fixtures solve this.** Define the data once in YAML, load it once per suite, wrap in transactions. Zero per-test cost.
|
|
159
|
+
|
|
160
|
+
## When To Apply
|
|
161
|
+
|
|
162
|
+
- **Every Ruby or Rails project.** Minitest is the Rails default. New projects should start with it unless the team has strong RSpec preferences.
|
|
163
|
+
- **When test speed matters.** Minitest boots faster and runs faster. For CI-heavy teams, this compounds.
|
|
164
|
+
- **When simplicity matters.** Junior developers and contributors learn Minitest in minutes. It's just Ruby.
|
|
165
|
+
|
|
166
|
+
## When NOT To Apply
|
|
167
|
+
|
|
168
|
+
- **The team already uses RSpec.** Don't switch mid-project. The cost of rewriting tests exceeds any speed benefit.
|
|
169
|
+
- **You need advanced matchers.** RSpec's `have_attributes`, `change { }.by`, `contain_exactly` are more expressive for complex assertions. Minitest can do the same but with more verbose code.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Minitest: System Tests (Capybara)
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
System tests drive a real browser to test full user journeys — clicking links, filling forms, asserting visible content. They're the most expensive tests but provide the highest confidence that the app works end-to-end.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# test/system/orders_test.rb
|
|
9
|
+
require "application_system_test_case"
|
|
10
|
+
|
|
11
|
+
class OrdersTest < ApplicationSystemTestCase
|
|
12
|
+
setup do
|
|
13
|
+
@user = users(:alice)
|
|
14
|
+
sign_in_as @user
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
test "viewing the orders list" do
|
|
18
|
+
visit orders_path
|
|
19
|
+
|
|
20
|
+
assert_text "Your Orders"
|
|
21
|
+
assert_text orders(:pending_order).reference
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test "creating a new order" do
|
|
25
|
+
visit new_order_path
|
|
26
|
+
|
|
27
|
+
fill_in "Shipping address", with: "789 Elm St, Austin, TX"
|
|
28
|
+
select "Widget", from: "Product"
|
|
29
|
+
fill_in "Quantity", with: "3"
|
|
30
|
+
|
|
31
|
+
click_button "Place Order"
|
|
32
|
+
|
|
33
|
+
assert_text "Order placed"
|
|
34
|
+
assert_text "789 Elm St"
|
|
35
|
+
assert_text "Widget"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test "editing an existing order" do
|
|
39
|
+
visit order_path(orders(:pending_order))
|
|
40
|
+
|
|
41
|
+
click_link "Edit"
|
|
42
|
+
|
|
43
|
+
fill_in "Shipping address", with: "Updated Address"
|
|
44
|
+
click_button "Save"
|
|
45
|
+
|
|
46
|
+
assert_text "Updated"
|
|
47
|
+
assert_text "Updated Address"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
test "cancelling an order" do
|
|
51
|
+
visit order_path(orders(:pending_order))
|
|
52
|
+
|
|
53
|
+
accept_confirm "Are you sure?" do
|
|
54
|
+
click_button "Cancel Order"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
assert_text "Order cancelled"
|
|
58
|
+
assert_text "Cancelled"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
test "searching orders" do
|
|
62
|
+
visit orders_path
|
|
63
|
+
|
|
64
|
+
fill_in "Search", with: orders(:pending_order).reference
|
|
65
|
+
click_button "Search"
|
|
66
|
+
|
|
67
|
+
assert_text orders(:pending_order).reference
|
|
68
|
+
assert_no_text orders(:shipped_order).reference
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
test "pagination" do
|
|
72
|
+
# Create enough orders to trigger pagination
|
|
73
|
+
30.times { |i| Order.create!(user: @user, reference: "ORD-#{i}", shipping_address: "Test", status: :pending, total: 10_00) }
|
|
74
|
+
|
|
75
|
+
visit orders_path
|
|
76
|
+
|
|
77
|
+
assert_selector ".order-row", count: 25 # First page
|
|
78
|
+
click_link "Next"
|
|
79
|
+
assert_selector ".order-row" # Second page has remaining orders
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def sign_in_as(user)
|
|
85
|
+
visit login_path
|
|
86
|
+
fill_in "Email", with: user.email
|
|
87
|
+
fill_in "Password", with: "password"
|
|
88
|
+
click_button "Sign In"
|
|
89
|
+
assert_text "Signed in"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Setup
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# test/application_system_test_case.rb
|
|
98
|
+
require "test_helper"
|
|
99
|
+
|
|
100
|
+
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
|
101
|
+
# Headless Chrome — fast, no browser window pops up
|
|
102
|
+
driven_by :selenium, using: :headless_chrome, screen_size: [1400, 900]
|
|
103
|
+
|
|
104
|
+
# Use visible Chrome for debugging
|
|
105
|
+
# driven_by :selenium, using: :chrome, screen_size: [1400, 900]
|
|
106
|
+
|
|
107
|
+
def take_debug_screenshot
|
|
108
|
+
take_screenshot # Saves to tmp/screenshots/
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Testing Turbo/Hotwire Interactions
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
class TurboOrdersTest < ApplicationSystemTestCase
|
|
117
|
+
setup do
|
|
118
|
+
sign_in_as users(:alice)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
test "inline editing with Turbo Frames" do
|
|
122
|
+
visit orders_path
|
|
123
|
+
|
|
124
|
+
within "##{dom_id(orders(:pending_order))}" do
|
|
125
|
+
click_link "Edit"
|
|
126
|
+
|
|
127
|
+
# The form appears INSIDE the frame (no page navigation)
|
|
128
|
+
fill_in "Shipping address", with: "New Address"
|
|
129
|
+
click_button "Save"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The frame updates in-place
|
|
133
|
+
assert_text "New Address"
|
|
134
|
+
assert_no_selector "form" # Form is gone, replaced with display
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
test "live search with debounce" do
|
|
138
|
+
visit orders_path
|
|
139
|
+
|
|
140
|
+
fill_in "Search", with: orders(:pending_order).reference
|
|
141
|
+
|
|
142
|
+
# Wait for Turbo Frame to update (debounced search)
|
|
143
|
+
assert_text orders(:pending_order).reference
|
|
144
|
+
assert_no_text orders(:shipped_order).reference
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
test "flash messages appear and dismiss" do
|
|
148
|
+
visit new_order_path
|
|
149
|
+
fill_in "Shipping address", with: "123 Main St"
|
|
150
|
+
click_button "Place Order"
|
|
151
|
+
|
|
152
|
+
assert_text "Order placed"
|
|
153
|
+
|
|
154
|
+
# Flash auto-dismisses after a few seconds (Stimulus controller)
|
|
155
|
+
sleep 4
|
|
156
|
+
assert_no_text "Order placed"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Capybara Matchers Cheat Sheet
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# Finding elements
|
|
165
|
+
assert_text "Expected text" # Anywhere on page
|
|
166
|
+
assert_no_text "Should not appear"
|
|
167
|
+
assert_selector "h1", text: "Orders" # CSS selector with text
|
|
168
|
+
assert_selector ".order-row", count: 5 # Exact count
|
|
169
|
+
assert_selector "#order_123" # By ID
|
|
170
|
+
assert_link "Edit" # Link text
|
|
171
|
+
assert_button "Submit" # Button text
|
|
172
|
+
assert_field "Email", with: "alice@example.com" # Input with value
|
|
173
|
+
|
|
174
|
+
# Scoping
|
|
175
|
+
within "#order-form" do
|
|
176
|
+
fill_in "Address", with: "123 Main"
|
|
177
|
+
click_button "Save"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
within_table "orders" do
|
|
181
|
+
assert_text "ORD-001"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Waiting (Capybara auto-waits by default)
|
|
185
|
+
assert_text "Loading complete" # Waits up to Capybara.default_max_wait_time
|
|
186
|
+
|
|
187
|
+
# Force wait for async operations
|
|
188
|
+
assert_selector ".result", wait: 10 # Wait up to 10 seconds
|
|
189
|
+
|
|
190
|
+
# JavaScript interactions
|
|
191
|
+
accept_confirm { click_button "Delete" }
|
|
192
|
+
dismiss_confirm { click_button "Delete" }
|
|
193
|
+
accept_alert { click_link "Dangerous action" }
|
|
194
|
+
|
|
195
|
+
page.execute_script("window.scrollTo(0, document.body.scrollHeight)")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Why This Is Good
|
|
199
|
+
|
|
200
|
+
- **Tests what users see.** "Fill in email, click sign in, see dashboard" — this is the user's actual experience. If this test passes, the feature works.
|
|
201
|
+
- **Catches integration bugs.** JavaScript errors, broken Turbo Frames, missing CSRF tokens, CSS hiding elements — system tests catch what unit tests miss.
|
|
202
|
+
- **Capybara auto-waits.** `assert_text` waits for the text to appear (up to the max wait time). No manual `sleep` needed for most async operations.
|
|
203
|
+
|
|
204
|
+
## Anti-Pattern
|
|
205
|
+
|
|
206
|
+
Too many system tests or testing logic that belongs in unit tests:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# BAD: Testing validation messages in a system test
|
|
210
|
+
test "shows error for blank email" do
|
|
211
|
+
visit registration_path
|
|
212
|
+
fill_in "Email", with: ""
|
|
213
|
+
click_button "Sign Up"
|
|
214
|
+
assert_text "Email can't be blank"
|
|
215
|
+
end
|
|
216
|
+
# This takes 2-3 seconds. A model test takes 2ms.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## When To Apply
|
|
220
|
+
|
|
221
|
+
- **Critical user journeys only.** Sign up, sign in, checkout, key CRUD flows. 10-20 system tests, not 200.
|
|
222
|
+
- **JavaScript-dependent features.** Turbo Frames, Stimulus controllers, live search, modals.
|
|
223
|
+
- **Smoke tests.** One test per major page to verify it loads without errors.
|
|
224
|
+
|
|
225
|
+
## When NOT To Apply
|
|
226
|
+
|
|
227
|
+
- **Validation logic.** Test in model specs (milliseconds vs seconds).
|
|
228
|
+
- **API endpoints.** Test with integration tests — no browser needed.
|
|
229
|
+
- **Every edge case.** System tests for happy paths, unit tests for edge cases.
|
|
230
|
+
- **CI with limited resources.** System tests are 10-100x slower. Keep the count low.
|
|
231
|
+
|
|
232
|
+
## Speed Tips
|
|
233
|
+
|
|
234
|
+
- Use `headless_chrome` (no GUI overhead)
|
|
235
|
+
- Minimize `sleep` calls — rely on Capybara's auto-waiting
|
|
236
|
+
- Share login state across tests in the same class (use `setup`)
|
|
237
|
+
- Keep system test count under 50 for fast CI
|