├── .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 | --------------------------------------------------------------------------------