rails_claude_skills 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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
  5. data/.github/ISSUE_TEMPLATE/question.yml +90 -0
  6. data/.github/dependabot.yml +19 -0
  7. data/.github/workflows/ci.yml +77 -0
  8. data/.github/workflows/release.yml +66 -0
  9. data/.rubocop.yml +52 -0
  10. data/CHANGELOG.md +94 -0
  11. data/CLAUDE.md +332 -0
  12. data/CODE_OF_CONDUCT.md +134 -0
  13. data/CONTRIBUTING.md +580 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +544 -0
  16. data/Rakefile +8 -0
  17. data/lib/generators/claude/agent/agent_generator.rb +71 -0
  18. data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
  19. data/lib/generators/claude/command/command_generator.rb +50 -0
  20. data/lib/generators/claude/command/templates/command.md.tt +28 -0
  21. data/lib/generators/claude/commands_library/create-pr.md +27 -0
  22. data/lib/generators/claude/commands_library/dbchange.md +19 -0
  23. data/lib/generators/claude/commands_library/quality.md +20 -0
  24. data/lib/generators/claude/commands_library/stimulus.md +19 -0
  25. data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
  26. data/lib/generators/claude/install/install_generator.rb +211 -0
  27. data/lib/generators/claude/install/templates/README.md.tt +59 -0
  28. data/lib/generators/claude/install/templates/USAGE +28 -0
  29. data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
  30. data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
  31. data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
  32. data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
  33. data/lib/generators/claude/rule/rule_generator.rb +175 -0
  34. data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
  35. data/lib/generators/claude/rules_library/code-style.md +37 -0
  36. data/lib/generators/claude/rules_library/database.md +47 -0
  37. data/lib/generators/claude/rules_library/hotwire.md +56 -0
  38. data/lib/generators/claude/rules_library/security.md +54 -0
  39. data/lib/generators/claude/rules_library/testing.md +47 -0
  40. data/lib/generators/claude/skill/skill_generator.rb +196 -0
  41. data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
  42. data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
  43. data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
  44. data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
  45. data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
  46. data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
  47. data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
  48. data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
  49. data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
  50. data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
  51. data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
  52. data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
  53. data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
  54. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
  55. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
  56. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
  57. data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
  58. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
  59. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
  60. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
  61. data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
  62. data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
  63. data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
  64. data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
  65. data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
  66. data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
  67. data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
  68. data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
  69. data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
  70. data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
  71. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
  72. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
  73. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
  74. data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
  75. data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
  76. data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
  77. data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
  78. data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
  79. data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
  80. data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
  81. data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
  82. data/lib/generators/claude/views/views_generator.rb +113 -0
  83. data/lib/rails_claude_skills/railtie.rb +16 -0
  84. data/lib/rails_claude_skills/version.rb +5 -0
  85. data/lib/rails_claude_skills.rb +27 -0
  86. data/sig/rails_claude_skills.rbs +4 -0
  87. metadata +199 -0
@@ -0,0 +1,889 @@
1
+ ## Minitest Testing Examples
2
+
3
+ Complete code examples and patterns for Rails testing with Minitest.
4
+
5
+ ---
6
+
7
+ ## Fixtures
8
+
9
+ ### Basic Fixture Structure
10
+
11
+ ```yaml
12
+ # test/fixtures/users.yml
13
+ alice:
14
+ email: alice@example.com
15
+ name: Alice Smith
16
+ role: admin
17
+ created_at: <%= 30.days.ago %>
18
+
19
+ bob:
20
+ email: bob@example.com
21
+ name: Bob Jones
22
+ role: member
23
+ created_at: <%= 7.days.ago %>
24
+ ```
25
+
26
+ ### Fixtures with Associations
27
+
28
+ ```yaml
29
+ # test/fixtures/articles.yml
30
+ published:
31
+ user: alice # references users(:alice)
32
+ title: Published Article
33
+ body: This article is live
34
+ status: published
35
+ published_at: <%= 1.day.ago %>
36
+
37
+ draft:
38
+ user: bob
39
+ title: Draft Article
40
+ body: Work in progress
41
+ status: draft
42
+ published_at: nil
43
+
44
+ # test/fixtures/comments.yml
45
+ first_comment:
46
+ article: published # references articles(:published)
47
+ user: bob
48
+ body: Great article!
49
+ created_at: <%= 1.hour.ago %>
50
+ ```
51
+
52
+ ### ERB in Fixtures
53
+
54
+ ```yaml
55
+ # test/fixtures/posts.yml
56
+ <% 10.times do |i| %>
57
+ post_<%= i %>:
58
+ title: "Post <%= i %>"
59
+ body: "Content for post <%= i %>"
60
+ published_at: <%= i.days.ago %>
61
+ <% end %>
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Model Testing
67
+
68
+ ### Testing Validations
69
+
70
+ ```ruby
71
+ # test/models/article_test.rb
72
+ class ArticleTest < ActiveSupport::TestCase
73
+ test "validates presence of title" do
74
+ article = Article.new(body: "Content")
75
+ assert_not article.valid?
76
+ assert_includes article.errors[:title], "can't be blank"
77
+ end
78
+
79
+ test "validates title length" do
80
+ article = Article.new(title: "a" * 256, body: "Content")
81
+ assert_not article.valid?
82
+ assert_includes article.errors[:title], "is too long"
83
+ end
84
+
85
+ test "validates uniqueness of title" do
86
+ existing = articles(:published)
87
+ duplicate = Article.new(title: existing.title, body: "Different body")
88
+
89
+ assert_not duplicate.valid?
90
+ assert_includes duplicate.errors[:title], "has already been taken"
91
+ end
92
+
93
+ test "valid with all required attributes" do
94
+ article = Article.new(
95
+ title: "Valid Title",
96
+ body: "Valid content",
97
+ user: users(:alice)
98
+ )
99
+ assert article.valid?
100
+ end
101
+ end
102
+ ```
103
+
104
+ ### Testing Associations
105
+
106
+ ```ruby
107
+ class ArticleTest < ActiveSupport::TestCase
108
+ test "belongs to user" do
109
+ article = articles(:published)
110
+ assert_instance_of User, article.user
111
+ assert_equal users(:alice), article.user
112
+ end
113
+
114
+ test "has many comments" do
115
+ article = articles(:published)
116
+ assert_respond_to article, :comments
117
+ assert article.comments.is_a?(ActiveRecord::Associations::CollectionProxy)
118
+ end
119
+
120
+ test "destroys dependent comments when destroyed" do
121
+ article = articles(:published)
122
+ comment_ids = article.comments.pluck(:id)
123
+
124
+ assert_difference "Comment.count", -article.comments.count do
125
+ article.destroy
126
+ end
127
+
128
+ comment_ids.each do |id|
129
+ assert_nil Comment.find_by(id: id)
130
+ end
131
+ end
132
+ end
133
+ ```
134
+
135
+ ### Testing Scopes
136
+
137
+ ```ruby
138
+ class ArticleTest < ActiveSupport::TestCase
139
+ test ".published returns only published articles" do
140
+ published = articles(:published)
141
+ draft = articles(:draft)
142
+
143
+ results = Article.published
144
+
145
+ assert_includes results, published
146
+ assert_not_includes results, draft
147
+ end
148
+
149
+ test ".recent orders by created_at desc" do
150
+ articles = Article.recent.to_a
151
+ assert_equal articles, articles.sort_by(&:created_at).reverse
152
+ end
153
+
154
+ test ".by_user filters articles by user" do
155
+ alice = users(:alice)
156
+ alice_articles = Article.by_user(alice)
157
+
158
+ alice_articles.each do |article|
159
+ assert_equal alice, article.user
160
+ end
161
+ end
162
+ end
163
+ ```
164
+
165
+ ### Testing Callbacks
166
+
167
+ ```ruby
168
+ class ArticleTest < ActiveSupport::TestCase
169
+ test "sets published_at when status changes to published" do
170
+ article = articles(:draft)
171
+ assert_nil article.published_at
172
+
173
+ article.update(status: :published)
174
+
175
+ assert_not_nil article.published_at
176
+ assert_in_delta Time.current, article.published_at, 2.seconds
177
+ end
178
+
179
+ test "generates slug before validation" do
180
+ article = Article.new(title: "Hello World", body: "Content")
181
+ article.valid?
182
+
183
+ assert_equal "hello-world", article.slug
184
+ end
185
+
186
+ test "sends notification email after create" do
187
+ assert_difference "ActionMailer::Base.deliveries.size", 1 do
188
+ Article.create!(
189
+ title: "New Article",
190
+ body: "Content",
191
+ user: users(:alice)
192
+ )
193
+ end
194
+ end
195
+ end
196
+ ```
197
+
198
+ ### Testing Enums
199
+
200
+ ```ruby
201
+ class ArticleTest < ActiveSupport::TestCase
202
+ test "defines status enum correctly" do
203
+ article = Article.new
204
+
205
+ assert_respond_to article, :status
206
+ assert_respond_to article, :draft?
207
+ assert_respond_to article, :published?
208
+ end
209
+
210
+ test "default status is draft" do
211
+ article = Article.new
212
+ assert article.draft?
213
+ end
214
+
215
+ test "can transition status" do
216
+ article = articles(:draft)
217
+ assert article.draft?
218
+
219
+ article.published!
220
+ assert article.published?
221
+ end
222
+ end
223
+ ```
224
+
225
+ ### Testing Custom Methods
226
+
227
+ ```ruby
228
+ class ArticleTest < ActiveSupport::TestCase
229
+ test "#excerpt returns first 100 characters" do
230
+ long_body = "a" * 200
231
+ article = Article.new(body: long_body)
232
+
233
+ excerpt = article.excerpt
234
+
235
+ assert_equal 100, excerpt.length
236
+ assert excerpt.ends_with?("...")
237
+ end
238
+
239
+ test "#reading_time calculates minutes" do
240
+ words = ("word " * 500).strip # 500 words
241
+ article = Article.new(body: words)
242
+
243
+ # Assuming 200 words per minute
244
+ assert_equal 3, article.reading_time # 500/200 = 2.5 rounded up
245
+ end
246
+
247
+ test "#publish! transitions to published and sets timestamp" do
248
+ article = articles(:draft)
249
+
250
+ article.publish!
251
+
252
+ assert article.published?
253
+ assert_not_nil article.published_at
254
+ end
255
+ end
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Controller Testing
261
+
262
+ ### Testing Index Action
263
+
264
+ ```ruby
265
+ # test/controllers/articles_controller_test.rb
266
+ class ArticlesControllerTest < ActionDispatch::IntegrationTest
267
+ test "GET index returns success" do
268
+ get articles_path
269
+ assert_response :success
270
+ end
271
+
272
+ test "GET index assigns @articles" do
273
+ get articles_path
274
+ assert_not_nil assigns(:articles)
275
+ end
276
+
277
+ test "GET index only shows published articles" do
278
+ get articles_path
279
+
280
+ assert_select "article", count: Article.published.count
281
+ end
282
+ end
283
+ ```
284
+
285
+ ### Testing Show Action
286
+
287
+ ```ruby
288
+ class ArticlesControllerTest < ActionDispatch::IntegrationTest
289
+ test "GET show displays article" do
290
+ article = articles(:published)
291
+ get article_path(article)
292
+
293
+ assert_response :success
294
+ assert_select "h1", text: article.title
295
+ end
296
+
297
+ test "GET show returns 404 for non-existent article" do
298
+ assert_raises ActiveRecord::RecordNotFound do
299
+ get article_path(id: "nonexistent")
300
+ end
301
+ end
302
+ end
303
+ ```
304
+
305
+ ### Testing Create Action
306
+
307
+ ```ruby
308
+ class ArticlesControllerTest < ActionDispatch::IntegrationTest
309
+ setup do
310
+ @user = users(:alice)
311
+ sign_in_as(@user)
312
+ end
313
+
314
+ test "POST create with valid params creates article" do
315
+ assert_difference("Article.count", 1) do
316
+ post articles_path, params: {
317
+ article: {
318
+ title: "New Article",
319
+ body: "Article content here"
320
+ }
321
+ }
322
+ end
323
+
324
+ assert_redirected_to article_path(Article.last)
325
+ follow_redirect!
326
+ assert_select ".notice", text: "Article was successfully created"
327
+ end
328
+
329
+ test "POST create with invalid params renders new" do
330
+ assert_no_difference("Article.count") do
331
+ post articles_path, params: {
332
+ article: { title: "", body: "" }
333
+ }
334
+ end
335
+
336
+ assert_response :unprocessable_entity
337
+ end
338
+
339
+ test "POST create assigns current user as author" do
340
+ post articles_path, params: {
341
+ article: { title: "Test", body: "Content" }
342
+ }
343
+
344
+ assert_equal @user, Article.last.user
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### Testing Update Action
350
+
351
+ ```ruby
352
+ class ArticlesControllerTest < ActionDispatch::IntegrationTest
353
+ setup do
354
+ @article = articles(:draft)
355
+ @user = @article.user
356
+ sign_in_as(@user)
357
+ end
358
+
359
+ test "PATCH update with valid params updates article" do
360
+ patch article_path(@article), params: {
361
+ article: { title: "Updated Title" }
362
+ }
363
+
364
+ assert_redirected_to article_path(@article)
365
+ @article.reload
366
+ assert_equal "Updated Title", @article.title
367
+ end
368
+
369
+ test "PATCH update with invalid params renders edit" do
370
+ patch article_path(@article), params: {
371
+ article: { title: "" }
372
+ }
373
+
374
+ assert_response :unprocessable_entity
375
+ @article.reload
376
+ assert_not_equal "", @article.title
377
+ end
378
+ end
379
+ ```
380
+
381
+ ### Testing Destroy Action
382
+
383
+ ```ruby
384
+ class ArticlesControllerTest < ActionDispatch::IntegrationTest
385
+ setup do
386
+ @article = articles(:draft)
387
+ sign_in_as(@article.user)
388
+ end
389
+
390
+ test "DELETE destroy removes article" do
391
+ assert_difference("Article.count", -1) do
392
+ delete article_path(@article)
393
+ end
394
+
395
+ assert_redirected_to articles_path
396
+ end
397
+ end
398
+ ```
399
+
400
+ ### Testing Authentication
401
+
402
+ ```ruby
403
+ class ArticlesControllerTest < ActionDispatch::IntegrationTest
404
+ test "GET new redirects to login when not authenticated" do
405
+ get new_article_path
406
+ assert_redirected_to login_path
407
+ end
408
+
409
+ test "POST create requires authentication" do
410
+ post articles_path, params: {
411
+ article: { title: "Test", body: "Content" }
412
+ }
413
+
414
+ assert_redirected_to login_path
415
+ end
416
+ end
417
+ ```
418
+
419
+ ### Testing Authorization
420
+
421
+ ```ruby
422
+ class ArticlesControllerTest < ActionDispatch::IntegrationTest
423
+ test "DELETE destroy only allowed by article owner" do
424
+ article = articles(:published) # owned by alice
425
+ other_user = users(:bob)
426
+ sign_in_as(other_user)
427
+
428
+ assert_no_difference("Article.count") do
429
+ delete article_path(article)
430
+ end
431
+
432
+ assert_response :forbidden
433
+ end
434
+
435
+ test "admin can delete any article" do
436
+ article = articles(:published)
437
+ admin = users(:alice) # alice is admin
438
+ sign_in_as(admin)
439
+
440
+ assert_difference("Article.count", -1) do
441
+ delete article_path(article)
442
+ end
443
+ end
444
+ end
445
+ ```
446
+
447
+ ---
448
+
449
+ ## System Testing
450
+
451
+ ### Basic System Test
452
+
453
+ ```ruby
454
+ # test/system/article_creation_test.rb
455
+ class ArticleCreationTest < ApplicationSystemTestCase
456
+ test "user creates new article successfully" do
457
+ visit root_path
458
+ click_on "Sign In"
459
+
460
+ fill_in "Email", with: "alice@example.com"
461
+ fill_in "Password", with: "password"
462
+ click_on "Log In"
463
+
464
+ click_on "New Article"
465
+
466
+ fill_in "Title", with: "My Test Article"
467
+ fill_in "Body", with: "This is the article content"
468
+ select "Published", from: "Status"
469
+
470
+ click_on "Create Article"
471
+
472
+ assert_text "Article was successfully created"
473
+ assert_text "My Test Article"
474
+ end
475
+ end
476
+ ```
477
+
478
+ ### Testing Form Validation
479
+
480
+ ```ruby
481
+ class ArticleCreationTest < ApplicationSystemTestCase
482
+ test "shows validation errors for invalid article" do
483
+ sign_in_as users(:alice)
484
+ visit new_article_path
485
+
486
+ click_on "Create Article"
487
+
488
+ assert_text "Title can't be blank"
489
+ assert_text "Body can't be blank"
490
+ end
491
+ end
492
+ ```
493
+
494
+ ### Testing JavaScript Behavior
495
+
496
+ ```ruby
497
+ class ArticleInteractionTest < ApplicationSystemTestCase
498
+ test "toggles article favorite with JavaScript" do
499
+ sign_in_as users(:alice)
500
+ article = articles(:published)
501
+ visit article_path(article)
502
+
503
+ # Click favorite button
504
+ find("[data-test-id='favorite-button']").click
505
+
506
+ # Wait for JS to complete
507
+ assert_selector "[data-test-id='favorite-button'][aria-pressed='true']"
508
+ end
509
+ end
510
+ ```
511
+
512
+ ### Testing Turbo Frames
513
+
514
+ ```ruby
515
+ class ArticleEditingTest < ApplicationSystemTestCase
516
+ test "edits article inline with Turbo Frame" do
517
+ sign_in_as users(:alice)
518
+ article = articles(:draft)
519
+ visit article_path(article)
520
+
521
+ within "#article_#{article.id}" do
522
+ click_on "Edit"
523
+ fill_in "Title", with: "Updated Title"
524
+ click_on "Update Article"
525
+
526
+ # Turbo Frame replaces content without full page reload
527
+ assert_text "Updated Title"
528
+ end
529
+
530
+ # Page URL hasn't changed
531
+ assert_current_path article_path(article)
532
+ end
533
+ end
534
+ ```
535
+
536
+ ### Testing Turbo Streams
537
+
538
+ ```ruby
539
+ class CommentCreationTest < ApplicationSystemTestCase
540
+ test "adds comment dynamically with Turbo Stream" do
541
+ sign_in_as users(:alice)
542
+ article = articles(:published)
543
+ visit article_path(article)
544
+
545
+ initial_count = article.comments.count
546
+
547
+ fill_in "Comment", with: "Great article!"
548
+ click_on "Post Comment"
549
+
550
+ # Turbo Stream appends new comment without reload
551
+ assert_selector ".comment", count: initial_count + 1
552
+ assert_text "Great article!"
553
+ end
554
+ end
555
+ ```
556
+
557
+ ### Capybara Selectors
558
+
559
+ ```ruby
560
+ class SearchTest < ApplicationSystemTestCase
561
+ test "searches articles with various selectors" do
562
+ visit articles_path
563
+
564
+ # By CSS
565
+ find(".search-input").fill_in with: "rails"
566
+
567
+ # By test ID
568
+ find("[data-test-id='search-submit']").click
569
+
570
+ # By text
571
+ click_on "Rails"
572
+
573
+ # By label
574
+ fill_in "Search", with: "ruby"
575
+
576
+ # Within scope
577
+ within ".search-results" do
578
+ assert_text "Found 5 articles"
579
+ end
580
+
581
+ # By XPath (less preferred)
582
+ find(:xpath, "//input[@name='q']").fill_in with: "test"
583
+ end
584
+ end
585
+ ```
586
+
587
+ ### Testing Modals and Dialogs
588
+
589
+ ```ruby
590
+ class ArticleDeletionTest < ApplicationSystemTestCase
591
+ test "confirms deletion with modal" do
592
+ sign_in_as users(:alice)
593
+ article = articles(:draft)
594
+ visit article_path(article)
595
+
596
+ click_on "Delete Article"
597
+
598
+ # Confirm in modal dialog
599
+ within "#confirmation-modal" do
600
+ assert_text "Are you sure?"
601
+ click_on "Confirm"
602
+ end
603
+
604
+ assert_text "Article was deleted"
605
+ assert_no_text article.title
606
+ end
607
+ end
608
+ ```
609
+
610
+ ---
611
+
612
+ ## Testing Background Jobs
613
+
614
+ ### Testing Job Enqueuing
615
+
616
+ ```ruby
617
+ # test/jobs/article_notification_job_test.rb
618
+ class ArticleNotificationJobTest < ActiveJob::TestCase
619
+ test "enqueues job when article is published" do
620
+ article = articles(:draft)
621
+
622
+ assert_enqueued_with(job: ArticleNotificationJob, args: [article]) do
623
+ article.update(status: :published)
624
+ end
625
+ end
626
+
627
+ test "performs job and sends notification" do
628
+ article = articles(:published)
629
+
630
+ assert_emails 1 do
631
+ ArticleNotificationJob.perform_now(article)
632
+ end
633
+ end
634
+
635
+ test "retries on failure" do
636
+ article = articles(:published)
637
+
638
+ # Stub to raise error
639
+ NotificationMailer.stub :article_published, -> { raise "API Error" } do
640
+ assert_raises "API Error" do
641
+ ArticleNotificationJob.perform_now(article)
642
+ end
643
+ end
644
+
645
+ assert_enqueued_jobs 1, only: ArticleNotificationJob
646
+ end
647
+ end
648
+ ```
649
+
650
+ ---
651
+
652
+ ## Testing Mailers
653
+
654
+ ### Testing Email Delivery
655
+
656
+ ```ruby
657
+ # test/mailers/notification_mailer_test.rb
658
+ class NotificationMailerTest < ActionMailer::TestCase
659
+ test "sends welcome email" do
660
+ user = users(:alice)
661
+ email = NotificationMailer.welcome_email(user)
662
+
663
+ assert_emails 1 do
664
+ email.deliver_now
665
+ end
666
+
667
+ assert_equal [user.email], email.to
668
+ assert_equal ["noreply@example.com"], email.from
669
+ assert_equal "Welcome to the Blog!", email.subject
670
+ end
671
+
672
+ test "includes user name in email body" do
673
+ user = users(:alice)
674
+ email = NotificationMailer.welcome_email(user)
675
+
676
+ assert_match user.name, email.html_part.body.to_s
677
+ assert_match user.name, email.text_part.body.to_s
678
+ end
679
+
680
+ test "enqueues email for async delivery" do
681
+ user = users(:alice)
682
+
683
+ assert_enqueued_with(job: ActionMailer::MailDeliveryJob) do
684
+ NotificationMailer.welcome_email(user).deliver_later
685
+ end
686
+ end
687
+ end
688
+ ```
689
+
690
+ ---
691
+
692
+ ## Test Helpers
693
+
694
+ ### Authentication Helper
695
+
696
+ ```ruby
697
+ # test/test_helper.rb
698
+ class ActionDispatch::IntegrationTest
699
+ def sign_in_as(user, password: "password")
700
+ post login_path, params: {
701
+ email: user.email,
702
+ password: password
703
+ }
704
+ end
705
+
706
+ def sign_out
707
+ delete logout_path
708
+ end
709
+
710
+ def current_user
711
+ User.find_by(id: session[:user_id]) if session[:user_id]
712
+ end
713
+ end
714
+ ```
715
+
716
+ ### System Test Helpers
717
+
718
+ ```ruby
719
+ # test/application_system_test_case.rb
720
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
721
+ driven_by :selenium, using: :headless_chrome
722
+
723
+ def sign_in_as(user)
724
+ visit login_path
725
+ fill_in "Email", with: user.email
726
+ fill_in "Password", with: "password"
727
+ click_on "Log In"
728
+ end
729
+
730
+ def wait_for_turbo(timeout: 2)
731
+ if has_css?(".turbo-progress-bar", visible: true, wait: 0.25.seconds)
732
+ has_no_css?(".turbo-progress-bar", wait: timeout)
733
+ end
734
+ end
735
+ end
736
+ ```
737
+
738
+ ### Custom Assertions
739
+
740
+ ```ruby
741
+ # test/test_helper.rb
742
+ module ActiveSupport
743
+ class TestCase
744
+ def assert_valid(record, message = nil)
745
+ msg = message || "Expected #{record.class} to be valid, errors: #{record.errors.full_messages.join(', ')}"
746
+ assert record.valid?, msg
747
+ end
748
+
749
+ def assert_invalid(record, attribute = nil)
750
+ assert_not record.valid?, "Expected #{record.class} to be invalid"
751
+ if attribute
752
+ assert_not_empty record.errors[attribute], "Expected errors on #{attribute}"
753
+ end
754
+ end
755
+
756
+ def assert_enqueued_email_to(recipient, &block)
757
+ jobs_before = enqueued_jobs.count
758
+ block.call
759
+ jobs_after = enqueued_jobs.count
760
+
761
+ assert jobs_after > jobs_before, "Expected email to be enqueued"
762
+
763
+ job = enqueued_jobs.last
764
+ assert_equal recipient, job[:args].first["arguments"].first["to"]
765
+ end
766
+ end
767
+ end
768
+ ```
769
+
770
+ ---
771
+
772
+ ## Mocking and Stubbing
773
+
774
+ ### Using Minitest::Mock
775
+
776
+ ```ruby
777
+ class PaymentServiceTest < ActiveSupport::TestCase
778
+ test "processes payment through external API" do
779
+ mock_gateway = Minitest::Mock.new
780
+ mock_gateway.expect :charge, true, [100, "USD"]
781
+
782
+ service = PaymentService.new(gateway: mock_gateway)
783
+ result = service.process(amount: 100, currency: "USD")
784
+
785
+ assert result
786
+ mock_gateway.verify
787
+ end
788
+ end
789
+ ```
790
+
791
+ ### Stubbing Methods
792
+
793
+ ```ruby
794
+ class ArticleTest < ActiveSupport::TestCase
795
+ test "publishes to social media" do
796
+ article = articles(:published)
797
+
798
+ article.stub :post_to_twitter, true do
799
+ article.stub :post_to_facebook, true do
800
+ result = article.share_on_social_media
801
+
802
+ assert result
803
+ end
804
+ end
805
+ end
806
+ end
807
+ ```
808
+
809
+ ### Stubbing Class Methods
810
+
811
+ ```ruby
812
+ class WeatherServiceTest < ActiveSupport::TestCase
813
+ test "fetches weather from API" do
814
+ WeatherAPI.stub :fetch, { temp: 72, condition: "Sunny" } do
815
+ result = WeatherService.current_weather("New York")
816
+
817
+ assert_equal 72, result[:temp]
818
+ assert_equal "Sunny", result[:condition]
819
+ end
820
+ end
821
+ end
822
+ ```
823
+
824
+ ---
825
+
826
+ ## Parallel Testing
827
+
828
+ ### Setup for Parallel Tests
829
+
830
+ ```ruby
831
+ # test/test_helper.rb
832
+ class ActiveSupport::TestCase
833
+ # Ensure tests can run in parallel
834
+ parallelize(workers: :number_of_processors)
835
+
836
+ # Use separate databases for parallel workers
837
+ parallelize_setup do |worker|
838
+ SimpleCov.command_name "#{SimpleCov.command_name}-#{worker}"
839
+ end
840
+
841
+ parallelize_teardown do |worker|
842
+ SimpleCov.result
843
+ end
844
+ end
845
+ ```
846
+
847
+ ### Running Parallel Tests
848
+
849
+ ```bash
850
+ # Run tests in parallel
851
+ bin/rails test
852
+
853
+ # Disable parallel for debugging
854
+ PARALLEL_WORKERS=1 bin/rails test
855
+
856
+ # Specify number of workers
857
+ PARALLEL_WORKERS=4 bin/rails test
858
+ ```
859
+
860
+ ---
861
+
862
+ ## Coverage and Reporting
863
+
864
+ ### SimpleCov Setup
865
+
866
+ ```ruby
867
+ # test/test_helper.rb
868
+ require "simplecov"
869
+ SimpleCov.start "rails" do
870
+ add_filter "/test/"
871
+ add_filter "/config/"
872
+ add_group "Models", "app/models"
873
+ add_group "Controllers", "app/controllers"
874
+ add_group "Jobs", "app/jobs"
875
+ add_group "Mailers", "app/mailers"
876
+ end
877
+
878
+ ENV["RAILS_ENV"] ||= "test"
879
+ require_relative "../config/environment"
880
+ require "rails/test_help"
881
+ ```
882
+
883
+ ```bash
884
+ # Run tests with coverage
885
+ COVERAGE=true bin/rails test
886
+
887
+ # View coverage report
888
+ open coverage/index.html
889
+ ```