├── .circleci
└── config.yml
├── .github
├── dependabot.yml
└── workflows
│ └── spell_checking.yml
├── .gitignore
├── README.adoc
└── codespell.txt
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | docs-build:
5 | docker:
6 | - image: ruby:2.6
7 | steps:
8 | - checkout
9 | - run:
10 | name: Install AsciiDoctor & Rouge
11 | command: |
12 | gem install asciidoctor
13 | gem install rouge -v 3.3.0
14 | - run:
15 | name: Build Site
16 | command: asciidoctor -a toc="left" -a toclevels=2 README.adoc -o _build/html/index.html
17 | - persist_to_workspace:
18 | root: _build
19 | paths: html
20 | docs-deploy:
21 | docker:
22 | - image: node:8.10.0
23 | steps:
24 | - checkout
25 | - attach_workspace:
26 | at: _build
27 | - run:
28 | name: Disable jekyll builds
29 | command: touch _build/html/.nojekyll
30 | - run:
31 | name: Install and configure dependencies
32 | command: |
33 | npm install -g --silent gh-pages@2.0.1
34 | git config user.email "ci-build@rubystyle.guide"
35 | git config user.name "ci-build"
36 | - add_ssh_keys:
37 | fingerprints:
38 | - "9a:e0:0d:71:48:ff:32:4b:0a:60:94:ae:e2:6a:44:1e"
39 | - run:
40 | name: Deploy docs to gh-pages branch
41 | command: gh-pages -add --dotfiles --message "[skip ci] Update site" --dist _build/html
42 |
43 | workflows:
44 | version: 2
45 | build:
46 | jobs:
47 | - docs-build
48 | - docs-deploy:
49 | requires:
50 | - docs-build
51 | filters:
52 | branches:
53 | only: master
54 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'github-actions'
4 | directory: '/'
5 | schedule:
6 | interval: 'weekly'
7 |
--------------------------------------------------------------------------------
/.github/workflows/spell_checking.yml:
--------------------------------------------------------------------------------
1 | name: Spell Checking
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | codespell:
7 | name: Check spelling with codespell
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python-version: [3.8]
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v5
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install codespell
22 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
23 | - name: Check spelling with codespell
24 | run: codespell --ignore-words=codespell.txt || exit 1
25 | misspell:
26 | name: Check spelling with misspell
27 | runs-on: ubuntu-latest
28 | steps:
29 | - uses: actions/checkout@v4
30 | - name: Install
31 | run: wget -O - -q https://git.io/misspell | sh -s -- -b .
32 | - name: Misspell
33 | run: ./misspell -error
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | README.html
2 | README.pdf
3 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | = RSpec Style Guide
2 | :idprefix:
3 | :idseparator: -
4 | :sectanchors:
5 | :sectlinks:
6 | :toc: preamble
7 | :toclevels: 1
8 | ifndef::backend-pdf[]
9 | :toc-title: pass:[
Table of Contents
]
10 | endif::[]
11 | :source-highlighter: rouge
12 |
13 | == Introduction
14 |
15 | [quote, Officer Alex J. Murphy / RoboCop]
16 | ____
17 | Role models are important.
18 | ____
19 |
20 | ifdef::env-github[]
21 | TIP: You can find a beautiful version of this guide with much improved navigation at https://rspec.rubystyle.guide.
22 | endif::[]
23 |
24 | This RSpec style guide outlines the recommended best practices for real-world programmers to write code that can be maintained by other real-world programmers.
25 |
26 | https://github.com/rubocop/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop/rubocop-rspec[`rubocop-rspec`] extension, provides a way to enforce the rules outlined in this guide.
27 |
28 | NOTE: This guide assumes you are using RSpec 3 or later.
29 |
30 | You can generate a PDF copy of this guide using https://asciidoctor.org/docs/asciidoctor-pdf/[AsciiDoctor PDF], and an HTML copy https://asciidoctor.org/docs/convert-documents/#converting-a-document-to-html[with] https://asciidoctor.org/#installation[AsciiDoctor] using the following commands:
31 |
32 | [source,shell]
33 | ----
34 | # Generates README.pdf
35 | asciidoctor-pdf -a allow-uri-read README.adoc
36 |
37 | # Generates README.html
38 | asciidoctor README.adoc
39 | ----
40 |
41 | [TIP]
42 | ====
43 | Install the `rouge` gem to get nice syntax highlighting in the generated document.
44 |
45 | [source,shell]
46 | ----
47 | gem install rouge
48 | ----
49 | ====
50 |
51 | == How to Read This Guide
52 |
53 | The guide is separated into sections based on the different pieces of an entire spec file. There was an attempt to omit all obvious information, if anything is unclear, feel free to open an issue asking for further clarity.
54 |
55 | == A Living Document
56 |
57 | Per the comment above, this guide is a work in progress - some rules are simply lacking thorough examples, but some things in the RSpec world change week by week or month by month.
58 | With that said, as the standard changes this guide is meant to be able to change with it.
59 |
60 | == Layout
61 |
62 | === Empty Lines inside Example Group[[empty-lines-after-describe]]
63 |
64 | Do not leave empty lines after `feature`, `context` or `describe` descriptions.
65 | It doesn't make the code more readable and lowers the value of logical chunks.
66 |
67 | [source,ruby]
68 | ----
69 | # bad
70 | describe Article do
71 |
72 | describe '#summary' do
73 |
74 | context 'when there is a summary' do
75 |
76 | it 'returns the summary' do
77 | # ...
78 | end
79 | end
80 | end
81 | end
82 |
83 | # good
84 | describe Article do
85 | describe '#summary' do
86 | context 'when there is a summary' do
87 | it 'returns the summary' do
88 | # ...
89 | end
90 | end
91 | end
92 | end
93 | ----
94 |
95 | === Empty Line between Example Groups [[empty-lines-between-describes]]
96 |
97 | Leave one empty line between `feature`, `context` or `describe` blocks.
98 | Do not leave empty line after the last such block in a group.
99 |
100 | [source,ruby]
101 | ----
102 | # bad
103 | describe Article do
104 | describe '#summary' do
105 | context 'when there is a summary' do
106 | # ...
107 | end
108 | context 'when there is no summary' do
109 | # ...
110 | end
111 |
112 | end
113 | describe '#comments' do
114 | # ...
115 | end
116 | end
117 |
118 | # good
119 | describe Article do
120 | describe '#summary' do
121 | context 'when there is a summary' do
122 | # ...
123 | end
124 |
125 | context 'when there is no summary' do
126 | # ...
127 | end
128 | end
129 |
130 | describe '#comments' do
131 | # ...
132 | end
133 | end
134 | ----
135 |
136 | === Empty Line After `let`[[empty-lines-after-let]]
137 |
138 | Leave one empty line after `let`, `subject`, and `before`/`after` blocks.
139 |
140 | [source,ruby]
141 | ----
142 | # bad
143 | describe Article do
144 | subject { FactoryBot.create(:some_article) }
145 | describe '#summary' do
146 | # ...
147 | end
148 | end
149 |
150 | # good
151 | describe Article do
152 | subject { FactoryBot.create(:some_article) }
153 |
154 | describe '#summary' do
155 | # ...
156 | end
157 | end
158 | ----
159 |
160 | === Let Grouping
161 |
162 | Only group `let`, `subject` blocks and separate them from `before`/`after` blocks.
163 | It makes the code much more readable.
164 |
165 | [source,ruby]
166 | ----
167 | # bad
168 | describe Article do
169 | subject { FactoryBot.create(:some_article) }
170 | let(:user) { FactoryBot.create(:user) }
171 | before do
172 | # ...
173 | end
174 | after do
175 | # ...
176 | end
177 | describe '#summary' do
178 | # ...
179 | end
180 | end
181 |
182 | # good
183 | describe Article do
184 | subject { FactoryBot.create(:some_article) }
185 | let(:user) { FactoryBot.create(:user) }
186 |
187 | before do
188 | # ...
189 | end
190 |
191 | after do
192 | # ...
193 | end
194 |
195 | describe '#summary' do
196 | # ...
197 | end
198 | end
199 | ----
200 |
201 | === Empty Lines around Examples[[empty-lines-around-it]]
202 |
203 | Leave one empty line around `it`/`specify` blocks. This helps to separate the expectations from their conditional logic (contexts for instance).
204 |
205 | [source,ruby]
206 | ----
207 | # bad
208 | describe '#summary' do
209 | let(:item) { double('something') }
210 |
211 | it 'returns the summary' do
212 | # ...
213 | end
214 | it 'does something else' do
215 | # ...
216 | end
217 | it 'does another thing' do
218 | # ...
219 | end
220 | end
221 |
222 | # good
223 | describe '#summary' do
224 | let(:item) { double('something') }
225 |
226 | it 'returns the summary' do
227 | # ...
228 | end
229 |
230 | it 'does something else' do
231 | # ...
232 | end
233 |
234 | it 'does another thing' do
235 | # ...
236 | end
237 | end
238 | ----
239 |
240 | == Example Group Structure
241 |
242 | === Leading `subject`
243 |
244 | When `subject` is used, it should be the first declaration in the example group.
245 |
246 | [source,ruby]
247 | ----
248 | # bad
249 | describe Article do
250 | before do
251 | # ...
252 | end
253 |
254 | let(:user) { FactoryBot.create(:user) }
255 | subject { FactoryBot.create(:some_article) }
256 |
257 | describe '#summary' do
258 | # ...
259 | end
260 | end
261 |
262 | # good
263 | describe Article do
264 | subject { FactoryBot.create(:some_article) }
265 | let(:user) { FactoryBot.create(:user) }
266 |
267 | before do
268 | # ...
269 | end
270 |
271 | describe '#summary' do
272 | # ...
273 | end
274 | end
275 | ----
276 |
277 | === Declaring `subject`, `let!`/`let` and `before`/`after` hooks
278 |
279 | When declaring `subject`, `let!`/`let` and `before`/`after` hooks they should be in the following order:
280 |
281 | * `subject`
282 | * `let!`/`let`
283 | * `before`/`after`
284 |
285 | [source,ruby]
286 | ----
287 | # bad
288 | describe Article do
289 | before do
290 | # ...
291 | end
292 |
293 | after do
294 | # ...
295 | end
296 |
297 | let(:user) { FactoryBot.create(:user) }
298 | subject { FactoryBot.create(:some_article) }
299 |
300 | describe '#summary' do
301 | # ...
302 | end
303 | end
304 |
305 | # good
306 | describe Article do
307 | subject { FactoryBot.create(:some_article) }
308 | let(:user) { FactoryBot.create(:user) }
309 |
310 | before do
311 | # ...
312 | end
313 |
314 | after do
315 | # ...
316 | end
317 |
318 | describe '#summary' do
319 | # ...
320 | end
321 | end
322 | ----
323 |
324 | === Use Contexts
325 |
326 | Use contexts to make the tests clear, well organized, and easy to read.
327 |
328 | [source,ruby]
329 | ----
330 | # bad
331 | it 'has 200 status code if logged in' do
332 | expect(response).to respond_with 200
333 | end
334 |
335 | it 'has 401 status code if not logged in' do
336 | expect(response).to respond_with 401
337 | end
338 |
339 | # good
340 | context 'when logged in' do
341 | it { is_expected.to respond_with 200 }
342 | end
343 |
344 | context 'when logged out' do
345 | it { is_expected.to respond_with 401 }
346 | end
347 | ----
348 |
349 | === Context Cases
350 |
351 | `context` blocks should pretty much always have an opposite negative case.
352 | It is a code smell if there is a single context (without a matching negative case), and this code needs refactoring, or may have no purpose.
353 |
354 | [source,ruby]
355 | ----
356 | # bad - needs refactoring
357 | describe '#attributes' do
358 | context 'the returned hash' do
359 | it 'includes the display name' do
360 | # ...
361 | end
362 |
363 | it 'includes the creation time' do
364 | # ...
365 | end
366 | end
367 | end
368 |
369 | # bad - the negative case needs to be tested, but isn't
370 | describe '#attributes' do
371 | context 'when display name is present' do
372 | before do
373 | article.display_name = 'something'
374 | end
375 |
376 | it 'includes the display name' do
377 | # ...
378 | end
379 | end
380 | end
381 |
382 | # good
383 | describe '#attributes' do
384 | subject(:attributes) { article.attributes }
385 | let(:article) { FactoryBot.create(:article) }
386 |
387 | context 'when display name is present' do
388 | before do
389 | article.display_name = 'something'
390 | end
391 |
392 | it { is_expected.to include(display_name: article.display_name) }
393 | end
394 |
395 | context 'when display name is not present' do
396 | before do
397 | article.display_name = nil
398 | end
399 |
400 | it { is_expected.not_to include(:display_name) }
401 | end
402 | end
403 | ----
404 |
405 | === `let` Blocks
406 |
407 | Use `let` and `let!` for data that is used across several examples in an example group.
408 | Use `let!` to define variables even if they are not referenced in some of the examples, e.g. when testing balancing negative cases.
409 | Do not overuse ``let``s for primitive data, find the balance between frequency of use and complexity of the definition.
410 |
411 | [source,ruby]
412 | ----
413 | # bad
414 | it 'finds shortest path' do
415 | tree = Tree.new(1 => 2, 2 => 3, 2 => 6, 3 => 4, 4 => 5, 5 => 6)
416 | expect(dijkstra.shortest_path(tree, from: 1, to: 6)).to eq([1, 2, 6])
417 | end
418 |
419 | it 'finds longest path' do
420 | tree = Tree.new(1 => 2, 2 => 3, 2 => 6, 3 => 4, 4 => 5, 5 => 6)
421 | expect(dijkstra.longest_path(tree, from: 1, to: 6)).to eq([1, 2, 3, 4, 5, 6])
422 | end
423 |
424 | # good
425 | let(:tree) { Tree.new(1 => 2, 2 => 3, 2 => 6, 3 => 4, 4 => 5, 5 => 6) }
426 |
427 | it 'finds shortest path' do
428 | expect(dijkstra.shortest_path(tree, from: 1, to: 6)).to eq([1, 2, 6])
429 | end
430 |
431 | it 'finds longest path' do
432 | expect(dijkstra.longest_path(tree, from: 1, to: 6)).to eq([1, 2, 3, 4, 5, 6])
433 | end
434 | ----
435 |
436 | === Instance Variables
437 |
438 | Use `let` definitions instead of instance variables.
439 |
440 | [source,ruby]
441 | ----
442 | # bad
443 | before { @name = 'John Wayne' }
444 |
445 | it 'reverses a name' do
446 | expect(reverser.reverse(@name)).to eq('enyaW nhoJ')
447 | end
448 |
449 | # good
450 | let(:name) { 'John Wayne' }
451 |
452 | it 'reverses a name' do
453 | expect(reverser.reverse(name)).to eq('enyaW nhoJ')
454 | end
455 | ----
456 |
457 | === Shared Examples
458 |
459 | Use shared examples to reduce code duplication.
460 |
461 | [source,ruby]
462 | ----
463 | # bad
464 | describe 'GET /articles' do
465 | let(:article) { FactoryBot.create(:article, owner: owner) }
466 |
467 | before { page.driver.get '/articles' }
468 |
469 | context 'when user is the owner' do
470 | let(:user) { owner }
471 |
472 | it 'shows all owned articles' do
473 | expect(page.status_code).to be(200)
474 | contains_resource resource
475 | end
476 | end
477 |
478 | context 'when user is an admin' do
479 | let(:user) { FactoryBot.create(:user, :admin) }
480 |
481 | it 'shows all resources' do
482 | expect(page.status_code).to be(200)
483 | contains_resource resource
484 | end
485 | end
486 | end
487 |
488 | # good
489 | describe 'GET /articles' do
490 | let(:article) { FactoryBot.create(:article, owner: owner) }
491 |
492 | before { page.driver.get '/articles' }
493 |
494 | shared_examples 'shows articles' do
495 | it 'shows all related articles' do
496 | expect(page.status_code).to be(200)
497 | contains_resource resource
498 | end
499 | end
500 |
501 | context 'when user is the owner' do
502 | let(:user) { owner }
503 |
504 | include_examples 'shows articles'
505 | end
506 |
507 | context 'when user is an admin' do
508 | let(:user) { FactoryBot.create(:user, :admin) }
509 |
510 | include_examples 'shows articles'
511 | end
512 | end
513 |
514 | # good
515 | describe 'GET /devices' do
516 | let(:resource) { FactoryBot.create(:device, created_from: user) }
517 |
518 | it_behaves_like 'a listable resource'
519 | it_behaves_like 'a paginable resource'
520 | it_behaves_like 'a searchable resource'
521 | it_behaves_like 'a filterable list'
522 | end
523 | ----
524 |
525 | === Redundant `before(:each)`
526 |
527 | Don't specify `:each`/`:example` scope for `before`/`after`/`around` blocks, as it is the default.
528 | Prefer `:example` when explicitly indicating the scope.
529 |
530 | [source,ruby]
531 | ----
532 | # bad
533 | describe '#summary' do
534 | before(:example) do
535 | # ...
536 | end
537 |
538 | # ...
539 | end
540 |
541 | # good
542 | describe '#summary' do
543 | before do
544 | # ...
545 | end
546 |
547 | # ...
548 | end
549 | ----
550 |
551 | === Ambiguous Hook Scope
552 |
553 | Use `:context` instead of the ambiguous `:all` scope in `before`/`after` hooks.
554 |
555 | [source,ruby]
556 | ----
557 | # bad
558 | describe '#summary' do
559 | before(:all) do
560 | # ...
561 | end
562 |
563 | # ...
564 | end
565 |
566 | # good
567 | describe '#summary' do
568 | before(:context) do
569 | # ...
570 | end
571 |
572 | # ...
573 | end
574 | ----
575 |
576 | === Avoid Hooks with `:context` Scope
577 |
578 | Avoid using `before`/`after` with `:context` scope.
579 | Beware of the state leakage between the examples.
580 |
581 | == Example Structure
582 |
583 | === Expectation per Example[[one-expectation]]
584 |
585 | For examples two styles are considered acceptable.
586 | The first variant is separate example for each expectation, which comes with a cost of repeated context initialization.
587 | The second variant is multiple expectations per example with `aggregate_failures` tag set for a group or example.
588 | Use your best judgement in each case, and apply your strategy consistently.
589 |
590 | [source,ruby]
591 | ----
592 | # good - one expectation per example
593 | describe ArticlesController do
594 | #...
595 |
596 | describe 'GET new' do
597 | it 'assigns a new article' do
598 | get :new
599 | expect(assigns[:article]).to be_a(Article)
600 | end
601 |
602 | it 'renders the new article template' do
603 | get :new
604 | expect(response).to render_template :new
605 | end
606 | end
607 | end
608 |
609 | # good - multiple expectations with aggregated failures
610 | describe ArticlesController do
611 | #...
612 |
613 | describe 'GET new', :aggregate_failures do
614 | it 'assigns new article and renders the new article template' do
615 | get :new
616 | expect(assigns[:article]).to be_a(Article)
617 | expect(response).to render_template :new
618 | end
619 | end
620 |
621 | # ...
622 | end
623 | ----
624 |
625 | === Subject
626 |
627 | When several tests relate to the same subject, use `subject` to reduce repetition.
628 |
629 | [source,ruby]
630 | ----
631 | # bad
632 | it { expect(hero.equipment).to be_heavy }
633 | it { expect(hero.equipment).to include 'sword' }
634 |
635 | # good
636 | subject(:equipment) { hero.equipment }
637 |
638 | it { expect(equipment).to be_heavy }
639 | it { expect(equipment).to include 'sword' }
640 | ----
641 |
642 | === Named Subject [[use-subject]]
643 |
644 | Use named `subject` when possible.
645 | Only use anonymous subject declaration when you don't reference it in any tests, e.g. when `is_expected` is used.
646 |
647 | [source,ruby]
648 | ----
649 | # bad
650 | describe Article do
651 | subject { FactoryBot.create(:article) }
652 |
653 | it 'is not published on creation' do
654 | expect(subject).not_to be_published
655 | end
656 | end
657 |
658 | # good
659 | describe Article do
660 | subject { FactoryBot.create(:article) }
661 |
662 | it 'is not published on creation' do
663 | is_expected.not_to be_published
664 | end
665 | end
666 |
667 | # even better
668 | describe Article do
669 | subject(:article) { FactoryBot.create(:article) }
670 |
671 | it 'is not published on creation' do
672 | expect(article).not_to be_published
673 | end
674 | end
675 | ----
676 |
677 | === Subject Naming in Context
678 |
679 | When you reassign subject with different attributes in different contexts, give different names to the subject, so it's easier to see what the actual subject represents.
680 |
681 | [source,ruby]
682 | ----
683 | # bad
684 | describe Article do
685 | context 'when there is an author' do
686 | subject(:article) { FactoryBot.create(:article, author: user) }
687 |
688 | it 'shows other articles by the same author' do
689 | expect(article.related_stories).to include(story1, story2)
690 | end
691 | end
692 |
693 | context 'when the author is anonymous' do
694 | subject(:article) { FactoryBot.create(:article, author: nil) }
695 |
696 | it 'matches stories by title' do
697 | expect(article.related_stories).to include(story3, story4)
698 | end
699 | end
700 | end
701 |
702 | # good
703 | describe Article do
704 | context 'when article has an author' do
705 | subject(:article) { FactoryBot.create(:article, author: user) }
706 |
707 | it 'shows other articles by the same author' do
708 | expect(article.related_stories).to include(story1, story2)
709 | end
710 | end
711 |
712 | context 'when the author is anonymous' do
713 | subject(:guest_article) { FactoryBot.create(:article, author: nil) }
714 |
715 | it 'matches stories by title' do
716 | expect(guest_article.related_stories).to include(story3, story4)
717 | end
718 | end
719 | end
720 | ----
721 |
722 | === Don't Stub Subject
723 |
724 | Don't stub methods of the object under test, it's a code smell and often indicates a bad design of the object itself.
725 |
726 | [source,ruby]
727 | ----
728 | # bad
729 | describe 'Article' do
730 | subject(:article) { Article.new }
731 |
732 | it 'indicates that the author is unknown' do
733 | allow(article).to receive(:author).and_return(nil)
734 | expect(article.description).to include('by an unknown author')
735 | end
736 | end
737 |
738 | # good - with correct subject initialization
739 | describe 'Article' do
740 | subject(:article) { Article.new(author: nil) }
741 |
742 | it 'indicates that the author is unknown' do
743 | expect(article.description).to include('by an unknown author')
744 | end
745 | end
746 |
747 | # good - with better object design
748 | describe 'Article' do
749 | subject(:presenter) { ArticlePresenter.new(article) }
750 | let(:article) { Article.new }
751 |
752 | it 'indicates that the author is unknown' do
753 | allow(article).to receive(:author).and_return(nil)
754 | expect(presenter.description).to include('by an unknown author')
755 | end
756 | end
757 | ----
758 |
759 | === `it` and `specify`
760 |
761 | Use `specify` if the example doesn't have a description, use `it` for examples with descriptions.
762 | An exception is one-line example, where `it` is preferable.
763 | `specify` is also useful when the docstring does not read well off of `it`.
764 |
765 | [source,ruby]
766 | ----
767 | # bad
768 | it do
769 | # ...
770 | end
771 |
772 | specify 'it sends an email' do
773 | # ...
774 | end
775 |
776 | specify { is_expected.to be_truthy }
777 |
778 | it '#do_something is deprecated' do
779 | ...
780 | end
781 |
782 | # good
783 | specify do
784 | # ...
785 | end
786 |
787 | it 'sends an email' do
788 | # ...
789 | end
790 |
791 | it { is_expected.to be_truthy }
792 |
793 | specify '#do_something is deprecated' do
794 | ...
795 | end
796 | ----
797 |
798 | === `it` in Iterators
799 |
800 | Do not write iterators to generate tests.
801 | When another developer adds a feature to one of the items in the iteration, they must then break it out into a separate test - they are forced to edit code that has nothing to do with their pull request.
802 |
803 | [source,ruby]
804 | ----
805 | # bad
806 | [:new, :show, :index].each do |action|
807 | it 'returns 200' do
808 | get action
809 | expect(response).to be_ok
810 | end
811 | end
812 |
813 | # good - more verbose, but better for the future development
814 | describe 'GET new' do
815 | it 'returns 200' do
816 | get :new
817 | expect(response).to be_ok
818 | end
819 | end
820 |
821 | describe 'GET show' do
822 | it 'returns 200' do
823 | get :show
824 | expect(response).to be_ok
825 | end
826 | end
827 |
828 | describe 'GET index' do
829 | it 'returns 200' do
830 | get :index
831 | expect(response).to be_ok
832 | end
833 | end
834 | ----
835 |
836 | === Incidental State
837 |
838 | Avoid incidental state as much as possible.
839 |
840 | [source,ruby]
841 | ----
842 | # bad
843 | it 'publishes the article' do
844 | article.publish
845 |
846 | # Creating another shared Article test object above would cause this
847 | # test to break
848 | expect(Article.count).to eq(2)
849 | end
850 |
851 | # good
852 | it 'publishes the article' do
853 | expect { article.publish }.to change(Article, :count).by(1)
854 | end
855 | ----
856 |
857 | === DRY
858 |
859 | Be careful not to focus on being 'DRY' by moving repeated expectations into a shared environment too early, as this can lead to brittle tests that rely too much on one another.
860 |
861 | In general, it is best to start with doing everything directly in your `it` blocks even if it is duplication and then refactor your tests after you have them working to be a little more DRY.
862 | However, keep in mind that duplication in test suites is NOT frowned upon, in fact it is preferred if it provides easier understanding and reading of a test.
863 |
864 | === Factories
865 |
866 | Use https://github.com/thoughtbot/factory_bot[Factory Bot] to create test data in integration tests.
867 | You should very rarely have to use `ModelName.create` within an integration spec.
868 | Do *not* use fixtures as they are not nearly as maintainable as factories.
869 |
870 | [source,ruby]
871 | ----
872 | # bad
873 | subject(:article) do
874 | Article.create(
875 | title: 'Piccolina',
876 | author: 'John Archer',
877 | published_at: '17 August 2172',
878 | approved: true
879 | )
880 | end
881 |
882 | # good
883 | subject(:article) { FactoryBot.create(:article) }
884 | ----
885 |
886 | NOTE: When talking about unit tests the best practice would be to use neither fixtures nor factories.
887 | Put as much of your domain logic in libraries that can be tested without needing complex, time consuming setup with either factories or fixtures.
888 |
889 | === Needed Data
890 |
891 | Do not load more data than needed to test your code.
892 |
893 | [source,ruby]
894 | ----
895 | # good
896 | RSpec.describe User do
897 | describe ".top" do
898 | subject { described_class.top(2) }
899 |
900 | before { FactoryBot.create_list(:user, 3) }
901 |
902 | it { is_expected.to have(2).items }
903 | end
904 | end
905 | ----
906 |
907 | === Doubles
908 |
909 | Prefer using verifying doubles over normal doubles.
910 |
911 | Verifying doubles are a stricter alternative to normal doubles that provide guarantees, e.g. a failure will be triggered if an invalid method is being stubbed or a method is called with an invalid number of arguments.
912 |
913 | In general, use doubles with more isolated/behavioral tests rather than with integration tests.
914 |
915 | NOTE: There is no justification for turning `verify_partial_doubles` configuration option off.
916 | That will significantly reduce the confidence in partial doubles.
917 |
918 | [source,ruby]
919 | ----
920 | # good - verifying instance double
921 | article = instance_double('Article')
922 | allow(article).to receive(:author).and_return(nil)
923 |
924 | presenter = described_class.new(article)
925 | expect(presenter.title).to include('by an unknown author')
926 |
927 |
928 | # good - verifying object double
929 | article = object_double(Article.new, valid?: true)
930 | expect(article.save).to be true
931 |
932 |
933 | # good - verifying partial double
934 | allow(Article).to receive(:find).with(5).and_return(article)
935 |
936 |
937 | # good - verifying class double
938 | notifier = class_double('Notifier')
939 | expect(notifier).to receive(:notify).with('suspended as')
940 | ----
941 |
942 | NOTE: If you stub a method that could give a false-positive test result, you have gone too far.
943 |
944 | === Dealing with Time
945 |
946 | Always use https://github.com/travisjeffery/timecop[Timecop] instead of stubbing anything on Time or Date.
947 |
948 | [source,ruby]
949 | ----
950 | describe InvoiceReminder do
951 | subject(:time_with_offset) { described_class.new.get_offset_time }
952 |
953 | # bad
954 | it 'offsets the time 2 days into the future' do
955 | current_time = Time.now
956 | allow(Time).to receive(:now).and_return(current_time)
957 | expect(time_with_offset).to eq(current_time + 2.days)
958 | end
959 |
960 | # good
961 | it 'offsets the time 2 days into the future' do
962 | Timecop.freeze(Time.now) do
963 | expect(time_with_offset).to eq 2.days.from_now
964 | end
965 | end
966 | end
967 | ----
968 |
969 | === Stub HTTP Requests
970 |
971 | Stub HTTP requests when the code is making them.
972 | Avoid hitting real external services.
973 |
974 | Use https://github.com/bblimke/webmock[webmock] and https://github.com/vcr/vcr[VCR] separately or https://marnen.github.io/webmock-presentation/webmock.html[together].
975 |
976 | [source,ruby]
977 | ----
978 | # good
979 | context 'with unauthorized access' do
980 | let(:uri) { 'http://api.lelylan.com/types' }
981 |
982 | before { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
983 |
984 | it 'returns access denied' do
985 | page.driver.get uri
986 | expect(page).to have_content 'Access denied'
987 | end
988 | end
989 | ----
990 |
991 | [#declare-constants]
992 | === Declare Constants
993 |
994 | Do not explicitly declare classes, modules, or constants in example groups.
995 | https://rspec.info/features/3-12/rspec-mocks/mutating-constants/[Stub constants instead].
996 |
997 | NOTE: Constants, including classes and modules, when declared in a block scope, are defined in global namespace, and leak between examples.
998 |
999 | [source,ruby]
1000 | ----
1001 | # bad
1002 | describe SomeClass do
1003 | CONSTANT_HERE = 'I leak into global namespace'
1004 | end
1005 |
1006 | # good
1007 | describe SomeClass do
1008 | before do
1009 | stub_const('CONSTANT_HERE', 'I only exist during this example')
1010 | end
1011 | end
1012 |
1013 | # bad
1014 | describe SomeClass do
1015 | class FooClass < described_class
1016 | def double_that
1017 | some_base_method * 2
1018 | end
1019 | end
1020 |
1021 | it { expect(FooClass.new.double_that).to eq(4) }
1022 | end
1023 |
1024 | # good - anonymous class, no constant needs to be defined
1025 | describe SomeClass do
1026 | let(:foo_class) do
1027 | Class.new(described_class) do
1028 | def double_that
1029 | some_base_method * 2
1030 | end
1031 | end
1032 | end
1033 |
1034 | it { expect(foo_class.new.double_that).to eq(4) }
1035 | end
1036 |
1037 | # good - constant is stubbed
1038 | describe SomeClass do
1039 | before do
1040 | foo_class = Class.new(described_class) do
1041 | def do_something
1042 | end
1043 | end
1044 | stub_const('FooClass', foo_class)
1045 | end
1046 |
1047 | it { expect(FooClass.new.double_that).to eq(4) }
1048 | end
1049 | ----
1050 |
1051 | [#implicit-block-expectations]
1052 | === Implicit Block Expectations
1053 |
1054 | Avoid using implicit block expectations.
1055 |
1056 | [source,ruby]
1057 | ----
1058 | # bad
1059 | subject { -> { do_something } }
1060 | it { is_expected.to change(something).to(new_value) }
1061 |
1062 | # good
1063 | it 'changes something to a new value' do
1064 | expect { do_something }.to change(something).to(new_value)
1065 | end
1066 | ----
1067 |
1068 | == Naming
1069 |
1070 | === Context Descriptions
1071 |
1072 | Context descriptions should describe the conditions shared by all the examples within. Full example names (formed by concatenation of all nested block descriptions) should form a readable sentence.
1073 |
1074 | A typical description will be an adjunct phrase starting with 'when', 'with', 'without', or similar words.
1075 |
1076 | [source,ruby]
1077 | ----
1078 | # bad - 'Summary user logged in no display name shows a placeholder'
1079 | describe 'Summary' do
1080 | context 'user logged in' do
1081 | context 'no display name' do
1082 | it 'shows a placeholder' do
1083 | end
1084 | end
1085 | end
1086 | end
1087 |
1088 | # good - 'Summary when the user is logged in when the display name is blank shows a placeholder'
1089 | describe 'Summary' do
1090 | context 'when the user is logged in' do
1091 | context 'when the display name is blank' do
1092 | it 'shows a placeholder' do
1093 | end
1094 | end
1095 | end
1096 | end
1097 | ----
1098 |
1099 | === Example Descriptions
1100 |
1101 | `it`/`specify` block descriptions should never end with a conditional.
1102 | This is a code smell that the `it` most likely needs to be wrapped in a `context`.
1103 |
1104 | [source,ruby]
1105 | ----
1106 | # bad
1107 | it 'returns the display name if it is present' do
1108 | # ...
1109 | end
1110 |
1111 | # good
1112 | context 'when display name is present' do
1113 | it 'returns the display name' do
1114 | # ...
1115 | end
1116 | end
1117 |
1118 | # This encourages the addition of negative test cases that might have
1119 | # been overlooked
1120 | context 'when display name is not present' do
1121 | it 'returns nil' do
1122 | # ...
1123 | end
1124 | end
1125 | ----
1126 |
1127 | === Keep Example Descriptions Short
1128 |
1129 | Keep example description shorter than 60 characters.
1130 |
1131 | Write the example that documents itself, and generates proper
1132 | documentation format output.
1133 |
1134 | [source,ruby]
1135 | ----
1136 | # bad
1137 | it 'rewrites "should not return something" as "does not return something"' do
1138 | # ...
1139 | end
1140 |
1141 | # good
1142 | it 'rewrites "should not return something"' do
1143 | expect(rewrite('should not return something')).to
1144 | eq 'does not return something'
1145 | end
1146 |
1147 | # good - self-documenting
1148 | specify do
1149 | expect(rewrite('should not return something')).to
1150 | eq 'does not return something'
1151 | end
1152 | ----
1153 |
1154 | === "Should" in Example Docstrings[[should-in-it]]
1155 |
1156 | Do not write 'should' or 'should not' in the beginning of your example docstrings.
1157 | The descriptions represent actual functionality, not what might be happening.
1158 | Use the third person in the present tense.
1159 |
1160 | [source,ruby]
1161 | ----
1162 | # bad
1163 | it 'should return the summary' do
1164 | # ...
1165 | end
1166 |
1167 | # good
1168 | it 'returns the summary' do
1169 | # ...
1170 | end
1171 | ----
1172 |
1173 | === Describe the Methods[[example-group-naming]]
1174 |
1175 | Be clear about what method you are describing.
1176 | Use the Ruby documentation convention of `.` when referring to a class method's name and `#` when referring to an instance method's name.
1177 |
1178 | [source,ruby]
1179 | ----
1180 | # bad
1181 | describe 'the authenticate method for User' do
1182 | # ...
1183 | end
1184 |
1185 | describe 'if the user is an admin' do
1186 | # ...
1187 | end
1188 |
1189 | # good
1190 | describe '.authenticate' do
1191 | # ...
1192 | end
1193 |
1194 | describe '#admin?' do
1195 | # ...
1196 | end
1197 | ----
1198 |
1199 | === Use `expect`
1200 |
1201 | Always use the newer `expect` syntax.
1202 |
1203 | Configure RSpec to only accept the new `expect` syntax.
1204 |
1205 | [source,ruby]
1206 | ----
1207 | # bad
1208 | it 'creates a resource' do
1209 | response.should respond_with_content_type(:json)
1210 | end
1211 |
1212 | # good
1213 | it 'creates a resource' do
1214 | expect(response).to respond_with_content_type(:json)
1215 | end
1216 | ----
1217 |
1218 | == Matchers
1219 |
1220 | === Predicate Matchers
1221 |
1222 | Use RSpec's predicate matcher methods when possible.
1223 |
1224 | [source,ruby]
1225 | ----
1226 | describe Article do
1227 | subject(:article) { FactoryBot.create(:article) }
1228 |
1229 | # bad
1230 | it 'is published' do
1231 | expect(article.published?).to be true
1232 | end
1233 |
1234 | # good
1235 | it 'is published' do
1236 | expect(article).to be_published
1237 | end
1238 |
1239 | # even better
1240 | it { is_expected.to be_published }
1241 | end
1242 | ----
1243 |
1244 | === Built in Matchers
1245 |
1246 | Use built-in matchers.
1247 |
1248 | [source,ruby]
1249 | ----
1250 | # bad
1251 | it 'includes a title' do
1252 | expect(article.title.include?('a lengthy title')).to be true
1253 | end
1254 |
1255 | # good
1256 | it 'includes a title' do
1257 | expect(article.title).to include 'a lengthy title'
1258 | end
1259 | ----
1260 |
1261 | === `be` Matcher
1262 |
1263 | Avoid using `be` matcher without arguments.
1264 | It is too generic, as it pass on everything that is not `nil` or `false`.
1265 | If that is the exact intent, use `be_truthy`.
1266 | In all other cases it's better to specify what exactly is the expected value.
1267 |
1268 | [source,ruby]
1269 | ----
1270 | # bad
1271 | it 'has author' do
1272 | expect(article.author).to be
1273 | end
1274 |
1275 | # good
1276 | it 'has author' do
1277 | expect(article.author).to be_truthy # same as the original
1278 | expect(article.author).not_to be_nil # `be` is often used to check for non-nil value
1279 | expect(article.author).to be_an(Author) # explicit check for the type of the value
1280 | end
1281 | ----
1282 |
1283 | === Extract Common Expectation Parts into Matchers
1284 |
1285 | Extract frequently used common logic from your examples into https://rspec.info/features/3-12/rspec-expectations/custom-matchers/define-matcher/[custom matchers].
1286 |
1287 | [source,ruby]
1288 | ----
1289 | # bad
1290 | it 'returns JSON with temperature in Celsius' do
1291 | json = JSON.parse(response.body).with_indifferent_access
1292 | expect(json[:celsius]).to eq 30
1293 | end
1294 |
1295 | it 'returns JSON with temperature in Fahrenheit' do
1296 | json = JSON.parse(response.body).with_indifferent_access
1297 | expect(json[:fahrenheit]).to eq 86
1298 | end
1299 |
1300 | # good
1301 | it 'returns JSON with temperature in Celsius' do
1302 | expect(response).to include_json(celsius: 30)
1303 | end
1304 |
1305 | it 'returns JSON with temperature in Fahrenheit' do
1306 | expect(response).to include_json(fahrenheit: 86)
1307 | end
1308 | ----
1309 |
1310 | === `any_instance_of`
1311 |
1312 | Avoid using `allow_any_instance_of`/`expect_any_instance_of`.
1313 | It might be an indication that the object under test is too complex, and is ambiguous when used with receive counts.
1314 |
1315 | [source,ruby]
1316 | ----
1317 | # bad
1318 | it 'has a name' do
1319 | allow_any_instance_of(User).to receive(:name).and_return('Tweedledee')
1320 | expect(account.name).to eq 'Tweedledee'
1321 | end
1322 |
1323 | # good
1324 | let(:account) { Account.new(user) }
1325 |
1326 | it 'has a name' do
1327 | allow(user).to receive(:name).and_return('Tweedledee')
1328 | expect(account.name).to eq 'Tweedledee'
1329 | end
1330 | ----
1331 |
1332 | === Matcher Libraries
1333 |
1334 | Use third-party matcher libraries that provide convenience helpers that will significantly simplify the examples, https://github.com/thoughtbot/shoulda-matchers[Shoulda Matchers] are one worth mentioning.
1335 |
1336 | [source,ruby]
1337 | ----
1338 | # bad
1339 | describe '#title' do
1340 | it 'is required' do
1341 | article.title = nil
1342 | article.valid?
1343 | expect(article.errors[:title])
1344 | .to contain_exactly('Article has no title')
1345 | not
1346 | end
1347 | end
1348 |
1349 | # good
1350 | describe '#title' do
1351 | it 'is required' do
1352 | expect(article).to validate_presence_of(:title)
1353 | .with_message('Article has no title')
1354 | end
1355 | end
1356 | ----
1357 |
1358 | == Rails: Integration[[integration]][[rails]]
1359 |
1360 | Test what you see.
1361 | Deeply test your models and your application behaviour (integration tests).
1362 | Do not add useless complexity testing controllers.
1363 |
1364 | This is an open debate in the Ruby community and both sides have good arguments supporting their idea.
1365 | People supporting the need of testing controllers will tell you that your integration tests don't cover all use cases and that they are slow.
1366 | Both are wrong.
1367 | It is possible to cover all use cases and it's possible to make them fast.
1368 |
1369 | == Rails: Views[[views]]
1370 |
1371 | === View Directory Structure
1372 |
1373 | The directory structure of the view specs `spec/views` matches the one in `app/views`.
1374 | For example the specs for the views in `app/views/users` are placed in `spec/views/users`.
1375 |
1376 | === View Spec File Name
1377 |
1378 | The naming convention for the view specs is adding `_spec.rb` to the view name, for example the view `_form.html.erb` has a corresponding spec `_form.html.erb_spec.rb`.
1379 |
1380 | === View Outer `describe`
1381 |
1382 | The outer `describe` block uses the path to the view without the `app/views` part.
1383 | This is used by the `render` method when it is called without arguments.
1384 |
1385 | [source,ruby]
1386 | ----
1387 | # spec/views/articles/new.html.erb_spec.rb
1388 | describe 'articles/new.html.erb' do
1389 | # ...
1390 | end
1391 | ----
1392 |
1393 | === View Mock Models
1394 |
1395 | Always mock the models in the view specs.
1396 | The purpose of the view is only to display information.
1397 |
1398 | === View `assign`
1399 |
1400 | The method `assign` supplies the instance variables which the view uses and are supplied by the controller.
1401 |
1402 | [source,ruby]
1403 | ----
1404 | # spec/views/articles/edit.html.erb_spec.rb
1405 | describe 'articles/edit.html.erb' do
1406 | it 'renders the form for a new article creation' do
1407 | assign(:article, double(Article).as_null_object)
1408 | render
1409 | expect(rendered).to have_selector('form',
1410 | method: 'post',
1411 | action: articles_path
1412 | ) do |form|
1413 | expect(form).to have_selector('input', type: 'submit')
1414 | end
1415 | end
1416 | end
1417 | ----
1418 |
1419 | === Capybara Negative Selectors[[view-capybara-negative-selectors]]
1420 |
1421 | Prefer capybara negative selectors over `to_not` with positive ones.
1422 |
1423 | [source,ruby]
1424 | ----
1425 | # bad
1426 | expect(page).to_not have_selector('input', type: 'submit')
1427 | expect(page).to_not have_xpath('tr')
1428 |
1429 | # good
1430 | expect(page).to have_no_selector('input', type: 'submit')
1431 | expect(page).to have_no_xpath('tr')
1432 | ----
1433 |
1434 | === View Helper Stub
1435 |
1436 | When a view uses helper methods, these methods need to be stubbed.
1437 | Stubbing the helper methods is done on the `template` object:
1438 |
1439 | [source,ruby]
1440 | ----
1441 | # app/helpers/articles_helper.rb
1442 | class ArticlesHelper
1443 | def formatted_date(date)
1444 | # ...
1445 | end
1446 | end
1447 | ----
1448 |
1449 | [source,ruby]
1450 | ----
1451 | # app/views/articles/show.html.erb
1452 | <%= 'Published at: #{formatted_date(@article.published_at)}' %>
1453 | ----
1454 |
1455 | [source,ruby]
1456 | ----
1457 | # spec/views/articles/show.html.erb_spec.rb
1458 | describe 'articles/show.html.erb' do
1459 | it 'displays the formatted date of article publishing' do
1460 | article = double(Article, published_at: Date.new(2012, 01, 01))
1461 | assign(:article, article)
1462 |
1463 | allow(template).to_receive(:formatted_date).with(article.published_at).and_return('01.01.2012')
1464 |
1465 | render
1466 | expect(rendered).to have_content('Published at: 01.01.2012')
1467 | end
1468 | end
1469 | ----
1470 |
1471 | === View Helpers
1472 |
1473 | The helpers specs are separated from the view specs in the `spec/helpers` directory.
1474 |
1475 | == Rails: Controllers[[controllers]]
1476 |
1477 | === Controller Models
1478 |
1479 | Mock the models and stub their methods.
1480 | Testing the controller should not depend on the model creation.
1481 |
1482 | === Controller Behaviour
1483 |
1484 | Test only the behaviour the controller should be responsible about:
1485 |
1486 | * Execution of particular methods
1487 | * Data returned from the action - assigns, etc.
1488 | * Result from the action - template render, redirect, etc.
1489 |
1490 | [source,ruby]
1491 | ----
1492 | # Example of a commonly used controller spec
1493 | # spec/controllers/articles_controller_spec.rb
1494 | # We are interested only in the actions the controller should perform
1495 | # So we are mocking the model creation and stubbing its methods
1496 | # And we concentrate only on the things the controller should do
1497 |
1498 | describe ArticlesController do
1499 | # The model will be used in the specs for all methods of the controller
1500 | let(:article) { double(Article) }
1501 |
1502 | describe 'POST create' do
1503 | before { allow(Article).to receive(:new).and_return(article) }
1504 |
1505 | it 'creates a new article with the given attributes' do
1506 | expect(Article).to receive(:new).with(title: 'The New Article Title').and_return(article)
1507 | post :create, message: { title: 'The New Article Title' }
1508 | end
1509 |
1510 | it 'saves the article' do
1511 | expect(article).to receive(:save)
1512 | post :create
1513 | end
1514 |
1515 | it 'redirects to the Articles index' do
1516 | allow(article).to receive(:save)
1517 | post :create
1518 | expect(response).to redirect_to(action: 'index')
1519 | end
1520 | end
1521 | end
1522 | ----
1523 |
1524 | === Controller Contexts
1525 |
1526 | Use context when the controller action has different behaviour depending on the received params.
1527 |
1528 | [source,ruby]
1529 | ----
1530 | # A classic example for use of contexts in a controller spec is creation or update when the object saves successfully or not.
1531 |
1532 | describe ArticlesController do
1533 | let(:article) { double(Article) }
1534 |
1535 | describe 'POST create' do
1536 | before { allow(Article).to receive(:new).and_return(article) }
1537 |
1538 | it 'creates a new article with the given attributes' do
1539 | expect(Article).to receive(:new).with(title: 'The New Article Title').and_return(article)
1540 | post :create, article: { title: 'The New Article Title' }
1541 | end
1542 |
1543 | it 'saves the article' do
1544 | expect(article).to receive(:save)
1545 | post :create
1546 | end
1547 |
1548 | context 'when the article saves successfully' do
1549 | before do
1550 | allow(article).to receive(:save).and_return(true)
1551 | end
1552 |
1553 | it 'sets a flash[:notice] message' do
1554 | post :create
1555 | expect(flash[:notice]).to eq('The article was saved successfully.')
1556 | end
1557 |
1558 | it 'redirects to the Articles index' do
1559 | post :create
1560 | expect(response).to redirect_to(action: 'index')
1561 | end
1562 | end
1563 |
1564 | context 'when the article fails to save' do
1565 | before do
1566 | allow(article).to receive(:save).and_return(false)
1567 | end
1568 |
1569 | it 'assigns @article' do
1570 | post :create
1571 | expect(assigns[:article]).to eq(article)
1572 | end
1573 |
1574 | it "re-renders the 'new' template" do
1575 | post :create
1576 | expect(response).to render_template('new')
1577 | end
1578 | end
1579 | end
1580 | end
1581 | ----
1582 |
1583 | == Rails: Models[[models]]
1584 |
1585 | === Model Mocks
1586 |
1587 | Do not mock the models in their own specs.
1588 |
1589 | === Model Objects
1590 |
1591 | Use `FactoryBot.create` to make real objects, or just use a new (unsaved) instance with `subject`.
1592 |
1593 | [source,ruby]
1594 | ----
1595 | describe Article do
1596 | subject(:article) { FactoryBot.create(:article) }
1597 |
1598 | it { is_expected.to be_an Article }
1599 | it { is_expected.to be_persisted }
1600 | end
1601 | ----
1602 |
1603 | === Model Mock Associations
1604 |
1605 | It is acceptable to mock other models or child objects.
1606 |
1607 | === Avoid Duplication in Model Tests[[model-avoid-duplication]]
1608 |
1609 | Create the model for all examples in the spec to avoid duplication.
1610 |
1611 | [source,ruby]
1612 | ----
1613 | describe Article do
1614 | let(:article) { FactoryBot.create(:article) }
1615 | end
1616 | ----
1617 |
1618 | === Check Model Validity[[model-check-validity]]
1619 |
1620 | Add an example ensuring that the model created with `FactoryBot.create` is valid.
1621 |
1622 | [source,ruby]
1623 | ----
1624 | describe Article do
1625 | it 'is valid with valid attributes' do
1626 | expect(article).to be_valid
1627 | end
1628 | end
1629 | ----
1630 |
1631 | === Model Validations
1632 |
1633 | When testing validations, use `expect(model.errors[:attribute].size).to eq(x)` to specify the attribute which should be validated.
1634 | Using `be_valid` does not guarantee that the problem is in the intended attribute.
1635 |
1636 | [source,ruby]
1637 | ----
1638 | # bad
1639 | describe '#title' do
1640 | it 'is required' do
1641 | article.title = nil
1642 | expect(article).to_not be_valid
1643 | end
1644 | end
1645 |
1646 | # preferred
1647 | describe '#title' do
1648 | it 'is required' do
1649 | article.title = nil
1650 | article.valid?
1651 | expect(article.errors[:title].size).to eq(1)
1652 | end
1653 | end
1654 | ----
1655 |
1656 | === Separate Example Group for Attribute Validations[[model-separate-describe-for-attribute-validations]]
1657 |
1658 | Add a separate `describe` for each attribute which has validations.
1659 |
1660 | [source,ruby]
1661 | ----
1662 | describe '#title' do
1663 | it 'is required' do
1664 | article.title = nil
1665 | article.valid?
1666 | expect(article.errors[:title].size).to eq(1)
1667 | end
1668 | end
1669 |
1670 | describe '#name' do
1671 | it 'is required' do
1672 | article.name = nil
1673 | article.valid?
1674 | expect(article.errors[:name].size).to eq(1)
1675 | end
1676 | end
1677 | ----
1678 |
1679 | === Naming Another Object[[model-name-another-object]]
1680 |
1681 | When testing uniqueness of a model attribute, name the other object `another_object`.
1682 |
1683 | [source,ruby]
1684 | ----
1685 | describe Article do
1686 | describe '#title' do
1687 | it 'is unique' do
1688 | another_article = FactoryBot.create(:article, title: article.title)
1689 | article.valid?
1690 | expect(article.errors[:title].size).to eq(1)
1691 | end
1692 | end
1693 | end
1694 | ----
1695 |
1696 | == Rails: Mailers[[mailers]]
1697 |
1698 | === Mailer Mock Model
1699 |
1700 | The model in the mailer spec should be mocked.
1701 | The mailer should not depend on the model creation.
1702 |
1703 | === Mailer Expectations
1704 |
1705 | The mailer spec should verify that:
1706 |
1707 | * the subject is correct
1708 | * the sender e-mail is correct
1709 | * the e-mail is sent to the correct recipient
1710 | * the e-mail contains the required information
1711 |
1712 | [source,ruby]
1713 | ----
1714 | describe SubscriberMailer do
1715 | let(:subscriber) { double(Subscription, email: 'johndoe@test.com', name: 'John Doe') }
1716 |
1717 | describe 'successful registration email' do
1718 | subject(:email) { SubscriptionMailer.successful_registration_email(subscriber) }
1719 |
1720 | it { is_expected.to have_attributes(subject: 'Successful Registration!', from: ['infor@your_site.com'], to: [subscriber.email]) }
1721 |
1722 | it 'contains the subscriber name' do
1723 | expect(email.body.encoded).to match(subscriber.name)
1724 | end
1725 | end
1726 | end
1727 | ----
1728 |
1729 | == Recommendations
1730 |
1731 | === Correct Setup
1732 |
1733 | Correctly set up RSpec configuration globally (`~/.rspec`), per project (`.rspec`), and in project override file that is supposed to be kept out of version control (`.rspec-local`).
1734 | Use `rspec --init` to generate `.rspec` and `spec/spec_helper.rb` files.
1735 |
1736 | ----
1737 | # .rspec
1738 | --color
1739 | --require spec_helper
1740 |
1741 | # .rspec-local
1742 | --profile 2
1743 | ----
1744 |
1745 | == Related Guides
1746 |
1747 | * https://rubystyle.guide[Ruby Style Guide]
1748 | * https://rails.rubystyle.guide[Rails Style Guide]
1749 | * https://minitest.rubystyle.guide[Minitest Style Guide]
1750 |
1751 | == Contributing
1752 |
1753 | Nothing written in this guide is set in stone.
1754 | Everyone is welcome to contribute, so that we could ultimately create a resource that will be beneficial to the entire Ruby community.
1755 |
1756 | Feel free to open tickets or send pull requests with improvements.
1757 | Thanks in advance for your help!
1758 |
1759 | You can also support the project (and RuboCop) with financial contributions via https://www.patreon.com/bbatsov[Patreon].
1760 |
1761 | === How to Contribute?
1762 |
1763 | It's easy, just follow the contribution guidelines below:
1764 |
1765 | * https://docs.github.com/en/get-started/quickstart/fork-a-repo[Fork] the https://github.com/rubocop/rspec-style-guide[project] on GitHub
1766 | * Make your feature addition or bug fix in a feature branch
1767 | * Include a http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[good description] of your changes
1768 | * Push your feature branch to GitHub
1769 | * Send a https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests[Pull Request]
1770 |
1771 | == License
1772 |
1773 | image:https://i.creativecommons.org/l/by/3.0/88x31.png[Creative Commons License]
1774 | This work is licensed under a http://creativecommons.org/licenses/by/3.0/deed.en_US[Creative Commons Attribution 3.0 Unported License]
1775 |
1776 | == Credit
1777 |
1778 | Inspiration was taken from the following:
1779 |
1780 | https://github.com/howaboutwe/rspec-style-guide[HowAboutWe's RSpec style guide]
1781 |
1782 | https://github.com/rubocop/rails-style-guide[Community Rails style guide]
1783 |
1784 | This guide was maintained by https://github.com/reachlocal[ReachLocal] for a long while.
1785 |
1786 | This guide includes material originally present in https://github.com/betterspecs/betterspecs[BetterSpecs] (https://betterspecs.github.io/betterspecs/[newer site] https://www.betterspecs.org/[older site]), sponsored by https://github.com/lelylan[Lelylan] and maintained by https://github.com/andreareginato[Andrea Reginato] and https://github.com/betterspecs/betterspecs/graphs/contributors[many others] for a long while.
1787 |
--------------------------------------------------------------------------------
/codespell.txt:
--------------------------------------------------------------------------------
1 | rouge
2 |
--------------------------------------------------------------------------------