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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
- data/.github/ISSUE_TEMPLATE/question.yml +90 -0
- data/.github/dependabot.yml +19 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/release.yml +66 -0
- data/.rubocop.yml +52 -0
- data/CHANGELOG.md +94 -0
- data/CLAUDE.md +332 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +580 -0
- data/LICENSE.txt +21 -0
- data/README.md +544 -0
- data/Rakefile +8 -0
- data/lib/generators/claude/agent/agent_generator.rb +71 -0
- data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
- data/lib/generators/claude/command/command_generator.rb +50 -0
- data/lib/generators/claude/command/templates/command.md.tt +28 -0
- data/lib/generators/claude/commands_library/create-pr.md +27 -0
- data/lib/generators/claude/commands_library/dbchange.md +19 -0
- data/lib/generators/claude/commands_library/quality.md +20 -0
- data/lib/generators/claude/commands_library/stimulus.md +19 -0
- data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
- data/lib/generators/claude/install/install_generator.rb +211 -0
- data/lib/generators/claude/install/templates/README.md.tt +59 -0
- data/lib/generators/claude/install/templates/USAGE +28 -0
- data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
- data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
- data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
- data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
- data/lib/generators/claude/rule/rule_generator.rb +175 -0
- data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
- data/lib/generators/claude/rules_library/code-style.md +37 -0
- data/lib/generators/claude/rules_library/database.md +47 -0
- data/lib/generators/claude/rules_library/hotwire.md +56 -0
- data/lib/generators/claude/rules_library/security.md +54 -0
- data/lib/generators/claude/rules_library/testing.md +47 -0
- data/lib/generators/claude/skill/skill_generator.rb +196 -0
- data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
- data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
- data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
- data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
- data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
- data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
- data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
- data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
- data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
- data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
- data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
- data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
- data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
- data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
- data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
- data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
- data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
- data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
- data/lib/generators/claude/views/views_generator.rb +113 -0
- data/lib/rails_claude_skills/railtie.rb +16 -0
- data/lib/rails_claude_skills/version.rb +5 -0
- data/lib/rails_claude_skills.rb +27 -0
- data/sig/rails_claude_skills.rbs +4 -0
- 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
|
+
```
|