├── .gitignore ├── LICENSE ├── README.md ├── bin ├── build ├── build-zip ├── release └── vbump ├── book ├── antipatterns │ ├── bloated_factories.md │ ├── brittle_tests.md │ ├── duplication.md │ ├── false_positives.md │ ├── intermittent_failures.md │ ├── let.md │ ├── logic.md │ ├── slow_tests.md │ ├── stubbing_the_system_under_test.md │ ├── testing_behavior_not_implementation.md │ ├── testing_code_you_dont_own.md │ ├── testing_implementation_details.md │ └── using_factories_like_fixtures.md ├── book.md ├── conclusion.md ├── images │ ├── cover.pdf │ ├── coverage-report-index.png │ ├── coverage-report-show.png │ ├── rails-test-types.png │ └── tdd-cycle.png ├── intermediate_testing │ ├── ci.md │ ├── coverage.md │ ├── external_services.md │ ├── javascript.md │ ├── javascript │ │ ├── ajax.md │ │ ├── cleaning_up.md │ │ ├── unit_tests.md │ │ ├── waiting.md │ │ └── webdrivers.md │ ├── page_objects.md │ ├── testing_in_isolation.md │ └── testing_in_isolation │ │ ├── a_pragmatic_approach.md │ │ ├── benefits.md │ │ ├── dangers.md │ │ ├── stubbing.md │ │ ├── terminology.md │ │ ├── test_doubles.md │ │ └── testing_side_effects.md ├── introduction │ ├── characteristics_of_an_effective_test_suite.md │ ├── example_app.md │ ├── rspec.md │ ├── test_driven_development.md │ └── why_test.md ├── otherbooks.md ├── sample.md └── types_of_tests │ ├── controller_specs.md │ ├── controller_specs │ └── invalid_links.md │ ├── feature_specs.md │ ├── feature_specs │ ├── submitting_a_link_post.md │ ├── submitting_an_invalid_link.md │ ├── viewing_the_homepage.md │ └── voting_on_links.md │ ├── helper_specs.md │ ├── helper_specs │ └── formatting_the_score.md │ ├── mailer_specs.md │ ├── model_specs.md │ ├── model_specs │ ├── class_methods.md │ ├── instance_methods.md │ └── validations_and_associations.md │ ├── request_specs.md │ ├── request_specs │ ├── creating_links.md │ └── viewing_links.md │ ├── testing_pyramid.md │ ├── view_specs.md │ └── view_specs │ └── rendering_images_inline.md ├── example_app ├── .gitignore ├── .gitkeep ├── .rspec ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── api │ │ │ ├── base_controller.rb │ │ │ └── v1 │ │ │ │ └── links_controller.rb │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── downvotes_controller.rb │ │ ├── links_controller.rb │ │ ├── new_links_controller.rb │ │ └── upvotes_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ ├── .keep │ │ ├── application_mailer.rb │ │ └── link_mailer.rb │ ├── models │ │ ├── .keep │ │ ├── concerns │ │ │ └── .keep │ │ ├── link.rb │ │ └── score.rb │ ├── serializers │ │ └── link_serializer.rb │ └── views │ │ ├── application │ │ ├── _error_messages.html.erb │ │ └── _navigation.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ ├── link_mailer │ │ ├── new_link.html.erb │ │ └── new_link.text.erb │ │ └── links │ │ ├── _link.html.erb │ │ ├── _link_list_item.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ ├── setup │ └── spring ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── db │ ├── migrate │ │ ├── 20150408194827_create_links.rb │ │ └── 20150504154305_add_upvotes_and_downvotes_to_links.rb │ ├── schema.rb │ └── seeds.rb ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── robots.txt ├── spec │ ├── controllers │ │ └── links_controller_spec.rb │ ├── factories.rb │ ├── features │ │ ├── user_downvotes_a_link_spec.rb │ │ ├── user_submits_a_link_spec.rb │ │ ├── user_upvotes_a_link_spec.rb │ │ ├── user_views_homepage_spec.rb │ │ └── user_views_new_links_spec.rb │ ├── helpers │ │ └── application_helper_spec.rb │ ├── mailers │ │ └── link_mailer_spec.rb │ ├── models │ │ ├── link_spec.rb │ │ └── score_spec.rb │ ├── rails_helper.rb │ ├── requests │ │ └── api │ │ │ └── v1 │ │ │ └── links_spec.rb │ ├── spec_helper.rb │ ├── support │ │ ├── api_helpers.rb │ │ ├── email_spec.rb │ │ └── factory_girl.rb │ └── views │ │ └── links │ │ ├── _link.html.erb_spec.rb │ │ └── show.html.erb_spec.rb └── vendor │ └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep └── release ├── cover.pdf ├── cover.png ├── images ├── cover.pdf ├── cover.png ├── coverage-report-index.png ├── coverage-report-show.png ├── rails-test-types.png └── tdd-cycle.png ├── tdd-cycle.png ├── testing-rails.epub ├── testing-rails.html ├── testing-rails.md ├── testing-rails.mobi ├── testing-rails.pdf └── testing-rails.toc.html /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | End-User Warranty and License Agreement 2 | 3 | 1. Grant of License 4 | thoughtbot has authorized download by you of one copy of the electronic book 5 | (ebook). thoughtbot grants you a nonexclusive, nontransferable license to use 6 | the ebook according to the terms and conditions herein. This 7 | License Agreement permits you to install the ebook for your use only. 8 | 9 | 2. Restrictions 10 | You shall not: (1) resell, rent, assign, timeshare, distribute, or transfer 11 | all or part of the ebook or any rights granted hereunder to any 12 | other person; (2) duplicate the ebook, except for a single backup 13 | or archival copy; (3) remove any proprietary notices, labels, or marks 14 | from the ebook ; (4) transfer or sublicense title to the ebook 15 | to any other party. 16 | 17 | 3. Intellectual Property Protection 18 | The ebook is owned by thoughtbot and is protected by United States 19 | and international copyright and other intellectual property 20 | laws. thoughtbot reserves all rights in the ebook not expressly 21 | granted herein. This license and your right to use the ebook 22 | terminate automatically if you violate any part of this Agreement. In 23 | the event of termination, you must destroy the original and all copies 24 | of the ebook. 25 | 26 | 4. Limited Warranty 27 | thoughtbot warrants that the files containing the ebook a copy of 28 | which you authorized to download are free from defects in the 29 | operational sense that they can be read by a PDF Reader. EXCEPT FOR 30 | THIS EXPRESS LIMITED WARRANTY, THOUGHTBOT MAKES AND YOU RECEIVE NO 31 | WARRANTIES, EXPRESS, IMPLIED, STATUTORY OR IN ANY COMMUNICATION WITH 32 | YOU, AND THOUGHTBOT SPECIFICALLY DISCLAIMS ANY OTHER WARRANTY INCLUDING 33 | THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS OR A PARTICULAR 34 | PURPOSE. THOUGHTBOT DOES NOT WARRANT THAT THE OPERATION OF THE EBOOK 35 | WILL BE UNINTERRUPTED OR ERROR FREE. If the ebook was purchased in 36 | the United States, the above exclusions may not apply to you as some 37 | states do not allow the exclusion of implied warranties. In addition 38 | to the above warranty rights, you may also have other rights that vary 39 | from state to state. 40 | 41 | 5. Limitation of Liability 42 | IN NO EVENT WILL THOUGHTBOT BE LIABLE FOR ANY DAMAGES, WHETHER RISING FOR 43 | TORT OR CONTRACT, INCLUDING LOSS OF DATA, LOST PROFITS, OR OTHER 44 | SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR INDIRECT DAMAGES RISING OUT OF 45 | THE USE OR INABILITY TO USE THE EBOOK. 46 | 47 | 6. General 48 | This Agreement constitutes the entire agreement between you and 49 | thoughtbot and supersedes any prior agreement concerning the 50 | ebook. This Agreement is governed by the laws of the Commonwealth of 51 | Massachusetts without reference to conflicts of laws provisions. 52 | 53 | (c) 2015 by thoughtbot 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testing Rails 2 | 3 | A book about testing Rails applications the thoughtbot way. 4 | 5 | ## Reading the Book 6 | 7 | You can find the current release in a variety of formats under the [release][] 8 | directory. To view older releases, check out a specific Git [tag][tags]. 9 | 10 | [release]: https://github.com/thoughtbot/testing-rails/tree/master/release 11 | [tags]: https://github.com/thoughtbot/testing-rails/releases 12 | 13 | ## Providing Feedback 14 | 15 | Please provide feedback via [GitHub][]. 16 | 17 | [GitHub]: https://github.com/thoughtbot/testing-rails/issues 18 | 19 | ## Paperback 20 | 21 | We use [Paperback][] (internal to thoughtbot) for generating eBooks. To build 22 | the book, follow [the instructions for setting up Paperback] and be sure to have 23 | Docker running. 24 | 25 | [Paperback]: https://github.com/thoughtbot/paperback 26 | [the instructions for setting up Paperback]: 27 | https://github.com/thoughtbot/paperback#installation 28 | 29 | ## Building the book 30 | 31 | To build the book (for inspecting compiled output): 32 | 33 | $ bin/build 34 | 35 | ## Releasing an update 36 | 37 | We're using tags and releases to track milestones in book updates. 38 | 39 | The release script builds the project, moves the built files into 40 | `/release`, and bumps the git tag: 41 | 42 | $ bin/release 43 | 44 | Build a zip to upload to Gumroad and attach it to the GitHub release: 45 | 46 | $ bin/build-zip 47 | 48 | ## Updating the sample.pdf 49 | 50 | Build and upload to by 51 | updating the website repo (samples are in public/). 52 | 53 | ## Contributors 54 | 55 | Thank you to all who've [contributed][contributors] so far! 56 | 57 | [contributors]: https://github.com/thoughtbot/testing-rails/graphs/contributors 58 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run --dns 8.8.8.8 -v $PWD:/src thoughtbot/paperback build "$@" 4 | -------------------------------------------------------------------------------- /bin/build-zip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd build && find . \! -name '*-sample*' -exec zip ../testing-rails.zip {} + 3 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ./bin/build 5 | 6 | find build \ 7 | -mindepth 2 -maxdepth 2 \! -name '*-sample*' \ 8 | -exec cp -Rv {} release/ \; 9 | 10 | # https://github.com/pbrisbin/vbump 11 | tag="$(git tag | ./bin/vbump "${1:-patch}")" 12 | git add release 13 | git commit -m "Releasing $tag" 14 | git tag -m "$tag" "$tag" 15 | -------------------------------------------------------------------------------- /bin/vbump: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # pbrisbin 2015 4 | # modified version of https://github.com/pbrisbin/vbump 5 | # 6 | # This script depends on the `gsort` (GNU sort) and `gsed` (GNU sed) commands. If 7 | # they are not found, the script will attempt to install them via homebrew (OSX 8 | # package manager) 9 | # 10 | ### 11 | err_usage() { 12 | printf "error: %s\n" "$*" >&2 13 | echo 'usage: vbump [major|minor|patch]' >&2 14 | exit 64 15 | } 16 | 17 | split() { 18 | local pattern='s/v\([0-9]\+\)\.\([0-9]\+\)\.\([0-9]\+\)\(.*\)/\1 \2 \3 \4/' 19 | 20 | gsort -rV | head -n 1 | gsed "$pattern" 21 | } 22 | 23 | bump() { 24 | local major minor patch 25 | 26 | read major minor patch _ 27 | 28 | case "$1" in 29 | major) 30 | major=$((major + 1)) 31 | minor=0 32 | patch=0 33 | ;; 34 | minor) 35 | minor=$((minor + 1)) 36 | patch=0 37 | ;; 38 | patch) 39 | patch=$((patch + 1)) 40 | ;; 41 | 42 | *) err_usage "invalid component: \`$1'" ;; 43 | esac 44 | 45 | printf "v%s.%s.%s\n" $major $minor $patch 46 | } 47 | 48 | if ! command -v gsort > /dev/null; then 49 | brew install coreutils 50 | fi 51 | 52 | split | bump "${1:-patch}" 53 | -------------------------------------------------------------------------------- /book/antipatterns/bloated_factories.md: -------------------------------------------------------------------------------- 1 | ## Bloated Factories 2 | 3 | A factory is the single source of truth for what it takes to instantiate a 4 | minimally valid object. When you define more attributes than you need on a 5 | factory, you implicitly couple yourself to these values every time you use the 6 | factory. These attributes can cause subtle side effects and make your tests 7 | harder to reason about and change as time goes on. 8 | 9 | Factories are intended to be customized directly in the test case you are using 10 | them in. This allows you to communicate what is significant about a record 11 | directly in the test. When you set these attributes in the test case itself, it 12 | is easier to understand the causes and effects in the test. This is useful for 13 | both test maintenance and communication about the feature or unit under test. 14 | 15 | When defining your factories, define the _minimum number of attributes for the 16 | model to pass validations_. Here's an example: 17 | 18 | ```ruby 19 | class User < ActiveRecord::Base 20 | validates :password_digest, presence: true 21 | validates :username, presence: true, uniqueness: true 22 | end 23 | 24 | # DON'T do this 25 | 26 | factory :user do 27 | sequence(:username) { |n| "username#{n}" } 28 | password_digest "password" 29 | name "Donald Duck" # according to our model, this attribute is optional 30 | age 24 # so is this 31 | end 32 | 33 | # DO this 34 | 35 | factory :user do 36 | sequence(:username) { |n| "username#{n}" } 37 | password_digest "password" 38 | end 39 | ``` 40 | -------------------------------------------------------------------------------- /book/antipatterns/brittle_tests.md: -------------------------------------------------------------------------------- 1 | ## Brittle Tests 2 | 3 | When it becomes difficult to make trivial changes to your code without breaking 4 | tests, your test suite can become a burden. Brittle code comes from coupling. 5 | The more coupled your code, the harder it is to make changes without having to 6 | update multiple locations in your code. We want to write test suites that fully 7 | cover all functionality of our application while still being resilient to 8 | change. 9 | 10 | We've learned how [stubbing and mocking can lead to brittle 11 | tests](#brittleness). Here's another example of coupling and some tips to fix 12 | it: 13 | 14 | ### Coupling to copy and the DOM 15 | 16 | An easy way to create brittle tests is to hard code DOM attributes or **[copy]** 17 | (user-facing text) into your tests. These should be easy to change by yourself, 18 | designers, and anyone else on the team without breaking any tests. Most often, 19 | the important thing that you want to test is that a representation of a certain 20 | piece of text or element is appearing in the right spot at the right time. The 21 | actual words and elements themselves are unimportant. 22 | 23 | Consider the following: 24 | 25 | ```html 26 |
27 |

Welcome, #{current_user.name}

28 |
29 | ``` 30 | 31 | 32 | ```ruby 33 | expect(page).to have_content "Welcome, #{user.name}" 34 | ``` 35 | 36 | Now, imagine later on that the text in the template needs to change from 37 | `Welcome, #{user.name}` to `Hello again, #{user.name}!`. We'd now have to change 38 | this text in two places, and if we had it in more tests we'd have to change it 39 | in each one. Let's look at some ways to decouple our tests from our copy. 40 | 41 | [copy]: https://en.wikipedia.org/wiki/Copy_(written) 42 | 43 | #### Internationalization 44 | 45 | Our preferred way to decouple your copy from your tests is to use 46 | internationalization (i18n), which is primarily used to support your app in 47 | multiple languages. i18n works by extracting all the copy in your application to 48 | YAML files, which have keys mapping to your copy. In your application, you 49 | reference these keys, which then output the correct text depending on the user's 50 | language. 51 | 52 | Using i18n can be costly if you _never_ end up supporting multiple languages, 53 | but if you do end up needing to internationalize your app, it is much easier to 54 | do it from the start. The benefit of doing this up front, is that you don't have 55 | to go back and find and replace every line of copy throughout your app, which 56 | grows in difficulty with the size of your app. 57 | 58 | The second benefit of i18n, and why it matters to us here, is that i18n does the 59 | hard work of decoupling our application from specific copy. We can use the keys 60 | in our tests without worrying about the exact text changing out from under us. 61 | With i18n, our tests would look like this: 62 | 63 | ```html 64 |
65 |

<%= t("dashboards.show.welcome", user: current_user) %>

66 |
67 | ``` 68 | 69 | 70 | ```ruby 71 | expect(page).to have_content t("dashboards.show.welcome", user: user) 72 | ``` 73 | 74 | A change in our copy would go directly into our YAML file, and we wouldn't have 75 | to change a thing in any of our templates or tests. 76 | 77 | #### Data Attributes 78 | 79 | If you have an existing app that has not been internationalized, an easier way 80 | to decouple your tests from copy or DOM elements is to use data attributes. You 81 | can add data attributes to any HTML tag and then assert on it's presence in your 82 | tests. Here's an example: 83 | 84 | ```html 85 |
86 |

This is a warning

87 |
88 | ``` 89 | 90 | 91 | ```ruby 92 | expect(page).to have_css "[data-role=warning]" 93 | ``` 94 | 95 | It's important to note that we aren't using `have_css` to assert on the CSS 96 | class or HTML tag either. Classes and tags are DOM elements with high churn and 97 | are often changed by designers who may not be as proficient with Ruby or tests. 98 | By using a separate `data-role`, and teaching designers their purpose, they can 99 | change the markup as much as they want (as long as they keep the `data-role`) 100 | without breaking our tests. 101 | 102 | #### Extract objects and methods 103 | 104 | As with most things in object-oriented programming, the best way to reduce 105 | duplication and minimize coupling is to extract a method or class that can be 106 | reused. That way, if something changes you only have to change it in a single 107 | place. We'll usually start by extracting common functionality to a method. If 108 | the functionality is more complex we'll then consider extracting a page 109 | object. 110 | -------------------------------------------------------------------------------- /book/antipatterns/duplication.md: -------------------------------------------------------------------------------- 1 | ## Duplication 2 | 3 | Test code can fall victim to many of the same traps as production code. One of 4 | the worst offenders is duplication. Those who don't recognize this slowly see 5 | productivity drop as it becomes necessary to modify multiple tests with small 6 | changes to the production codebase. 7 | 8 | Just like you refactor your production code, you should refactor test code, lest 9 | it become a burden. In fact, refactoring tests should be handled at the same 10 | time as refactoring production code — during the refactoring step in _Red, Green, 11 | Refactor_. 12 | 13 | You can use all the tools you use in object oriented programming to DRY up 14 | duplicate test code, such as extracting to methods and classes. For feature 15 | specs, you may consider using [Page Objects](#page-objects) to clean up 16 | repetitive interactions. 17 | 18 | You may also consider using i18n to have a single source of truth for all copy. 19 | i18n can help make your tests resilient, as minor copy tweaks won't require any 20 | changes to your test or even production code. This is of course a secondary 21 | benefit to the fact that it allows you to localize your app to multiple 22 | languages! 23 | 24 | ### Extracting Helper Methods 25 | 26 | Common helper methods should be extracted to `spec/support`, where they can be 27 | organized by utility and automatically included into a specific subset of the 28 | tests. Here's an example from [FormKeep's](https://formkeep.com) test suite: 29 | 30 | ```ruby 31 | # spec/support/kaminari_helper.rb 32 | module KaminariHelper 33 | def with_kaminari_per_page(value, &block) 34 | old_value = Kaminari.config.default_per_page 35 | Kaminari.config.default_per_page = value 36 | block.call 37 | ensure 38 | Kaminari.config.default_per_page = old_value 39 | end 40 | end 41 | 42 | RSpec.configure do |config| 43 | config.include KaminariHelper, type: :request 44 | end 45 | ``` 46 | 47 | The above code allows us to configure Kaminari's `default_per_page` setting in 48 | the block, and ensures it is set back to the original value. The 49 | `RSpec.configure` bit includes our module into all request specs. This file (and 50 | others in `spec/support`) is automatically included in our `rails_helper.rb`: 51 | 52 | ` spec/rails_helper.rb@0eb55ce8d6ea88:22 53 | -------------------------------------------------------------------------------- /book/antipatterns/false_positives.md: -------------------------------------------------------------------------------- 1 | ## False Positives 2 | 3 | Occasionally, you'll run into a case where a feature doesn't work while the test 4 | for it is incorrectly passing. This usually manifests itself when the test is 5 | written after the production code in question. The solution here is to always 6 | follow Red, Green, Refactor. If you don't see your test fail before seeing it 7 | turn green, you can't be certain that the change you are making is the thing 8 | that actually got the test to pass, or if it is passing for some other reason. 9 | By seeing it fail first, you know that once you get it to pass it is passing 10 | because of the changes you made. 11 | 12 | Sometimes, you need to figure out how to get your production code working before 13 | writing your test. This may be because you aren't sure how the production 14 | code is going to work and you just want to try some things out before you know 15 | what you're going to test. When you do this and you go back to write your test, 16 | be sure that you comment out the production code that causes the feature to 17 | work. This way, you can write your test and see it fail. Then, when you comment 18 | in the code to make it pass, you'll be certain that _that_ was the thing to make 19 | the test pass, so your test is valid. 20 | -------------------------------------------------------------------------------- /book/antipatterns/intermittent_failures.md: -------------------------------------------------------------------------------- 1 | ## Intermittent Failures 2 | 3 | Intermittent test failures are one of the hardest kinds of bug to find. Before 4 | you can fix a bug, you need to know why it is happening, and if the bug 5 | manifests itself at seemingly random intervals, this can be especially 6 | difficult. Intermittent failures can happen for a lot of reasons, typically due 7 | to time or from tests affecting other tests. 8 | 9 | We usually advise running your tests in a random order. The goal of this is to 10 | make it easy to tell when tests are being impacted by other tests. If your tests 11 | _aren't_ cleaning up after themselves, then they may cause failures in other 12 | tests, intermittently depending on the order the tests happen to be run in. When 13 | this happens, the best way to start diagnosing is to rerun the tests using the 14 | `seed` of the failing test run. 15 | 16 | You may have noticed that your tests output something like `Randomized with seed 17 | 30205` at the end of each test run. You can use that seed to rerun the tests in 18 | the same "randomized" order: `rspec --seed 30205`. If you want to narrow down 19 | the number of examples that are run, you can use [RSpec 20 | bisect](https://relishapp.com/rspec/rspec-core/v/3-3/docs/command-line/bisect) ( 21 | `rspec --seed 30205 --bisect`), which runs the tests in different combinations 22 | to hone in on the one that is causing problems. 23 | 24 | Here are some likely candidates to look for when trying to diagnose intermittent 25 | failures: 26 | 27 | ### Database contamination 28 | 29 | Database contamination occurs when writes to the database are not cleaned up 30 | after a single test is run. When the subsequent test is run, the effects of the 31 | first test can cause unexpected output. RSpec has transactional fixtures turned 32 | on by default, meaning it runs each test within a transaction, rolling that 33 | transaction back at the end of the test. 34 | 35 | The problem is, tests run with the JavaScript driver are run in a separate 36 | thread which doesn't share a connection to the database. This means that the 37 | test has to commit the changes to the database. In order to return to the 38 | original state, you have to truncate the database, essentially deleting all 39 | records and resetting all indexes. The one downside of this, is that it's a bit 40 | slower than transactions. 41 | 42 | As we've mentioned previously, we use Database Cleaner to automatically use 43 | transaction or truncation to reset our database depending on which strategy is 44 | necessary. 45 | 46 | ### Global state 47 | 48 | Whenever you modify global state, be sure to reset it to the original state 49 | after the test is run, _even if the test raises an error_. If the state is never 50 | reset, the modified value can leak into the following tests in the test run. 51 | Here's a common helper file I'll use to set `ENV` variables in my tests: 52 | 53 | ```ruby 54 | # spec/support/env_helper.rb 55 | 56 | module EnvHelper 57 | def with_env(variable, value) 58 | old_value = ENV[variable] 59 | ENV[variable] = value 60 | yield 61 | ensure 62 | ENV[variable] = old_value 63 | end 64 | end 65 | 66 | RSpec.configure do |config| 67 | config.include EnvHelper 68 | end 69 | ``` 70 | 71 | You can use this in a test, like so: 72 | 73 | ```ruby 74 | require "spec_helper" 75 | 76 | feature "User views the form setup page", :js do 77 | scenario "after creating a submission, they see the continue button" do 78 | with_env("POLLING_INTERVAL", "1") do 79 | form = create(:form) 80 | 81 | visit setup_form_path(form, as: form.user) 82 | 83 | expect(page).not_to have_css "[data-role=continue]" 84 | 85 | submission = create(:submission, form: form) 86 | 87 | expect(page).to have_css "[data-role=continue]" 88 | end 89 | end 90 | end 91 | ``` 92 | 93 | You could also use [Climate 94 | Control](https://github.com/thoughtbot/climate_control), a pre-baked solution 95 | that works in a similar fashion. 96 | 97 | ### Time 98 | 99 | Time and time zones can be tricky. Sometimes microseconds can be the difference 100 | between a passing and failing test, and if you've ever run your tests from 101 | different time zones you may have seen failures on assertions about the current 102 | day. 103 | 104 | The best way to ensure that the time is what you think it is, is to stub it out 105 | with a known value. Rails 4.1 introduced the `travel_to` helper, which allows 106 | you to stub the time within a block: 107 | 108 | ```ruby 109 | it "sets submitted at to the current time" do 110 | form = Form.new 111 | 112 | travel_to Time.now do 113 | form.submit 114 | expect(form.reload.submitted_at).to eq Time.now 115 | end 116 | end 117 | ``` 118 | 119 | If you are on older versions of Rails, you can use 120 | [timecop](https://github.com/travisjeffery/timecop) to control time. 121 | -------------------------------------------------------------------------------- /book/antipatterns/let.md: -------------------------------------------------------------------------------- 1 | ## Let, Subject, and Before 2 | 3 | RSpec has a few features that we have not yet mentioned, because we find that 4 | they make test suites difficult to maintain. The main offenders are `let`, 5 | `let!`, `subject`, and `before`. They share similar problems, so this section 6 | will use `let` and `let!` as examples. `let` allows you to declare a fixture 7 | that will be automatically defined in all other tests in the same context of the 8 | `let`. 9 | 10 | `let` works by passing it a symbol and a block. You are then provided a method 11 | with the same name as the symbol you passed to `let` in your test. When you call 12 | it, RSpec will evaluate and memoize the respective block. Since the block is not 13 | run until you call the method, we say that it is lazy-evaluated. `let!` on the 14 | other hand, will define a method that runs the code in the given block, but it 15 | will always be invoked one time before each test is run. 16 | 17 | Here's an example taken from [Hound](https://github.com/thoughtbot/hound): 18 | 19 | ```ruby 20 | describe RepoActivator, "#deactivate" do 21 | let(:repo) { 22 | create(:repo) 23 | } 24 | 25 | let(:activator) { 26 | allow(RemoveHoundFromRepo).to receive(:run) 27 | allow(AddHoundToRepo).to receive(:run).and_return(true) 28 | 29 | RepoActivator.new(github_token: "githubtoken", repo: repo) 30 | } 31 | 32 | let!(:github_api) { 33 | hook = double(:hook, id: 1) 34 | api = double(:github_api, remove_hook: true) 35 | allow(api).to receive(:create_hook).and_yield(hook) 36 | allow(GithubApi).to receive(:new).and_return(api) 37 | api 38 | } 39 | 40 | context "when repo deactivation succeeds" do 41 | it "marks repo as deactivated" do 42 | activator.deactivate 43 | 44 | expect(repo.reload).not_to be_active 45 | end 46 | 47 | it "removes GitHub hook" do 48 | activator.deactivate 49 | 50 | expect(github_api).to have_received(:remove_hook) 51 | expect(repo.hook_id).to be_nil 52 | end 53 | 54 | it "returns true" do 55 | expect(activator.deactivate).to be true 56 | end 57 | end 58 | end 59 | ``` 60 | 61 | The biggest issue of this code is readability. As with other types of fixtures, 62 | `let` obscures the code by introducing a [Mystery Guest](#fixtures). Having the 63 | test's dependencies declared at the top of the file make it difficult to know 64 | which dependencies are required for each test. If you added more tests to this 65 | test group, they may not all have the same dependencies. 66 | 67 | `let` can also lead to [brittle tests](#brittle-tests). Since your tests are 68 | reliant on objects that are created far from the test cases themselves, it's 69 | easy for somebody to change the setup code unaware of how it will effect each 70 | individual test. This issue is compounded when we override definitions in nested 71 | contexts: 72 | 73 | ```ruby 74 | describe RepoActivator, "#deactivate" do 75 | let(:repo) { 76 | create(:repo) 77 | } 78 | 79 | let(:activator) { 80 | allow(RemoveHoundFromRepo).to receive(:run) 81 | allow(AddHoundToRepo).to receive(:run).and_return(true) 82 | 83 | RepoActivator.new(github_token: "githubtoken", repo: repo) 84 | } 85 | 86 | ... 87 | 88 | context "when repo deactivation succeeds" do 89 | let(:repo) { 90 | create(:repo, some_attribute: "some value") 91 | } 92 | 93 | ... 94 | end 95 | end 96 | ``` 97 | 98 | In the above scenario, we have overriden the definition of `repo` in our nested 99 | context. While we can assume that a direct call to `repo` will return this 100 | locally defined `repo`, what happens when we call `activator`, which also 101 | depends on `repo` but is declared in the outer context? Does it call the `repo` 102 | that is defined in the same context, or does it call the `repo` that is defined 103 | in the same context of our test? 104 | 105 | This code has another, more sneaky problem. If you noticed, there's a subtle use 106 | of `let!` when we declare `github_api`. We used `let!`, because the first and 107 | last example need it to be stubbed, but don't need to reference it in the test. 108 | Since `let!` forces the execution of the code in the block, we've introduced the 109 | possibility for a potential future bug. If we write a new test in this context, 110 | this code will now be run for that test case, even if we didn't intend for that 111 | to happen. This is a recipe for unintentionally slowing down your suite. 112 | 113 | If we were to scroll down so that the `let` statements go off the screen, 114 | our examples would look like this: 115 | 116 | ```ruby 117 | context "when repo deactivation succeeds" do 118 | it "marks repo as deactivated" do 119 | activator.deactivate 120 | 121 | expect(repo.reload).not_to be_active 122 | end 123 | 124 | it "removes GitHub hook" do 125 | activator.deactivate 126 | 127 | expect(github_api).to have_received(:remove_hook) 128 | expect(repo.hook_id).to be_nil 129 | end 130 | 131 | it "returns true" do 132 | expect(activator.deactivate).to be true 133 | end 134 | end 135 | ``` 136 | 137 | We now have no context as to what is happening in these tests. It's impossible to 138 | tell what your test depends on, and what else is happening behind the scenes. In 139 | a large file, you'd have to go back and forth between your tests and `let` 140 | statements, which is slow and error prone. In poorly organized files, you might 141 | even have multiple levels of nesting and dispersed `let` statements, which make 142 | it almost impossible to know which `let` statements are associated with each 143 | test. 144 | 145 | So what's the solution to these problems? Instead of using RSpec's DSL, you can 146 | use plain old Ruby. Variables, methods, and classes! A refactored version of the 147 | code above might look like this: 148 | 149 | ```ruby 150 | describe RepoActivator, "#deactivate" do 151 | context "when repo deactivation succeeds" do 152 | it "marks repo as deactivated" do 153 | repo = create(:repo) 154 | activator = build_activator(repo: repo) 155 | stub_github_api 156 | 157 | activator.deactivate 158 | 159 | expect(repo.reload).not_to be_active 160 | end 161 | 162 | it "removes GitHub hook" do 163 | repo = create(:repo) 164 | activator = build_activator(repo: repo) 165 | github_api = stub_github_api 166 | 167 | activator.deactivate 168 | 169 | expect(github_api).to have_received(:remove_hook) 170 | expect(repo.hook_id).to be_nil 171 | end 172 | 173 | it "returns true" do 174 | activator = build_activator 175 | stub_github_api 176 | 177 | result = activator.deactivate 178 | 179 | expect(result).to be true 180 | end 181 | end 182 | 183 | def build_activator(token: "githubtoken", repo: build(:repo)) 184 | allow(RemoveHoundFromRepo).to receive(:run) 185 | allow(AddHoundToRepo).to receive(:run).and_return(true) 186 | 187 | RepoActivator.new(github_token: token, repo: repo) 188 | end 189 | 190 | def stub_github_api 191 | hook = double(:hook, id: 1) 192 | api = double(:github_api, remove_hook: true) 193 | allow(api).to receive(:create_hook).and_yield(hook) 194 | allow(GithubApi).to receive(:new).and_return(api) 195 | api 196 | end 197 | end 198 | ``` 199 | 200 | By calling these Ruby constructs directly from our test, it's easy to see what 201 | is being generated in the test, and if we need to dig deeper into what's 202 | happening, we can follow the method call trail all the way down. In effect, 203 | we've optimized for communication rather than terseness. We've also avoided 204 | implicitly adding unnecessary dependencies to each of our tests. 205 | 206 | Another thing to note is that while we build the activator and GitHub API stub 207 | in methods external to our tests, we do the assignment within the tests 208 | themselves. Memoizing the value to an instance variable in the external method 209 | is simply a reimplementation of `let`, and suffers the same pitfalls. 210 | -------------------------------------------------------------------------------- /book/antipatterns/logic.md: -------------------------------------------------------------------------------- 1 | ## Using logic to generate tests 2 | 3 | As programmers, one of the first refactorings we are taught is **DRY**, don't 4 | repeat yourself. Although the principle is to prevent the duplication of 5 | _concepts_ in a code base, many developers try to eliminate _duplicated lines or 6 | characters_ which is not necessarily the same thing. 7 | 8 | In tests in particular, trying to remove all duplication can lead you down a 9 | dark path. Taken to an extreme, it leads to unreadable and unchangeable specs. 10 | 11 | Consider the following "clever" code: 12 | 13 | ```ruby 14 | tests = [ 15 | { value: 1, return: false }, 16 | { value: 2, return: true }, 17 | { value: 3, return: false }, 18 | { value: 4, return: true }, 19 | ] 20 | 21 | tests.each do |test| 22 | it "is #{test[:return]} when given #{test[:value]}" do 23 | validator = Validator.new(test[:value]) 24 | 25 | expect(validator.valid?).to eq test[:return] 26 | end 27 | end 28 | ``` 29 | 30 | Reading this is not straightforward. You spend a lot of time understanding the 31 | test-generation logic instead of the test cases themselves. 32 | 33 | It is also very **brittle**. When some use cases start changing but others 34 | don't, the whole structure will grow to be very complex as you try to express 35 | concepts such as conditionals, optional values, and special cases. 36 | 37 | Debugging failures is difficult in this scenario as all failures will break on 38 | the same line. If you try putting a `binding.pry` in there, it will stop for 39 | each of the cases. 40 | 41 | Generating tests can also hide a deeper design problem. Why do you need so many 42 | similar tests that generating them from a data structure seemed like a good 43 | solution? Is it reflecting a lot of conditionals and special cases in your 44 | production code? 45 | 46 | When you are trying to build a modular test suite that can scale and adapt to 47 | change, avoid being "clever" and trying to meta-program your tests in the name 48 | of DRY. 49 | -------------------------------------------------------------------------------- /book/antipatterns/slow_tests.md: -------------------------------------------------------------------------------- 1 | ## Slow tests 2 | 3 | As applications grow, test suites naturally and necessarily get slower. The 4 | longer the test suite, the less you will run it. The more often you can run your 5 | tests, the more valuable they are because you can catch bugs faster than you 6 | otherwise would have. As a baseline, after every line of code that I write, I 7 | try to run its respective test. I always run my entire test suite before 8 | submitting pull requests and after rebasing. As you can imagine, this leads to 9 | running your tests frequently. If it's a chore to run your tests you aren't 10 | going to run them, and they quickly become out of date. At that point, you may 11 | as well not have written them in the first place. 12 | 13 | While continuous integration is a good tool to double check that your suite 14 | passes in a public way, it should not be the _only_ place that the entire suite 15 | is run. If you have to wait to see if your tests pass on CI, this will seriously 16 | slow down the development of new features. 17 | 18 | Here are some things to think about when trying to write a fast test suite: 19 | 20 | ### Use profiling to find the slowest tests 21 | 22 | The easiest way to find the worst offenders is to profile your suite. Running 23 | `rspec` with the `--profile` flag will output the 10 slowest tests (`--profile 24 | 4` will output the 4 slowest). You can add this flag to your `.rspec` file to 25 | output with every run. 26 | 27 | ### Have a fast spec helper 28 | 29 | When you repeatedly run individual tests and test files, you may notice that a 30 | majority of the time running the tests isn't spent running the test itself, but 31 | is actually spent loading your application's dependencies. One of the main 32 | culprits here is Rails. With a large application, loading your entire 33 | application can take seconds, and that's a long time to wait if you want to run 34 | your tests after every line of code you change. 35 | 36 | The nice thing is, some of the tests you write won't depend on Rails at all. 37 | Depending on how you architect your code, this could be a lot of tests. We favor 38 | writing a lot of small objects called POROs, or Plain Old Ruby Objects (objects 39 | that aren't backed by ActiveRecord). Since these objects don't depend on Rails, 40 | we can avoid loading it when running just these tests. 41 | 42 | For this reason, rspec-rails 3.0 introduced multiple default spec helpers. When 43 | you initialize a Rails app with RSpec, it creates a `rails_helper.rb` which 44 | loads Rails and a `spec_helper.rb` which doesn't. When you don't need Rails, or 45 | any of its dependencies, require your `spec_helper.rb` for a modest time 46 | savings. 47 | 48 | ### Use an application preloader 49 | 50 | Rails 4.1 introduced another default feature that reduces some of the time it 51 | takes to load Rails. The feature is bundled in a gem called 52 | [Spring](https://github.com/rails/spring), and classifies itself as an 53 | application preloader. An application preloader automatically keeps your 54 | application running in the background so that you don't have to load it 55 | repeatedly for various different tasks or test runs. Spring is available for 56 | many tasks by default, such as rake tasks, migrations, and TestUnit tests. 57 | 58 | To use spring, you can prefix these commands with the `spring` command, e.g. 59 | `spring rake db:migrate`. The first time you run the command, Spring will start 60 | your application. Subsequent uses of Spring will have already booted your 61 | application, so you should see some time savings. You can avoid having to type 62 | the `spring` command prefix by installing the Spring binstubs: 63 | 64 | ``` 65 | bundle exec spring binstub --all 66 | ``` 67 | 68 | To use spring with RSpec, you'll have to install the 69 | [spring-commands-rspec](https://github.com/jonleighton/spring-commands-rspec) 70 | gem and run `bundle exec spring binstub rspec`. 71 | 72 | If you are on older versions of Rails, you can manually add `spring` to your 73 | Gemfile, or use other application preloaders such as 74 | [Zeus](https://github.com/burke/zeus). 75 | 76 | ### Only persist what is necessary 77 | 78 | One of the most common causes of slow tests is excessive database interaction. 79 | Persisting to the database takes far longer than initializing objects in memory, 80 | and while we're talking fractions of a second, each of these round trips to the 81 | database adds up when running your entire suite. 82 | 83 | When you initialize new objects, try to do so with the least overhead. Depending 84 | on what you need, you should choose your initialization method in this order: 85 | 86 | * `Object.new` - initializes the object without FactoryBot. Use this when you 87 | don't care about any validations or default values. 88 | * `FactoryBot.build_stubbed(:object)` - initializes the object with 89 | FactoryBot, setting up default values and associates records using the 90 | `build_stubbed` method. Nothing is persisted to the database. 91 | * `FactoryBot.build(:object)` - initializes the object with FactoryBot, 92 | setting up default values and persisting associated records with `create`. 93 | * `FactoryBot.create(:object)` - initializes and persists the object with 94 | FactoryBot, setting up default values and persisting associated records with 95 | `create`. 96 | 97 | Another thing to look out for is factory definitions with more associations than 98 | are necessary for a valid model. We talk about this more in [Using Factories 99 | Like Fixtures](#using-factories-like-fixtures). 100 | 101 | ### Move sad paths out of feature specs 102 | 103 | Feature specs are slow. They have to boot up a fake browser and navigate around. 104 | They're particularly slow when using a JavaScript driver which incurs even more 105 | overhead. While you do want a feature spec to cover every user facing feature 106 | of your application, you also don't want to duplicate coverage. 107 | 108 | Many times, feature specs are written to cover both _happy paths_ and _sad 109 | paths_. In an attempt to mitigate duplicate code coverage with slower tests, 110 | we'll often write our happy path tests with feature specs, and sad paths with 111 | some other medium, such as request specs or view specs. Finding a balance 112 | between too many and too few feature specs comes with experience. 113 | 114 | ### Don't hit external APIs 115 | 116 | External APIs are slow and unreliable. Furthermore, you can't access them 117 | without an internet connection and many APIs have rate limits. To avoid all 118 | these problems, you should _not_ be hitting external APIs in the test 119 | environment. For most APIs you should be writing fakes or stubbing them out. At 120 | the very least, you can use the [VCR](https://github.com/vcr/vcr) gem to cache 121 | your test's HTTP requests. If you use VCR, be sure to auto-expire the tests 122 | once every one or two weeks to ensure the API doesn't change out from under you. 123 | 124 | If you want to be extra certain that you are testing against the real API, you 125 | can configure your test suite to hit the API on CI only. 126 | 127 | ### Delete tests 128 | 129 | Sometimes, a test isn't worth it. There are always tradeoffs, and if you have a 130 | particularly slow test that is testing a non-mission critical feature, or a 131 | feature that is unlikely to break, maybe it's time to throw the test out if it 132 | prevents you from running the suite. 133 | -------------------------------------------------------------------------------- /book/antipatterns/stubbing_the_system_under_test.md: -------------------------------------------------------------------------------- 1 | ## Stubbing the System Under Test 2 | 3 | As we've learned, [stubbing](#stubbing) allows us to isolate code we are testing 4 | from other complex behavior. Sometimes, we are tempted to stub a method inside 5 | the class we are testing. If a behavior is so complicated that we feel compelled 6 | to stub it out in a test, that behavior is its own concern and should be 7 | encapsulated in its own class. 8 | 9 | Imagine we are interacting with a payment service that allows us to create and 10 | refund charges to a credit card. Interacting with the service is similar for 11 | each of these requests, so we implement a method `#create_transaction` that we 12 | can reuse when creating and refunding charges: 13 | 14 | ```ruby 15 | class CreditCard 16 | def initialize(id) 17 | @id = id 18 | end 19 | 20 | def create_charge(amount) 21 | create_transaction("/cards/#{@id}/charges", amount: amount) 22 | end 23 | 24 | def refund_charge(transaction_id) 25 | create_transaction("/cards/#{@id}/charges/#{transaction_id}/refund") 26 | end 27 | 28 | private 29 | 30 | def create_transaction(path, data = {}) 31 | response = Net::HTTP.start("payments.example.com") do |http| 32 | post = Net::HTTP::Post.new(path) 33 | post.body = data.to_json 34 | http.request(post) 35 | end 36 | 37 | data = JSON.parse(response.body) 38 | Response.new(transaction_id: data["transaction_id"]) 39 | end 40 | end 41 | ``` 42 | 43 | `#create_transaction` makes an HTTP request to our payment gateway's endpoint, 44 | parses the response data, and returns a new response object with the 45 | `transaction_id` from the returned data. This is a relatively complicated 46 | method, and we've learned before that external web requests can be unreliable 47 | and slow, so we decide to stub this out in our tests for `#create_charge` 48 | and `#refund_charge`. 49 | 50 | An initial test for `#create_charge` might look like this: 51 | 52 | ```ruby 53 | describe CreditCard, "#create_charge" do 54 | it "returns transaction IDs on success" do 55 | credit_card = CreditCard.new("4111") 56 | expected = double("expected") 57 | allow(credit_card).to receive(:create_transaction) 58 | .with("/cards/4111/charges", amount: 100) 59 | .and_return(expected) 60 | 61 | result = credit_card.create_charge(100) 62 | 63 | expect(result).to eq(expected) 64 | end 65 | end 66 | ``` 67 | 68 | This test will work, but it carries some warning signs about how we've factored 69 | our code. The first is we're stubbing a private method. [As we've 70 | learned](#private-methods), tests shouldn't even be aware of private methods. If 71 | you're paying attention, you'll also notice that we've stubbed the system under 72 | test. 73 | 74 | This stub breaks up our `CreditCard` class in an ad hoc manner. We've defined 75 | behavior in our `CreditCard` class definition that we are currently trying to 76 | test, and now we've introduced new behavior with our stub. Instead of splitting 77 | this behavior just in our test, we should separate concerns deliberately in our 78 | production code. 79 | 80 | If we reexamine our code, we'll realize that our `CreditCard` class does in fact 81 | have multiple concerns. One, is acting as our credit card, which can be charged 82 | and refunded. The second, alluded to by our need to stub it, is formatting and 83 | requesting data from our payment gateway. By extracting this behavior to a 84 | `GatewayClient` class, we can create a clear distinction between our two 85 | responsibilities, make each easier to test, and make our `GatewayClient` 86 | functionality easier to reuse. 87 | 88 | Let's extract the class and inject it into our `CreditCard` instance as a 89 | dependency. First, the refactored test: 90 | 91 | ```ruby 92 | describe CreditCard, "#create_charge" do 93 | it "returns transaction IDs on success" do 94 | expected = double("expected") 95 | gateway_client = double("gateway_client") 96 | allow(gateway_client).to receive(:post) 97 | .with("/cards/4111/charges", amount: 100) 98 | .and_return(expected) 99 | credit_card = CreditCard.new(gateway_client, "4111") 100 | 101 | result = credit_card.create_charge(100) 102 | 103 | expect(result).to eq(expected) 104 | end 105 | end 106 | ``` 107 | 108 | Now, we are no longer stubbing the SUT. Instead, we've injected a double 109 | that responds to a method `post` and returns our canned response. Now, we need 110 | to refactor our code to match our expectations. 111 | 112 | ```ruby 113 | class CreditCard 114 | def initialize(client, id) 115 | @id = id 116 | @client = client 117 | end 118 | 119 | def create_charge(amount) 120 | @client.post("/cards/#{@id}/charges", amount: amount) 121 | end 122 | 123 | def refund_charge(transaction_id) 124 | @client.post("/cards/#{@id}/charges/#{transaction_id}/refund") 125 | end 126 | end 127 | 128 | class GatewayClient 129 | def post(path, data = {}) 130 | response = Net::HTTP.start("payments.example.com") do |http| 131 | post = Net::HTTP::Post.new(path) 132 | post.body = data.to_json 133 | http.request(post) 134 | end 135 | 136 | data = JSON.parse(response.body) 137 | Response.new(transaction_id: data["transaction_id"]) 138 | end 139 | end 140 | ``` 141 | 142 | Whenever you are tempted to stub the SUT, take a moment to think about why you 143 | didn't want to set up the required state. If you could easily set up the state 144 | with a factory or helper, prefer that and remove the stub. If the method you are 145 | stubbing has complicated behavior which is difficult to retest, use that as a 146 | cue to extract a new class, and stub the new dependency. 147 | -------------------------------------------------------------------------------- /book/antipatterns/testing_behavior_not_implementation.md: -------------------------------------------------------------------------------- 1 | ## Testing Behavior, Not Implementation 2 | 3 | We've said previously that tests should assert on behavior, not implementation. 4 | But, what happens when our tests _do_ know too much about how code is 5 | implemented? Let's look at an example: 6 | 7 | ```ruby 8 | # app/models/item.rb 9 | class Item < ActiveRecord::Base 10 | def self.unique_names 11 | pluck(:name).uniq.sort 12 | end 13 | end 14 | 15 | # spec/models/item_spec.rb 16 | describe Item, ".unique_names" do 17 | it "returns a list of sorted, unique, Item names" do 18 | create(:item, name: "Gamma") 19 | create(:item, name: "Gamma") 20 | create(:item, name: "Alpha") 21 | create(:item, name: "Beta") 22 | 23 | expected = Item.pluck(:name).uniq.sort 24 | 25 | expect(Item.unique_names).to eq expected 26 | end 27 | end 28 | ``` 29 | 30 | The implementation of the method under test is `pluck(:name).uniq.sort`, and 31 | we're testing it against `Item.pluck(:name).uniq.sort`. In essence, we've 32 | repeated the logic, or implementation, of the code directly in our test. There 33 | are a couple issues with this. 34 | 35 | For one, if we change how we are getting names in the future — say we change the 36 | underlying field name to `some_name` — we'll have to change the expectation in 37 | our test, even though our expectation hasn't changed. While this isn't the 38 | biggest concern, if our test suite has many instances of this it can become 39 | strenuous to maintain. 40 | 41 | Second and more importantly, this test is weak and potentially incorrect. If our 42 | logic is wrong in our production code, it's likely also wrong in our test, 43 | however the test would still be green because our test matches our production 44 | code. 45 | 46 | Instead of testing the implementation of the code, we should test it's behavior. 47 | A better test would look like this: 48 | 49 | ```ruby 50 | describe Item, ".unique_names" do 51 | it "returns a list of sorted, unique, Item names" do 52 | create(:item, name: "Gamma") 53 | create(:item, name: "Gamma") 54 | create(:item, name: "Alpha") 55 | create(:item, name: "Beta") 56 | 57 | expect(Item.unique_names).to eq %w(Alpha Beta Gamma) 58 | end 59 | end 60 | ``` 61 | 62 | Our refactored test specifies the _behavior_ we expect. When the given items 63 | exist, we assert that the method returns `["Alpha", "Beta" "Gamma"]`. We aren't 64 | asserting on logic that could potentially be wrong, but rather how the method 65 | behaves given the above inputs. As an added benefit, it's easier to see what 66 | happens when we call `Item.unique_names`. 67 | 68 | Testing implementation details is a common symptom of _not_ following TDD. Once 69 | you know how the code under question will work, it's all too easy to reimplement 70 | that logic in the test. To avoid this in your codebase, be sure you are writing 71 | your tests first. 72 | -------------------------------------------------------------------------------- /book/antipatterns/testing_code_you_dont_own.md: -------------------------------------------------------------------------------- 1 | ## Testing Code You Don't Own 2 | 3 | When writing tests, it can be easy to get carried away and start testing more 4 | than is necessary. One common mistake is to write unit tests for functionality 5 | provided by a third-party library. For example: 6 | 7 | ```ruby 8 | class User < ActiveRecord::Base 9 | end 10 | ``` 11 | 12 | ```ruby 13 | describe "#save" do 14 | it "saves the user" do 15 | user = User.new 16 | 17 | user.save 18 | 19 | expect(user).to eq User.find(user.id) 20 | end 21 | end 22 | ``` 23 | 24 | This test is not testing any code you've written but instead is testing 25 | `ActiveRecord::Base#save` provided by Rails. Rails should already have tests for 26 | this functionality, so you don't need to test it again. 27 | 28 | This may seem obvious, but we've seen this in the wild more than you'd expect. 29 | 30 | A more subtle example would be when composing behavior from third-party 31 | libraries. For example: 32 | 33 | ```ruby 34 | require "twitter" 35 | 36 | class PublishService 37 | def initialize 38 | @twitterClient = Twitter::REST::Client.new 39 | end 40 | 41 | def publish(message) 42 | @twitter_client.update(message) 43 | end 44 | end 45 | ``` 46 | 47 | ```ruby 48 | describe "#publish" do 49 | it "publishes to twitter" do 50 | new_tweet_request = stub_request(:post, "api.twitter.com/tweets") 51 | service = PublishService.new 52 | 53 | service.publish("message") 54 | 55 | expect(new_tweet_request).to have_been_requested 56 | end 57 | end 58 | ``` 59 | 60 | This unit test is too broad and tests that the twitter gem is correctly 61 | implementing HTTP requests. You should expect that the gem's maintainers have 62 | already tested that. Instead, you can test behavior up to the boundary of the 63 | third-party code using **stub**. 64 | 65 | ``` 66 | describe "#publish" do 67 | it "publishes to twitter" do 68 | client = double(publish: nil) 69 | allow(Twitter::REST::Client).to receive(:new).and_return(client) 70 | service = PublishService.new 71 | 72 | service.publish("message") 73 | 74 | expect(client).to have_received(:publish).with("message") 75 | end 76 | end 77 | ``` 78 | 79 | Methods provided by third-party libraries should already be tested by those 80 | libraries. In fact, they probably test more thoroughly and with more edge cases 81 | than anything you would write yourself. (Re)writing these tests adds overhead to 82 | your test suite without providing any additional value so we encourage you not 83 | to write them at all. 84 | -------------------------------------------------------------------------------- /book/antipatterns/testing_implementation_details.md: -------------------------------------------------------------------------------- 1 | ## Testing Implementation Details 2 | 3 | One metric of a solid test suite is that you shouldn't have to modify your tests 4 | when refactoring production code. If your tests know too much about the 5 | implementation of your code, your production and test code will be highly 6 | coupled, and even minor changes in your production code will require reciprocal 7 | changes in the test suite. When you find yourself refactoring your test suite 8 | alongside a refactoring of your production code, it's likely you've 9 | tested too many implementation details of your code. At this point, your tests 10 | have begun to slow rather than assist in refactoring. 11 | 12 | The solution to this problem is to favor testing behavior over implementation. 13 | You should test _what_ your code does, not _how_ it does it. Let's use some code 14 | as an example: 15 | 16 | ```ruby 17 | class Numeric 18 | def negative? 19 | self < 0 20 | end 21 | end 22 | 23 | def absolute_value(number) 24 | if number.negative? 25 | -number 26 | else 27 | number 28 | end 29 | end 30 | ``` 31 | 32 | The following is a _bad_ test (not to mention, it doesn't fully test the method): 33 | 34 | ```ruby 35 | # this is bad 36 | 37 | describe "#absolute_value" do 38 | it "checks if the number is negative" do 39 | number = 5 40 | allow(number).to receive(:negative?) 41 | 42 | absolute_value(number) 43 | 44 | expect(number).to have_received(:negative?) 45 | end 46 | end 47 | ``` 48 | 49 | The above code tests an implementation detail. If we later removed our 50 | implementation of `Numeric#negative?` we'd have to change both our production 51 | code _and_ our test code. 52 | 53 | A better test would look like this: 54 | 55 | ```ruby 56 | describe "#absolute_value" do 57 | it "returns the number's distance from zero" do 58 | expect(absolute_value(4)).to eq 4 59 | expect(absolute_value(0)).to eq 0 60 | expect(absolute_value(-2)).to eq 2 61 | end 62 | end 63 | ``` 64 | 65 | The above code tests the interface of `#absolute_value`. By testing just the 66 | inputs and outputs, we can freely change the implementation of the method 67 | without having to change our test case. The nice thing is that if we are 68 | following TDD, our tests will naturally follow this guideline, since TDD 69 | encourages us to write tests for the behavior we expect to see. 70 | 71 | ### Gotcha 72 | 73 | It is occasionally true that testing behavior and testing implementation will 74 | be one and the same. A common case for this is when testing methods that must 75 | delegate to other methods. For example, many service objects will queue up a 76 | background job. Queuing that job is a crucial behavior of the service object, 77 | so it may be necessary to stub the job and assert it was called: 78 | 79 | ```ruby 80 | describe "Notifier#notify" do 81 | it "queues a NotifierJob" do 82 | allow(NotifierJob).to receive(:notify) 83 | 84 | Notifier.notify("message") 85 | 86 | expect(NotifierJob).to have_received(:notify).with("message") 87 | end 88 | end 89 | ``` 90 | 91 | ### Private Methods 92 | 93 | As you may have guessed, private methods are an implementation detail. We say 94 | it's an implementation detail, because the consumer of the class will rely on 95 | the public interface, but shouldn't care what is happening behind the scenes. 96 | When you encapsulate code into a private method, the code is not part of the 97 | class's public interface. You should be able to change how the code works (but 98 | not what it does) without disrupting anything that depends on the class. 99 | 100 | The benefit of being able to refactor code freely is a huge boon, as long as you 101 | know that the behavior of your class is well tested. While you shouldn't test 102 | your private methods directly, they can and should be tested indirectly by 103 | exercising the code from public methods. This allows you to change the internals 104 | of your code down the road without having to change your tests. 105 | 106 | If you feel that the logic in your private methods is necessary to test 107 | independently, that may be a hint that the functionality can be encapsulated in 108 | its own class. At that point, you can extract a new class to test. This has the 109 | added benefit of improved reusability and readability. 110 | -------------------------------------------------------------------------------- /book/antipatterns/using_factories_like_fixtures.md: -------------------------------------------------------------------------------- 1 | ## Using Factories Like Fixtures 2 | 3 | While [we prefer factories over fixtures](#factorygirl), it is important to use 4 | factories appropriately to get any benefit. Occasionally we'll see test suites 5 | create factory definitions as if they were fixtures: 6 | 7 | ```ruby 8 | factory :pam, class: User do 9 | name "Pam" 10 | manager false 11 | end 12 | 13 | factory :michael, class: User do 14 | name "Michael" 15 | manager true 16 | end 17 | ``` 18 | 19 | This is the worst of both worlds. Factories are great because they're flexible, 20 | however they are slower than fixtures. When you use them like fixtures, they are 21 | slow, inflexible, _and_ cryptic. As the factory definitions grow, they tend to 22 | violate the rule of having a minimal set of attributes for a valid records. In 23 | addition to the [issues that brings](#bloated-factories), it becomes difficult 24 | to remember which factories return which attributes. 25 | 26 | Instead of creating multiple factory definitions to group related functionality, 27 | use traits or nested factories. 28 | 29 | Traits allow you to compose attributes within the test itself. 30 | 31 | ```ruby 32 | factory :message do 33 | body "What's up?" 34 | 35 | trait :read do 36 | read_at { 1.month.ago } 37 | end 38 | end 39 | 40 | # In the test 41 | build_stubbed(:message, :read) # it's clear what we're getting here 42 | ``` 43 | 44 | You may even consider pulling traits out to the global level for reuse between 45 | factories: 46 | 47 | ```ruby 48 | factory :message do 49 | # noop 50 | end 51 | 52 | factory :notification do 53 | # noop 54 | end 55 | 56 | trait :read do 57 | read_at { 1.month.ago } 58 | end 59 | 60 | # In the test 61 | build_stubbed(:message, :read) 62 | build_stubbed(:notification, :read) 63 | ``` 64 | 65 | In addition to traits, you can extend functionality through inheritance with 66 | nested factories: 67 | 68 | ```ruby 69 | factory :user do 70 | sequence(:username) { |n| "username#{n}" } 71 | password_digest "password" 72 | 73 | factory :subscriber do 74 | subscribed true 75 | end 76 | end 77 | 78 | # In the test 79 | build_stubbed(:subscriber) 80 | ``` 81 | 82 | This allows you to better communicate state and still maintain a single source 83 | of knowledge about the necessary attributes to build a user. 84 | 85 | ```ruby 86 | # This is good 87 | build_stubbed(:user, :subscribed) 88 | 89 | # This is better 90 | build_stubbed(:subscriber) 91 | ``` 92 | 93 | Note that nesting is not as composable as traits since you can only build an 94 | object from a single factory. Traits, however, are more flexible as multiple can 95 | be used at the same time. 96 | -------------------------------------------------------------------------------- /book/book.md: -------------------------------------------------------------------------------- 1 | % Testing Rails 2 | % Josh Steiner 3 | % Joël Quenneville 4 | 5 | \clearpage 6 | 7 | \mainmatter 8 | 9 | # Introduction 10 | 11 | <<[introduction/why_test.md] 12 | 13 | <<[introduction/test_driven_development.md] 14 | 15 | <<[introduction/characteristics_of_an_effective_test_suite.md] 16 | 17 | <<[introduction/example_app.md] 18 | 19 | <<[introduction/rspec.md] 20 | 21 | # Types of Tests 22 | 23 | <<[types_of_tests/testing_pyramid.md] 24 | 25 | <<[types_of_tests/feature_specs.md] 26 | 27 | <<[types_of_tests/model_specs.md] 28 | 29 | <<[types_of_tests/request_specs.md] 30 | 31 | <<[types_of_tests/view_specs.md] 32 | 33 | <<[types_of_tests/controller_specs.md] 34 | 35 | <<[types_of_tests/helper_specs.md] 36 | 37 | <<[types_of_tests/mailer_specs.md] 38 | 39 | # Intermediate Testing 40 | 41 | <<[intermediate_testing/testing_in_isolation.md] 42 | 43 | <<[intermediate_testing/external_services.md] 44 | 45 | <<[intermediate_testing/page_objects.md] 46 | 47 | <<[intermediate_testing/javascript.md] 48 | 49 | <<[intermediate_testing/ci.md] 50 | 51 | <<[intermediate_testing/coverage.md] 52 | 53 | # Antipatterns 54 | 55 | <<[antipatterns/slow_tests.md] 56 | 57 | <<[antipatterns/intermittent_failures.md] 58 | 59 | <<[antipatterns/brittle_tests.md] 60 | 61 | <<[antipatterns/duplication.md] 62 | 63 | <<[antipatterns/testing_implementation_details.md] 64 | 65 | <<[antipatterns/let.md] 66 | 67 | <<[antipatterns/bloated_factories.md] 68 | 69 | <<[antipatterns/using_factories_like_fixtures.md] 70 | 71 | <<[antipatterns/false_positives.md] 72 | 73 | <<[antipatterns/stubbing_the_system_under_test.md] 74 | 75 | <<[antipatterns/testing_behavior_not_implementation.md] 76 | 77 | <<[antipatterns/testing_code_you_dont_own.md] 78 | 79 | # Conclusion 80 | 81 | <<[conclusion.md] 82 | -------------------------------------------------------------------------------- /book/conclusion.md: -------------------------------------------------------------------------------- 1 | Congratulations! You've reached the end. Over the course of this book, we've 2 | learned how testing can be used to ensure the correctness of your code, how to 3 | use tools to automate your tests, and how to integrate them with Rails. 4 | 5 | We've also explored how techniques such as **TDD** and **Outside-in testing**, 6 | can improve not only your tests but also the implementation of your source code. 7 | However, poorly written tests can have the opposite effect so we've discussed 8 | various pitfalls and anti-patterns to avoid. Finally, we've taken a look at 9 | first principles and how coupling among the objects in a system impacts the 10 | tests. 11 | 12 | You have the tools to write an effective test suite that documents your code, 13 | catches regressions, and gives you the confidence you need to keep moving. Take 14 | advantage of this to tame your existing projects and tackle bigger, more 15 | challenging project! 16 | -------------------------------------------------------------------------------- /book/images/cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/book/images/cover.pdf -------------------------------------------------------------------------------- /book/images/coverage-report-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/book/images/coverage-report-index.png -------------------------------------------------------------------------------- /book/images/coverage-report-show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/book/images/coverage-report-show.png -------------------------------------------------------------------------------- /book/images/rails-test-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/book/images/rails-test-types.png -------------------------------------------------------------------------------- /book/images/tdd-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/book/images/tdd-cycle.png -------------------------------------------------------------------------------- /book/intermediate_testing/ci.md: -------------------------------------------------------------------------------- 1 | ## Continuous Integration 2 | 3 | Tests are a great way to make sure an application is working correctly. However, 4 | they only provide that value if you remember to run them. It's easy to forget to 5 | re-run the test suite after rebasing or to think that everything is fine because 6 | the change you made was so small it couldn't *possibly* break anything (hint: it 7 | probably did). 8 | 9 | Enter **continuous integration**, or **CI** for short. Continuous integration is 10 | a service that watches a repository and automatically tries to build the project 11 | and run the test suite every time new code is committed. Ideally it runs on a 12 | separate machine with a clean environment to prevent "works on my machine" bugs. 13 | It should build all branches, allowing you to know if a branch is "green" before 14 | merging it. 15 | 16 | There are many CI providers that will build Rails apps and run their test suite 17 | for you. Our current favorite is [CircleCI](https://circleci.com/). 18 | 19 | GitHub can run your CI service against commits in a pull request and will 20 | integrate the result into the pull request status, clearly marking it as passing 21 | or failing. 22 | 23 | Continuous integration is a great tool for preventing broken code from getting 24 | into `master` and to keep nagging you if any broken code does get there. It is 25 | not a replacement for running tests locally. Having tests that are so slow that 26 | you only run them on CI is a red flag and [should be addressed](#slowtests). 27 | 28 | CI can be used for **continuous deployment**, automatically deploying all green 29 | builds of `master`. 30 | -------------------------------------------------------------------------------- /book/intermediate_testing/coverage.md: -------------------------------------------------------------------------------- 1 | ## Coverage Reports 2 | 3 | In addition to [continuous integration](#continuous-integration), many teams opt 4 | to generate coverage reports. Coverage reports are generated by running 5 | alongside your test suite, and monitoring which lines in your application are 6 | executed. This produces reports allowing you to visualize the frequency in which 7 | each line is hit, along with high level statistics about each file. 8 | 9 | ![High Level Coverage Report](../images/coverage-report-index.png) 10 | 11 | As you can probably guess, you're looking to _approach_ 100% coverage, but take 12 | note that pursuit of this metric brings diminishing returns. As with most things 13 | in life, there is nuance, and you may have to make trade-offs. 14 | 15 | Furthermore, you want to minimize the `Hits / Line` (see figure 3.1). If you are 16 | testing the same code path 50+ times, it's a good sign that you may be 17 | over-testing, which could lead to brittle tests. In a perfect world, you'd have 18 | 100% coverage and only hit each line a single time. 19 | 20 | ![Coverage Report For File](../images/coverage-report-show.png) 21 | 22 | Figure 3.2 shows each executed line in green, and lines that the test suite did 23 | not touch in red. Lines in red are a good target for new tests. 24 | 25 | Coverage reports can be generated in a few ways. They can be generated locally 26 | or [on CI](https://circleci.com/docs/code-coverage/) with the 27 | [simplecov](https://github.com/colszowka/simplecov) gem. Alternatively, you can 28 | rely on third party services, such as [Coveralls](https://coveralls.io/) or 29 | [Code Climate](https://codeclimate.com/). 30 | -------------------------------------------------------------------------------- /book/intermediate_testing/javascript.md: -------------------------------------------------------------------------------- 1 | ## JavaScript 2 | 3 | At some point, your application is going to use JavaScript. However, all the 4 | tools we've explored so far are written for testing Ruby code. How can we test 5 | this behavior? 6 | 7 | <<[intermediate_testing/javascript/webdrivers.md] 8 | 9 | <<[intermediate_testing/javascript/cleaning_up.md] 10 | 11 | <<[intermediate_testing/javascript/waiting.md] 12 | 13 | <<[intermediate_testing/javascript/ajax.md] 14 | 15 | <<[intermediate_testing/javascript/unit_tests.md] 16 | -------------------------------------------------------------------------------- /book/intermediate_testing/javascript/ajax.md: -------------------------------------------------------------------------------- 1 | ### AJAX 2 | 3 | In addition to just manipulating the UI, it is common to use JavaScript to 4 | communicate asynchronously with the server. Testing this in a feature spec can 5 | be tricky. 6 | 7 | Remember, feature specs test the application from a _user's perspective_. As a 8 | user, I don't care whether you use AJAX or not, that is an implementation 9 | detail. What I _do_ care about is the functionality of the application. 10 | Therefore, _feature tests should assert on the UI only_. 11 | 12 | So how do you test AJAX via the UI? Imagine we are trying to test an online 13 | document app where you can click a button and your document is saved via AJAX. 14 | How does that interaction look like for the user? 15 | 16 | ```ruby 17 | click_on "Save" 18 | 19 | # This will automatically wait up to 2 seconds 20 | # giving AJAX time to complete 21 | expect(page).to have_css(".notice", text: "Document saved!) 22 | ``` 23 | 24 | Almost all AJAX interactions will change the UI in some manner for usability 25 | reasons. Assert on these changes and take advantage of Capybara's auto-waiting 26 | matchers. 27 | -------------------------------------------------------------------------------- /book/intermediate_testing/javascript/cleaning_up.md: -------------------------------------------------------------------------------- 1 | ### Cleaning up test data 2 | 3 | By default, RSpec wraps all database interactions in a **database transaction**. 4 | This means that any records created are only accessible within the transaction 5 | and any changes made to the database will be rolled back at the end of the 6 | transaction. Using transactions allows each test to start from a clean 7 | environment with a fresh database. 8 | 9 | This pattern breaks down when dealing with JavaScript in feature specs. Real 10 | browsers (headless or not) run in a separate thread from your Rails app and are 11 | therefore _outside_ of the database transaction. Requests made from these 12 | drivers will not have access to the data created within the specs. 13 | 14 | We can disable transactions in our feature specs but now we need to clean up 15 | manually. This is where [**Database Cleaner**][database cleaner] comes in. 16 | Database Cleaner offers three different ways to handle cleanup: 17 | 18 | 1. Transactions 19 | 2. Deletion (via the SQL `DELETE` command) 20 | 3. Truncation (via the SQL `TRUNCATE` command) 21 | 22 | Transactions are much faster but won't work with JavaScript drivers. The speed 23 | of deletion and truncation depends on the table structure and how many tables 24 | have been populated. Generally speaking, SQL `DELETE` is slower the more rows 25 | there are in your table while `TRUNCATE` has more of a fixed cost. 26 | 27 | [database cleaner]: https://github.com/DatabaseCleaner/database_cleaner 28 | 29 | First, disable transactions in `rails_helper.rb`. 30 | 31 | ```ruby 32 | RSpec.configure do |config| 33 | config.use_transactional_fixtures = false 34 | end 35 | ``` 36 | 37 | Our default database cleaner config looks like this: 38 | 39 | ```ruby 40 | RSpec.configure do |config| 41 | config.before(:suite) do 42 | DatabaseCleaner.clean_with(:deletion) 43 | end 44 | 45 | config.before(:each) do 46 | DatabaseCleaner.strategy = :transaction 47 | end 48 | 49 | config.before(:each, js: true) do 50 | DatabaseCleaner.strategy = :deletion 51 | end 52 | 53 | config.before(:each) do 54 | DatabaseCleaner.start 55 | end 56 | 57 | config.after(:each) do 58 | DatabaseCleaner.clean 59 | end 60 | end 61 | ``` 62 | 63 | We clean the database with deletion once before running the suite. Specs default 64 | to cleaning up via a transaction with the exception of those that use a 65 | JavaScript driver. This gets around the issues created by using a real browser 66 | while still keeping the clean up fast for most specs. 67 | -------------------------------------------------------------------------------- /book/intermediate_testing/javascript/unit_tests.md: -------------------------------------------------------------------------------- 1 | ### Unit tests 2 | 3 | If you have more than just a little jQuery scattered throughout your 4 | application, you are probably going to want to unit test some of it. As with 5 | other things JavaScript, there is an overwhelming amount of choice. 6 | We've had success with both [Jasmine][jasmine] and [Mocha][mocha]. Some 7 | front-end frameworks will push you very strongly towards a particular libary. 8 | For example, Ember is biased towards [Qunit][qunit]. 9 | 10 | These all come with some way of running the suite via the command-line. You 11 | can then build a custom Rake task that will run both your RSpec and JavaScript 12 | suites. The Rake task can be run both locally and on CI. RSpec provides a `rake 13 | spec` task that you can hook into. 14 | 15 | In your `Rakefile`: 16 | 17 | ```ruby 18 | # the jasmine:ci task is provided by the jasmine gem 19 | task :full_suite, ["jasmine:ci", "spec"] 20 | ``` 21 | 22 | You can also override the default rake task to run both suites with just `rake`: 23 | 24 | ```ruby 25 | task(:default).clear 26 | task default: ["jasmin:ci", "spec"] 27 | ``` 28 | 29 | [jasmine]: https://jasmine.github.io/ 30 | [mocha]: https://mochajs.org/ 31 | [qunit]: https://qunitjs.com/ 32 | -------------------------------------------------------------------------------- /book/intermediate_testing/javascript/waiting.md: -------------------------------------------------------------------------------- 1 | ### Asynchronous helpers 2 | 3 | One of the nice things about JavaScript is that you can add interactivity to a 4 | web page in a non-blocking manner. For example, you open a modal when a user 5 | clicks a button. Although it takes a couple seconds (you have a sweet 6 | animation), the user's mouse isn't frozen and they still feel in control. 7 | 8 | This breaks if we try to test via Capybara: 9 | 10 | ```ruby 11 | first(".modal-open").click 12 | first(".confirm").click 13 | ``` 14 | 15 | It will click the button but the next interaction will fail because the modal 16 | hasn't finished loading. The ideal behavior would be for the test to wait until 17 | the modal finished loading. We could add a `sleep` here in the test but this 18 | would slow the test down a lot and won't guarantee that the modal is loaded. 19 | 20 | Luckily, Capybara provides some helpers for this exact situation. Finders such 21 | as `first` or `all` return `nil` if there is no such element. `find` on the 22 | other hand will keep trying until the element shows up on the page or a maximum 23 | wait time has been exceeded (default 2 seconds). While a `sleep 2` will stop 24 | your tests for two seconds on every run, these finders will only wait as long as 25 | it needs to before moving on. 26 | 27 | We can rewrite the previous test as: 28 | 29 | ```ruby 30 | # this will take a few seconds to open modal 31 | find(".modal-open").click 32 | 33 | # this will keep trying to find up to two seconds 34 | find(".confirm").click 35 | ``` 36 | 37 | Similar to `find`, most of Capybara's matchers support waiting. _You should 38 | always use the matchers and not try to call the query methods directly._ 39 | 40 | ```ruby 41 | # This will _not_ retry 42 | expect(page.has_css?(".active")).to eq false 43 | 44 | # This _will_ retry if the element isn't initially on the page 45 | expect(page).not_to have_active_class 46 | ``` 47 | -------------------------------------------------------------------------------- /book/intermediate_testing/javascript/webdrivers.md: -------------------------------------------------------------------------------- 1 | ### Webdrivers 2 | 3 | At the integration level, we don't care what technology is being used under the 4 | hood. The focus is on the user interactions instead. By default, RSpec/Capybara 5 | run feature specs using `Rack::Test` which simulates a browser. Although it is 6 | fast, it cannot execute JavaScript. 7 | 8 | In order to execute JavaScript, we need a real browser. Capybara allows using 9 | different **drivers** instead of the default of `Rack::Test`. 10 | [Selenium][selenium] is a wrapper around Firefox that gives us programmatic 11 | access. If you configure Capybara to use Selenium, you will see a real Firefox 12 | window open up and run through your scenarios. 13 | 14 | The downside to Selenium is that it is slow and somewhat brittle (depends on 15 | your version of Firefox installed). To address these issues, it is more common 16 | to use a **headless driver** such as [Capybara Webkit][webkit] or 17 | [Poltergeist][poltergeist]. These are real browser engines but without the UI. 18 | By packaging the engine and not rendering the UI, these headless browsers can 19 | improve speed by a significant factor as well as avoid breaking every time you 20 | upgrade your browser. 21 | 22 | [selenium]: https://github.com/seleniumhq/selenium 23 | [webkit]: https://github.com/thoughtbot/capybara-webkit 24 | [poltergeist]: https://github.com/teampoltergeist/poltergeist 25 | 26 | To use a JavaScript driver (Capybara Webkit in this case) you install its gem 27 | and then point Capybara to the driver. In your `rails_helper.rb`, you want to 28 | add the following: 29 | 30 | ```ruby 31 | Capybara.javascript_driver = :webkit 32 | ``` 33 | 34 | Then, you want to add a `:js` tag to all scenarios that need to be run with 35 | JavaScript. 36 | 37 | ```ruby 38 | feature "A user does something" do 39 | scenario "and sees a success message", :js do 40 | # test some things 41 | end 42 | end 43 | ``` 44 | -------------------------------------------------------------------------------- /book/intermediate_testing/page_objects.md: -------------------------------------------------------------------------------- 1 | ## Levels of Abstraction 2 | 3 | Capybara gives us many useful commands and matchers for testing an 4 | application from a user's point of view. However, these feature specs can easily 5 | become hard to grok after adding just a few interactions. The best way to combat 6 | this is to write feature specs at a **single level of abstraction**. 7 | 8 | This test has many different levels of abstraction. 9 | 10 | ```ruby 11 | # spec/features/user_marks_todo_complete_spec.rb 12 | feature "User marks todo complete" do 13 | scenario "updates todo as completed" do 14 | sign_in # straight forward 15 | create_todo "Buy milk" # makes sense 16 | 17 | # huh? HTML list element ... text ... some kind of button? 18 | find(".todos li", text: "Buy milk").click_on "Mark complete" 19 | 20 | # hmm... styles ... looks like we want completed todos to look different? 21 | expect(page).to have_css(".todos li.completed", text: "Buy milk") 22 | end 23 | 24 | def create_todo(name) 25 | click_on "Add new todo" 26 | fill_in "Name", with: name 27 | click_on "Submit" 28 | end 29 | end 30 | ``` 31 | 32 | The first two lines are about a user's interactions with the app. Then the next 33 | lines drop down to a much lower level, messing around with CSS selectors and 34 | text values. Readers of the test have to parse all these implementation details 35 | just to understand _what_ is going on. 36 | 37 | Ideally, the spec should read almost like pseudo-code: 38 | 39 | ```ruby 40 | # spec/features/user_marks_todo_complete_spec.rb 41 | feature "User marks todo complete" do 42 | scenario "updates todo as completed" do 43 | # sign_in 44 | # create_todo 45 | # mark todo complete 46 | # assert todo is completed 47 | end 48 | end 49 | ``` 50 | 51 | The two most common ways to get there are **extract method** and **page 52 | objects**. 53 | 54 | ### Extract Method 55 | 56 | The **extract method** pattern is commonly used to hide implementation details 57 | and to maintain a single level of abstraction in both source code and specs. 58 | 59 | Consider the following spec: 60 | 61 | ```ruby 62 | feature "User marks todo complete" do 63 | scenario "updates todo as completed" do 64 | sign_in 65 | create_todo "Buy milk" 66 | 67 | mark_complete "Buy milk" 68 | 69 | expect(page).to have_completed_todo "Buy milk" 70 | end 71 | 72 | def create_todo(name) 73 | click_on "Add new todo" 74 | fill_in "Name", with: name 75 | click_on "Submit" 76 | end 77 | 78 | def mark_complete(name) 79 | find(".todos li", text: name).click_on "Mark complete" 80 | end 81 | 82 | def have_completed_todo(name) 83 | have_css(".todos li.completed", text: name) 84 | end 85 | end 86 | ``` 87 | 88 | Notice how obvious it is what happens in the scenario now. There is no more 89 | context switching, no need to pause and decipher CSS selectors. The interactions 90 | are front and center now. Details such as selectors or the exact text of that 91 | link we need to click are largely irrelevant to readers of our spec and will 92 | likely change often. If we really want to know what is entailed in marking a 93 | todo as "complete", the definition is available just a few lines below. 94 | Convenient yet out of the way. 95 | 96 | Although this does make code reusable if we were to write another scenario, the 97 | primary purpose of extracting these methods is not to reduce duplication. 98 | Instead, it serves as a way to bundle lower-level steps and name them as 99 | higher-level concepts. Communication and maintainability are the main goal here, 100 | easier code-reuse is a useful side effect. 101 | 102 | ### Page objects 103 | 104 | In a RESTful Rails application, the interactions on a page are usually based 105 | around a single resource. Notice how all of the extracted methods in the example 106 | above are about todos (creating, completing, expecting to be complete) and most 107 | of them have `todo` in their name. 108 | 109 | What if instead of having a bunch of helper methods that did things with todos, 110 | we encapsulated that logic into some sort of object that manages todo 111 | interactions on the page? This is the **page object** pattern. 112 | 113 | Our feature spec (with a few more scenarios) might look like: 114 | 115 | ```ruby 116 | scenario "create a new todo" do 117 | sign_in_as "person@example.com" 118 | todo = todo_on_page 119 | 120 | todo.create 121 | 122 | expect(todo).to be_visible 123 | end 124 | 125 | scenario "view only todos the user has created" do 126 | sign_in_as "other@example.com" 127 | todo = todo_on_page 128 | 129 | todo.create 130 | sign_in_as "me@example.com" 131 | 132 | expect(todo).not_to be_visible 133 | end 134 | 135 | scenario "complete my todos" do 136 | sign_in_as "person@example.com" 137 | todo = todo_on_page 138 | 139 | todo.create 140 | todo.mark_complete 141 | 142 | expect(todo).to be_complete 143 | end 144 | 145 | scenario "mark completed todo as incomplete" do 146 | sign_in_as "person@example.com" 147 | todo = todo_on_page 148 | 149 | todo.create 150 | todo.mark_complete 151 | todo.mark_incomplete 152 | 153 | expect(todo).not_to be_complete 154 | end 155 | 156 | def todo_on_page 157 | TodoOnPage.new("Buy eggs") 158 | end 159 | ``` 160 | 161 | The todo is now front and center in all these tests. Notice that the tests now 162 | only say _what_ to do. In fact, this test is no longer web-specific. It could be 163 | for a mobile or desktop app for all we know. Low-level details, the _how_, are 164 | encapsulated in the `TodoOnPage` object. Using an object instead of simple 165 | helper methods allows us to build more complex interactions, extract state and 166 | extract private methods. Notice that the helper methods all required the same 167 | title parameter that is now instance state on the page object. 168 | 169 | Let's take a look at what an implementation of `TodoOnPage` might look like. 170 | 171 | ```ruby 172 | class TodoOnPage 173 | include Capybara::DSL 174 | 175 | attr_reader :title 176 | 177 | def initialize(title) 178 | @title = title 179 | end 180 | 181 | def create 182 | click_link "Create a new todo" 183 | fill_in "Title", with: title 184 | click_button "Create" 185 | end 186 | 187 | def mark_complete 188 | todo_element.click_link "Complete" 189 | end 190 | 191 | def mark_incomplete 192 | todo_element.click_link "Incomplete" 193 | end 194 | 195 | def visible? 196 | todo_list.has_css? "li", text: title 197 | end 198 | 199 | def complete? 200 | todo_list.has_css? "li.complete", text: title 201 | end 202 | 203 | private 204 | 205 | def todo_element 206 | find "li", text: title 207 | end 208 | 209 | def todo_list 210 | find "ol.todos" 211 | end 212 | end 213 | ``` 214 | 215 | This takes advantage of RSpec's "magic" matchers, which turn predicate methods 216 | such as `#visible?` and `#complete?` into matchers like `be_visible` and 217 | `be_complete`. Also, we include `Capybara::DSL` to get all of the nice Capybara 218 | helper methods. 219 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation.md: -------------------------------------------------------------------------------- 1 | ## Testing in isolation 2 | 3 | In a previous chapter we discussed **unit tests**, tests that exercise a single 4 | component of a system in isolation. That's nice in theory, but in the real world 5 | most objects depend on **collaborators** which may in turn depend on their own 6 | collaborators. You set out to test a single object and end up with a whole 7 | sub-system. 8 | 9 | Say we want to add the ability to calculate whether or not a link is 10 | controversial. We're starting to have a lot of score-related functionality so we 11 | extract it into its own `Score` object that takes in a `Link` in its 12 | constructor. `Score` implements the following: `#upvotes`, `#downvotes`, 13 | `#value`, and `#controversial?`. 14 | 15 | The spec looks like: 16 | 17 | ` spec/models/score_spec.rb@e5de94e90a46b9d4 18 | 19 | The **system under test** (often abbreviated SUT) is the unit we are trying to 20 | test. In this case, the SUT is the instance of `Score` which we've named `score` 21 | in each test. However, `score` can't do it's work alone. It needs help from a 22 | **collaborator**. Here, the collaborator (`Link`) is passed in as a parameter to 23 | `Score`'s constructor. 24 | 25 | You'll notice the tests all follow the same pattern. First, we create an 26 | instance of `Link`. Then we use it to build an instance of `Score`. Finally, we 27 | test behavior on the score. Our test can now *fail for reasons completely 28 | unrelated to the score object*: 29 | 30 | * There is no `Link` class defined yet 31 | * `Link`'s constructor expects different arguments 32 | * `Link` does not implement the instance methods `#upvotes` and `#downvotes` 33 | 34 | Note that the collaborator doesn't *have* to be an instance of `Link`. Ruby is a 35 | **duck-typed** language which means that collaborators just need to implement an 36 | expected set of methods rather than be of a given class. In the case of the 37 | `Score`'s constructor, any object that implements the `#upvotes`, and 38 | `#downvotes` methods could be a collaborator. For example if we introduce 39 | comments that could be upvoted/downvoted, `Comment` would be another equally 40 | valid collaborator. 41 | 42 | Ideally, in a pure unit test we could isolate the SUT from its collaborators so 43 | that only the SUT would cause our spec to fail. In fact, we should be able to 44 | TDD the SUT even if collaborating components haven't been built yet. 45 | 46 | <<[intermediate_testing/testing_in_isolation/test_doubles.md] 47 | 48 | <<[intermediate_testing/testing_in_isolation/stubbing.md] 49 | 50 | <<[intermediate_testing/testing_in_isolation/testing_side_effects.md] 51 | 52 | <<[intermediate_testing/testing_in_isolation/terminology.md] 53 | 54 | <<[intermediate_testing/testing_in_isolation/benefits.md] 55 | 56 | <<[intermediate_testing/testing_in_isolation/dangers.md] 57 | 58 | <<[intermediate_testing/testing_in_isolation/a_pragmatic_approach.md] 59 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation/a_pragmatic_approach.md: -------------------------------------------------------------------------------- 1 | ### A pragmatic approach 2 | 3 | Sometimes you need to test a component that is really tightly coupled with 4 | another. When this is framework code it is often better just to back up a bit 5 | and test the two components together. For example models that inherit from 6 | `ActiveRecord::Base` are coupled to ActiveRecord's database code. Trying to 7 | isolate the model from the database can get really painful and there's nothing 8 | you can do about it because you don't own the ActiveRecord code. 9 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation/benefits.md: -------------------------------------------------------------------------------- 1 | ### Benefits 2 | 3 | Taking this approach yields several benefits. Because we aren't using real 4 | collaborators, we can TDD a unit of code even if the collaborators haven't been 5 | written yet. 6 | 7 | Using test doubles gets painful for components that are highly coupled to many 8 | collaborators. Let that pain drive you to reduce the coupling in your system. 9 | Remember the final step of the TDD cycle is *refactor*. 10 | 11 | Test doubles make the interfaces the SUT depends on explicit. Whereas the old 12 | spec said that the helper method relied on a `Link`, the new spec says that 13 | methods on `Score` depend on an object that must implement `#upvotes`, and 14 | `#downvotes`. This improves the unit tests as a source of documentation. 15 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation/dangers.md: -------------------------------------------------------------------------------- 1 | ### Dangers 2 | 3 | One of the biggest benefits of testing in isolation is just that: the ability to 4 | test-drive the creation of an object without worrying about the implementation 5 | of its collaborators. In fact, you can build an object even if its collaborators 6 | don't even exist yet. 7 | 8 | This advantage is also one of its pitfalls. It is possible to have a perfectly 9 | unit-tested system that has components that don't line up with each other or 10 | even have some components missing altogether. The software is broken even though 11 | the test suite is green. 12 | 13 | This is why it is important to also have **integration tests** that test that 14 | the system as a whole works as expected. In a Rails application, these will 15 | usually be your feature specs. 16 | 17 | RSpec also provides some tools to help us combat this. **Verifying doubles** 18 | (created with the method `instance_double`) take a class as their first 19 | argument. When that class is not loaded, it acts like a regular double. However, 20 | when the class is loaded, it will raise an error if you try to call methods on 21 | the double that are not defined for instances of the class. 22 | 23 | ` spec/models/score_spec.rb@5b7564bf3b4d25d 24 | 25 | Here we convert the score spec to use verifying doubles. Now if we try to make 26 | our doubles respond to methods that `Link` does not respond to (such as 27 | `total_upvotes`), we get the following error: 28 | 29 | > Failure/Error: `link = instance_double(Link, total_upvotes: 10, downvotes: 0)` 30 | > Link does not implement: `total_upvotes` 31 | 32 | ### Brittleness 33 | 34 | One of the key ideas behind testing code is that you should test _what_ your 35 | code does, not _how_ it is done. The various techniques for testing in isolation 36 | bend that rule. The tests are more closely coupled to which collaborators an 37 | object uses and the names of the messages the SUT will send to those 38 | collaborators. 39 | 40 | This can make the tests more **brittle**, more likely to break if the 41 | implementation changes. For example, if we changed the `LinksController` to use 42 | `save!` instead of `save`, we would now have to update the double or stubbed 43 | method in the tests. 44 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation/stubbing.md: -------------------------------------------------------------------------------- 1 | ### Stubbing 2 | 3 | **Doubles** make it easy for us to isolate collaborators that are passed into 4 | the object we are testing (the **system under test** or **SUT**). Sometimes 5 | however, we have to deal with collaborators that are hard-coded inside our 6 | object. We can isolate these objects too with a technique called **stubbing**. 7 | 8 | Stubbing allows us to tell collaborators to return a canned response when they 9 | receive a given message. 10 | 11 | ` spec/controllers/links_controller_spec.rb@19e77101e30f69dc 12 | 13 | In this controller spec, we assert that the form should get re-rendered when 14 | given invalid data. However, validation is not done by the controller (the SUT 15 | in this case) but by a collaborator (`Link`). *This test could pass or fail 16 | unexpectedly if the link validations were updated even though no controller code 17 | has changed.* 18 | 19 | We can use a combination RSpec's stubs and test doubles to solve this problem. 20 | 21 | ` spec/controllers/links_controller_spec.rb@bbac4fd4e5244083 22 | 23 | We've already seen how to create a test double to pretend to be a collaborator 24 | that returns the responses we need for a scenario. In the case of this 25 | controller however, the link isn't passed in as a parameter. Instead it is 26 | returned by another collaborator, the hard-coded class `Link`. 27 | 28 | RSpec's `allow`, `to_receive`, and `and_return` methods allow us to target a 29 | collaborator, intercept messages sent to it, and return a canned response. In 30 | this case, whenever the controller asks `Link` for a new instance, it will 31 | return our test double instead. 32 | 33 | By isolating this controller spec, we can change the definition of what a 34 | "valid" link is all we want without impacting this test. The only way this test 35 | can fail now is if it does not re-render the form when `Link#save` returns 36 | `false`. 37 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation/terminology.md: -------------------------------------------------------------------------------- 1 | ### Terminology 2 | 3 | The testing community has a lot of overlapping nomenclature when it comes to the 4 | techniques for testing things in isolation. For example many refer to fake 5 | objects that stand in for collaborators (**doubles**) as **mock objects** or 6 | **test stubs** (not to be confused with **mocking** and **stubbing**). 7 | 8 | RSpec itself added to the confusion by providing `stub` and `mock` aliases for 9 | `double` in older versions (not to be confused with **mocking** and 10 | **stubbing**). 11 | 12 | Forcing a real collaborator to return a canned response to certain messages 13 | (**stubbing**) is sometimes referred to as a **partial double**. 14 | 15 | Finally, RSpec provides a `spy` method which creates a **double** that will 16 | respond to any method. Although often used when **spying**, these can be used 17 | anywhere you'd normally use a standard double and any double can be used when 18 | spying. They term **spy** can be a bit ambiguous as it can refer to both 19 | objects created via the `spy` method and objects used for spying. 20 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation/test_doubles.md: -------------------------------------------------------------------------------- 1 | ### Test doubles 2 | 3 | RSpec gives us **test doubles** (sometimes also called **mock objects**) which 4 | act as fake collaborators in tests. The name derives from stunt doubles in 5 | movies that stand in for the real actor when a difficult stunt needs to be done. 6 | Test doubles are constructed with the `double` method. It takes an optional hash 7 | of methods it needs to respond to as well as their return values. 8 | 9 | Let's try using this in our spec: 10 | 11 | ` spec/models/score_spec.rb@10efa8bc7779a520 12 | 13 | Here, we've replaced the dependency on `Link` and are constructing a double that 14 | responds to the following interface: 15 | 16 | * `upvotes` 17 | * `downvotes` 18 | -------------------------------------------------------------------------------- /book/intermediate_testing/testing_in_isolation/testing_side_effects.md: -------------------------------------------------------------------------------- 1 | ### Testing Side Effects 2 | 3 | So far, we've seen how to isolate ourselves from input from our collaborators. 4 | But what about methods with side-effects whose only behavior is to send a 5 | message to a collaborator? How do we test side-effects without having to test 6 | the whole subsystem? 7 | 8 | A common side-effect in a Rails application is sending email. Because we are 9 | trying to test the controller in isolation here, we don't want to also have to 10 | test the mailer or the filesystem in this spec. Instead, we'd like to just test 11 | that we told the mailer to send the email at the appropriate time and trust that 12 | it will do its job correctly like proper object-oriented citizens. 13 | 14 | RSpec provides two ways of "listening" for and expecting on messages sent to 15 | collaborators. These are **mocking** and **spying**. 16 | 17 | ### Mocking 18 | 19 | When **mocking** an interaction with a collaborator we set up an expectation 20 | that it will receive a given message and then exercise the system to see if that 21 | does indeed happen. Let's return to our example of sending emails to moderators: 22 | 23 | ` spec/controllers/links_controller_spec.rb@b6755ba1b764d1d1 24 | 25 | ### Spying 26 | 27 | Mocking can be a little weird because the expectation happens in the middle of 28 | the test, contrary to the [**four-phase test**](#fourphasetest) pattern 29 | discussed in an earlier section. **Spying** on the other hand does follow that 30 | approach. 31 | 32 | ` spec/controllers/links_controller_spec.rb@db8110e40a4cc40a 33 | 34 | Note that you can only spy on methods that have been stubbed or on test doubles 35 | (often referred to as **spies** in this context because they are often passed 36 | into an object just to record what messages are sent to it). If you try to spy 37 | on an unstubbed method, you will get a warning that looks like: 38 | 39 | > `` expected to have received new_link, but that object is 40 | > not a spy or method has not been stubbed. 41 | -------------------------------------------------------------------------------- /book/introduction/characteristics_of_an_effective_test_suite.md: -------------------------------------------------------------------------------- 1 | ## Characteristics of an Effective Test Suite 2 | 3 | The most effective test suites share the following characteristics. 4 | 5 | ### Fast 6 | 7 | The faster your tests are, the more often you can run them. Ideally, you can run 8 | your tests after every change you make to your codebase. Tests give you the 9 | feedback you need to change your code. The faster they are the faster you can 10 | work and the sooner you can deliver a product. 11 | 12 | When you run slow tests, you have to wait for them to complete. If they are slow 13 | enough, you may even decide to take a coffee break or check Twitter. This 14 | quickly becomes a costly exercise. Even worse, you may decide that running tests 15 | is such an inconvenience that you stop running your tests altogether. 16 | 17 | ### Complete 18 | 19 | Tests cover all public code paths in your application. You should not be able to 20 | remove publicly accessible code from your production app without seeing test 21 | failures. If you aren't sufficiently covered, you can't make changes and be 22 | confident they won't break things. This makes it difficult to maintain your 23 | codebase. 24 | 25 | ### Reliable 26 | 27 | Tests do not wrongly fail or pass. If your tests fail intermittently or you get 28 | false positives you begin to lose confidence in your test suite. Intermittent 29 | failures can be difficult to diagnose. We'll discuss some common symptoms later. 30 | 31 | ### Isolated 32 | 33 | Tests can run in isolation. They set themselves up, and clean up after 34 | themselves. Tests need to set themselves up so that you can run tests 35 | individually. When working on a portion of code, you don't want to have to waste 36 | time running the entire suite just to see output from a single test. Tests that 37 | don't clean up after themselves may leave data or global state which can lead to 38 | failures in other tests when run as an entire suite. 39 | 40 | ### Maintainable 41 | 42 | It is easy to add new tests and existing tests are easy to change. If it is 43 | difficult to add new tests, you will stop writing them and your suite becomes 44 | ineffective. You can use the same principles of good object oriented design to 45 | keep your codebase maintainable. We'll discuss some of them later in this book. 46 | 47 | ### Expressive 48 | 49 | Tests are a powerful form of documentation because they are always kept up to 50 | date. Thus, they should be easy enough to read so they can serve as said 51 | documentation. During the refactor phase of your TDD cycle, be sure you remove 52 | duplication and abstract useful constructs to keep your test code tidy. 53 | -------------------------------------------------------------------------------- /book/introduction/example_app.md: -------------------------------------------------------------------------------- 1 | ## Example Application 2 | 3 | This book comes with a bundled [example application], a Reddit clone called 4 | Reddat. If you are unfamiliar with Reddit, it is an online community for posting 5 | links and text posts. People can then comment on and upvote those posts. Ours 6 | will be a simplified version with no users (anyone can post) and only link 7 | posts. Make sure that you sign into GitHub before attempting to view the example 8 | application and commit links, or you'll receive a 404 error. 9 | 10 | [example application]: https://github.com/thoughtbot/testing-rails/tree/master/example_app 11 | 12 | Most of the code samples included in the book come directly from commits in the 13 | example application. At any point, you can check out the application locally and 14 | check out those commits to explore solutions in progress. For some solutions, 15 | the entire change is not included in the chapter for the sake of focus and 16 | brevity. However, you can see every change made for a solution in the example 17 | commits. 18 | 19 | The book is broken into chapters for specific topics in testing, which makes it 20 | easier to use as a reference and learn about each part step by step. However, 21 | it does make it more challenging to see how a single feature is developed that 22 | requires multiple types of tests. To get a sense of how features develop 23 | naturally please check out the app's [commit history] to see the code evolve one 24 | feature at a time. Additionally, you'll find more tests to learn from that we 25 | won't cover in the book. 26 | 27 | [commit history]: https://github.com/thoughtbot/testing-rails/commits/master/example_app 28 | 29 | Make sure to take a look at the application's [README], as it contains a summary 30 | of the application and instructions for setting it up. 31 | 32 | [README]: https://github.com/thoughtbot/testing-rails/blob/master/example_app/README.md 33 | -------------------------------------------------------------------------------- /book/introduction/rspec.md: -------------------------------------------------------------------------------- 1 | ## RSpec 2 | 3 | We'll need a testing framework in order to write and run our tests. The 4 | framework we choose determines the format we use to write our tests, the 5 | commands we use to execute our tests, and the output we see when we run our 6 | tests. 7 | 8 | Examples of such frameworks include Test::Unit, Minitest, and RSpec. Minitest is 9 | the default Rails testing framework, but we use RSpec for the mature test runner 10 | and a syntax that encourages human readable tests. RSpec provides a Domain 11 | Specific Language (DSL) specifically written for test writing that makes reading 12 | and writing tests feel more natural. The gem is called RSpec, because the tests 13 | read like specifications. They describe _what_ the software does and how the 14 | interface should behave. For this reason, we refer to RSpec tests as _specs_. 15 | 16 | While this book uses RSpec, the content will be based in theories and practice 17 | that you can use with any framework. 18 | 19 | ### Installation 20 | 21 | When creating new apps, we run `rails new` with the `-T` flag to avoid creating 22 | any Minitest files. If you have an existing Rails app and forgot to pass that 23 | flag, you can always remove `/test` manually to avoid having an unused folder in 24 | your project. 25 | 26 | Use [`rspec-rails`] to install RSpec in a Rails app, as it configures many of 27 | the things you need for Rails testing out of the box. The plain ol' 28 | [`rspec`] gem is used for testing non-Rails programs. 29 | 30 | [`rspec-rails`]: https://github.com/rspec/rspec-rails 31 | [`rspec`]: https://github.com/rspec/rspec 32 | 33 | Be sure to add the gem to *both* the `:development` and `:test` groups in your 34 | `Gemfile`. It needs to be in `:development` to expose Rails generators and rake 35 | tasks at the command line. 36 | 37 | ```ruby 38 | group :development, :test do 39 | gem 'rspec-rails', '~> 3.0' 40 | end 41 | ``` 42 | 43 | Bundle install: 44 | 45 | ``` 46 | bundle install 47 | ``` 48 | 49 | Generate RSpec files: 50 | 51 | ``` 52 | rails generate rspec:install 53 | ``` 54 | 55 | This creates the following files: 56 | 57 | * [`.rspec`](https://github.com/thoughtbot/testing-rails/blob/b86752a0690a2800c6f57e23974bfe11c8b5fe28/example_app/.rspec) 58 | 59 | Configures the default flags that are passed when you run `rspec`. The line 60 | [`--require spec_helper`] is notable, as it will automatically require the spec 61 | helper file for every test you run. 62 | 63 | [`--require spec_helper`]: https://github.com/thoughtbot/testing-rails/blob/b86752a0690a2800c6f57e23974bfe11c8b5fe28/example_app/.rspec#L2 64 | 65 | * [`spec/spec_helper.rb`](https://github.com/thoughtbot/testing-rails/blob/b86752a0690a2800c6f57e23974bfe11c8b5fe28/example_app/spec/spec_helper.rb) 66 | 67 | Further customizes how RSpec behaves. Because this is loaded in every test, 68 | you can guarantee it will be run when you run a test in isolation. Tests run 69 | in isolation should run near instantaneously, so be careful adding any 70 | dependencies to this file that won't be needed by every test. If you have 71 | configurations that need to be loaded for a subset of your test suite, 72 | consider making a separate helper file and load it only in those files. 73 | 74 | At the bottom of this file is a comment block the RSpec maintainers suggest 75 | we enable for a better experience. We agree with most of the customizations. 76 | I've [uncommented them], then [commented out a few specific settings] to 77 | reduce some noise in test output. 78 | 79 | [uncommented them]: https://github.com/thoughtbot/testing-rails/commit/572ddcebcf86c74687ced40ddb0aad234f6e9657 80 | [commented out a few specific settings]: https://github.com/thoughtbot/testing-rails/commit/1c5e29def9e64d4e67abb5a0867c67348468ab5b 81 | 82 | 83 | * [`spec/rails_helper.rb`](https://github.com/thoughtbot/testing-rails/blob/b86752a0690a2800c6f57e23974bfe11c8b5fe28/example_app/spec/rails_helper.rb) 84 | 85 | A specialized helper file that loads Rails and its dependencies. Any file that 86 | depends on Rails will need to require this file explicitly. 87 | 88 | The generated spec helpers come with plenty of comments describing what each 89 | configuration does. I encourage you to read those comments to get an idea of how 90 | you can customize RSpec to suit your needs. I won't cover them as they tend to 91 | change with each RSpec version. 92 | -------------------------------------------------------------------------------- /book/introduction/why_test.md: -------------------------------------------------------------------------------- 1 | ## Why test? 2 | 3 | As software developers, we are hired to write code that _works_. 4 | If our code doesn't work, we have failed. 5 | 6 | So how do we ensure correctness? 7 | 8 | One way is to manually run your program after writing it. You write a new 9 | feature, open a browser, click around to see that it works, then continue adding 10 | more features. This works while your application is small, but at some point 11 | your program has too many features to keep track of. You write some new code, 12 | but it unexpectedly breaks old features and you might not even know it. This is 13 | called a **regression**. At one point your code worked, but you later introduced 14 | new code which broke the old functionality. 15 | 16 | A better way is to have the computer check our work. We write software to 17 | automate our lives, so why not write programs to test our code as well? 18 | **Automated tests** are scripts that output whether or not your code works 19 | as intended. They verify that our program works now, and will continue to work 20 | in the future, without humans having to test it by hand. Once you write a test, 21 | you should be able to reuse it for the lifetime of the code it tests, although 22 | your tests can change as expectations of your application change. 23 | 24 | Any large scale and long lasting Rails application should have a comprehensive 25 | test suite. A **test suite** is the collection of tests that ensure that your 26 | system works. Before marking any task as "complete" (i.e. merging into the 27 | `master` branch of your Git repository), it is imperative to run your entire 28 | test suite to catch regressions. If you have written an effective test suite, 29 | and the test suite passes, you can be confident that your entire application 30 | behaves as expected. 31 | 32 | A test suite will be comprised of many different kinds of tests, varying in 33 | scope and subject matter. Some tests will be high level, testing an entire 34 | feature and walking through your application as if they were a real user. Others 35 | may be specific to a single line of code. We'll discuss the varying flavors of 36 | tests in detail throughout this book. 37 | 38 | ### Saving Time and Money 39 | 40 | At the end of the day, testing is about saving time and money. Automated tests 41 | catch bugs sooner, preventing them from ever being deployed. By reducing the 42 | manpower necessary to test an entire system, you quickly make up the time it 43 | takes to implement a test in the first place. 44 | 45 | Automated tests also offer a quicker feedback loop to programmers, as they don't 46 | have to walk through every path in their application by hand. A well written 47 | test can take milliseconds to run, and with a good development setup you don't 48 | even have to leave your editor. Compare that to using a manual approach a 49 | hundred times a day and you can save a good chunk of time. This enables 50 | developers to implement features faster because they can code confidently 51 | without opening the browser. 52 | 53 | When applications grow without a solid test suite, teams are often discouraged 54 | by frequent bugs quietly sneaking into their code. The common solution is 55 | to hire dedicated testers; a Quality Assurance (QA) team. This is an expensive 56 | mistake. As your application grows, now you have to scale the number of hands on 57 | deck, who will never be as effective as automated tests at catching regressions. 58 | QA increases the time to implement features, as developers must communicate back 59 | and forth with another human. Compared to a test suite, this is costly. 60 | 61 | This is not to say that QA is completely useless, but they should be hired in 62 | addition to a good test suite, not as a replacement. While manual testers are 63 | not as efficient as computers at finding regressions, they are much better at 64 | validating subjective qualities of your software, such as user interfaces. 65 | 66 | ### Confidence 67 | 68 | Having a test suite you can trust enables you do things you would otherwise not 69 | be able to. It allows you to make large, sweeping changes in your codebase 70 | without fearing you will break something. It gives you the confidence to deploy 71 | code at 5pm on a Friday. Confidence allows you to move faster. 72 | 73 | ### Living Documentation 74 | 75 | Since every test covers a small piece of functionality in your app, they serve 76 | as something more than just validations of correctness. Tests are a great form 77 | of living documentation. Since comments and dedicated documentation are 78 | decoupled from your code, they quickly go stale as you change your application. 79 | Tests must be up to date, or they will fail. This makes them the second best 80 | source of truth, after the code itself, though they are often easier to read. 81 | When I am unsure how a piece of functionality works, I'll look first at the test 82 | suite to gain insight into how the program is supposed to behave. 83 | -------------------------------------------------------------------------------- /book/otherbooks.md: -------------------------------------------------------------------------------- 1 | # thoughtbot Books 2 | 3 | We have many other books available, covering topics like languages, frameworks, 4 | and even building games. Check them out: 5 | 6 | 7 | 8 | ## Contact us 9 | 10 | If you have any questions, or just want to get in touch, drop us a line: 11 | 12 | [books@thoughtbot.com](mailto:books@thoughtbot.com) 13 | -------------------------------------------------------------------------------- /book/sample.md: -------------------------------------------------------------------------------- 1 | % Testing Rails 2 | % Josh Steiner 3 | 4 | \clearpage 5 | 6 | This sample contains a few hand-picked chapters, published directly from the 7 | book, allowing you to get a sense of the content, style, and delivery of the 8 | product. 9 | 10 | If you enjoy the sample, you can get access to the entire book at: 11 | 12 | 13 | 14 | As a purchaser of the book, you also get access to: 15 | 16 | * Multiple formats, including HTML, PDF, EPUB and Kindle. 17 | * Access to a GitHub repository to receive updates as soon as they're pushed. 18 | * Access to GitHub Issues, where you can provide feedback and tell us what you'd 19 | like to see. 20 | 21 | <<[otherbooks.md] 22 | 23 | \clearpage 24 | 25 | \mainmatter 26 | 27 | # Introduction 28 | 29 | <<[introduction/why_test.md] 30 | 31 | <<[introduction/test_driven_development.md] 32 | 33 | <<[introduction/characteristics_of_an_effective_test_suite.md] 34 | 35 | # Closing 36 | 37 | Thanks for checking out this sample of _Testing Rails_. If you'd like to get 38 | access to the full content, the example application, ongoing updates and the 39 | opportunity to have your questions about Rails testing answered by us, you can 40 | get it all on our website: 41 | 42 | 43 | -------------------------------------------------------------------------------- /book/types_of_tests/controller_specs.md: -------------------------------------------------------------------------------- 1 | ## Controller Specs 2 | 3 | Controller specs exist in a weird space between other test types. They have some 4 | overlap with many of the other test types discussed so far so their use can be 5 | controversial. 6 | 7 | In terms of scope they aren't really unit tests because controllers are so 8 | tightly coupled to other parts of the Rails infrastructure. On the other hand, 9 | they aren't integration tests either because requests don't go through the 10 | routes and don't render the view. 11 | 12 | As their name implies, controller specs are used to test the logic in a 13 | controller. We've previously seen that feature specs can drive the creation of a 14 | controller. Given that Rails developers actively try to keep logic out of their 15 | controllers and that feature specs do cover controllers, controller tests can 16 | often be redundant. A good rule of thumb is that you don't need a controller 17 | test until you introduce conditional logic to your controller. In our 18 | experience, we tend to write very few controller specs in our applications. 19 | 20 | As previously mentioned, feature specs are *slow* (relative to other spec 21 | types). They are best used to test flows through an application. If there are 22 | multiple ways to error out of a flow early, it can be expensive to write the 23 | same feature spec over and over with minor variations. 24 | 25 | Time for a controller spec! Or what about a request spec? The two spec types are 26 | quite similar and there are many situations where either would do the job. The 27 | main difference is that controller specs don't actually render views or hit URLs 28 | and go through the routing system. 29 | 30 | So if you have logic in a controller and 31 | 32 | * the forking logic is part of two distinct and important features, you may want 33 | a **feature spec** 34 | * you care about the URL, you may want a **request spec** 35 | * you care about the rendered content, you may want a **request spec** or even a 36 | **view spec** 37 | * none of the above apply, you may want a **controller spec** or a **request 38 | spec** 39 | 40 | One common rule of thumb is to use feature specs for **happy paths** and 41 | controller tests for the **sad paths**. 42 | 43 | The "happy path" is where everything succeeds (e.g. successfully navigating the 44 | app and submitting a link) while the "sad path" is where a failure occurs (e.g. 45 | successfully navigating the app but submitting an invalid link). Some flows 46 | through the app have multiple points of potential failure so there can be 47 | multiple "sad paths" for a given "happy path". 48 | 49 | All this being said, let's look at an actual controller spec! In this section, 50 | we'll be rewriting the tests for the invalid link case to use a controller spec 51 | rather than a feature spec. 52 | 53 | <<[types_of_tests/controller_specs/invalid_links.md] 54 | -------------------------------------------------------------------------------- /book/types_of_tests/controller_specs/invalid_links.md: -------------------------------------------------------------------------------- 1 | ### Invalid Links 2 | 3 | In this test, we want to try and submit an invalid link and expect that it will 4 | not succeed but that the form will be re-rendered. 5 | 6 | The specs looks like this: 7 | 8 | ` spec/controllers/links_controller_spec.rb@19e77101e30f69dc 9 | 10 | Just like with the request spec, the `post` method will make a `POST` request. 11 | However, unlike the request spec, we are making the request directly to a 12 | controller action rather than to a URL. 13 | 14 | The first parameter to `post` is the action we want to exercise. In addition, we 15 | may pass an optional hash of params. Since we are simulating a form submission, 16 | we need a hash of attributes nested under the `link` key. We can generate these 17 | attributes by taking advantage of the invalid link factory we created 18 | earlier. Finally, the controller is inferred from the `RSpec.describe`. 19 | 20 | This will make a `POST` request to the `LinksController#create` action with an 21 | invalid link as its payload. 22 | 23 | Controller specs expose a `response` object that we can assert against. Although 24 | we cannot assert against actual rendered content, we can assert against the name 25 | of the template that will be rendered. 26 | 27 | It is worth noting here that this spec is *not* equivalent to the feature spec 28 | it replaces. The feature test tested that an error message *actually appeared on 29 | the page*. The controller test, on the other hand, only tests that the form gets 30 | re-rendered. 31 | 32 | This is one of those situations where you have to make a judgment call. Is it 33 | important enough to test that the error message shows up on the page, or is 34 | testing that the application handles the error sufficient? Is it worth trading a 35 | slow and partially duplicated feature spec for a faster controller test that 36 | doesn't test the UI? Would a request spec be a good compromise? What about a 37 | controller spec plus a view spec to test the both sides independently? 38 | 39 | All of these options are valid solutions. Based on the context you will pick the 40 | one that gives you the best combination of confidence, coverage, and speed. 41 | -------------------------------------------------------------------------------- /book/types_of_tests/feature_specs.md: -------------------------------------------------------------------------------- 1 | ## Feature Specs 2 | 3 | Feature specs simulate a user opening your app in a browser and interacting with 4 | the page. Since they test that the application works for the end user, they are 5 | considered a form of **acceptance tests**, and you may hear them referred to as 6 | such. When developing a new feature and practicing outside-in development, this 7 | is where we'll typically start. 8 | 9 | <<[types_of_tests/feature_specs/submitting_a_link_post.md] 10 | 11 | <<[types_of_tests/feature_specs/submitting_an_invalid_link.md] 12 | 13 | <<[types_of_tests/feature_specs/viewing_the_homepage.md] 14 | 15 | <<[types_of_tests/feature_specs/voting_on_links.md] 16 | -------------------------------------------------------------------------------- /book/types_of_tests/feature_specs/submitting_an_invalid_link.md: -------------------------------------------------------------------------------- 1 | ### Submitting an invalid link 2 | 3 | All links should have a title and URL, so we should prevent users from 4 | submitting invalid links. Since this is part of the "User submits a link" 5 | feature, we can add it to the same feature block under a different scenario. A 6 | basic feature spec might look like this: 7 | 8 | ` spec/features/user_submits_a_link_spec.rb@5ed398161:17,28 9 | 10 | This test intentionally leaves the URL blank, and expects to see an error 11 | message on the page for the missing URL. While we could test every possible path 12 | (without a title, without a URL, without both), we really only need to test one 13 | at an integration level. This will assure us that an error message renders if the 14 | link is invalid. To ensure that each of our fields are valid, we instead test 15 | this at the model layer. You can see how I tested this in the [respective 16 | commit], but we won't cover model specs until the next chapter. 17 | 18 | [respective commit]: https://github.com/thoughtbot/testing-rails/commit/5ed3981619066bb71c1b8f4b17647c57aebd2707 19 | 20 | There are a couple new methods in this test. The first is `#context`. As you 21 | might guess, it allows you to provide additional context to wrap one or more 22 | scenarios. In fact, you can even nest additional context blocks, however we 23 | recommend against that. Specs are much easier to read with minimal nesting. If 24 | you need to nest scenarios more than a couple levels deep, you might consider 25 | pulling out a new feature file. 26 | 27 | The other new method is `#have_content`. Like `#have_link`, this method comes 28 | from Capybara, and is actually `#has_content?`. `#has_content?` will look on the 29 | page for the given text, ignoring any HTML tags. 30 | 31 | #### Passing the test 32 | 33 | As always, I'll run the test now and follow the error messages to a solution. 34 | I'll leave this up to the reader, but feel free to check out [the commit] to see 35 | what I did. 36 | 37 | [the commit]: https://github.com/thoughtbot/testing-rails/commit/5ed3981619066bb71c1b8f4b17647c57aebd2707 38 | 39 | #### Four Phase Test 40 | 41 | You'll note that in each of our tests so far, we've used some strategic spacing. 42 | This spacing is meant to make the tests easier to read by sectioning it into 43 | multiple phases. The pattern here is modeled after the [Four Phase Test], which 44 | takes the form: 45 | 46 | [Four Phase Test]: http://xunitpatterns.com/Four%20Phase%20Test.html 47 | 48 | ``` 49 | test do 50 | setup 51 | exercise 52 | verify 53 | teardown 54 | end 55 | ``` 56 | 57 | **Setup** 58 | 59 | During setup, we create any objects that your test depends on. 60 | 61 | **Exercise** 62 | 63 | During exercise, we execute the functionality we are testing. 64 | 65 | **Verify** 66 | 67 | During verify, we check our expectations against the result of the exercise 68 | phase. 69 | 70 | **Teardown** 71 | 72 | During teardown, we clean-up after ourselves. This may involve resetting the 73 | database to it's pre-test state or resetting any modified global state. This is 74 | usually handled by our test framework. 75 | 76 | Four phase testing is more prominently used with model and unit tests, however 77 | it is still useful for our acceptance tests. This is especially true for simple 78 | tests like the two we've demonstrated, however some acceptance tests may be 79 | large enough to warrant even more grouping. It's best to use your discretion and 80 | group things into logical sections to make code easier to read. 81 | -------------------------------------------------------------------------------- /book/types_of_tests/feature_specs/viewing_the_homepage.md: -------------------------------------------------------------------------------- 1 | ### Viewing the homepage 2 | 3 | Now that we can create links, we should test that we actually see them on the 4 | homepage. Again, we'll start with some pseudocode: 5 | 6 | ``` 7 | As a user 8 | Given a link has already been submitted 9 | When I visit the home page 10 | Then I should see the link's title on the page 11 | And it should link to the correct URL 12 | ``` 13 | 14 | This test is a little different than our last. This time we have a "given". 15 | Instead of creating a link ourselves, we're going to assume one already exists. 16 | The reason behind this is simple. Walking through our application with Capybara 17 | is slow. We shouldn't do it any more than we have to. We've already tested that 18 | we can submit a link, so we don't need to test it again. Instead, we can create 19 | records directly in the database. 20 | 21 | We could go about creating records the way you'd expect: 22 | 23 | ```ruby 24 | link = Link.create(title: "Testing Rails", url: "http://testingrailsbook.com") 25 | ``` 26 | 27 | This _would_ work, but it has some serious downfalls when using it to test 28 | software. Imagine we have a large application, with hundreds of tests, each one 29 | having created a `Link` the manual way. If we were to add a required field to 30 | links, we would have to go through all of our tests and add the required field 31 | for _all_ of these tests to get them to pass again. There are two widely used 32 | fixes for this pain point. The first one is called fixtures. 33 | 34 | #### Fixtures 35 | 36 | Fixtures allow you to define sample data in YAML files that you can 37 | load and reuse through your tests. It might look something like this: 38 | 39 | 40 | ```yaml 41 | # fixtures/links.yml 42 | testing_rails: 43 | title: Testing Rails 44 | url: http://testingrailsbook.com 45 | ``` 46 | 47 | ```ruby 48 | # In your test 49 | link = links(:testing_rails) 50 | ``` 51 | 52 | Now that we've extracted the definition of our _Testing Rails_ link, if our 53 | model adds new required fields we only have to update our fixtures file. This is 54 | a big step up, but we still see some problems with this solution. 55 | 56 | For one, fixtures are a form of **Mystery Guest**. You have a Mystery Guest when 57 | data used by your test is defined outside the test, thus obscuring the cause and 58 | effect between that data and what is being verified. This makes tests harder to 59 | reason about, because you have to hunt down another file to be able to 60 | understand the entirety of what is happening. 61 | 62 | As applications grow, you'll typically need variations on each of your models 63 | for different situations. For example, you may have a fixture for every user 64 | role in your application, then even more users for different roles depending on 65 | whether or not the user is a member of a specific organization. All these are 66 | possible states a user can be in and grow the number of fixtures you will have to 67 | recall. 68 | 69 | #### FactoryBot 70 | 71 | We've found factories to be a better alternative to fixtures. Rather than 72 | defining hardcoded data, factories define generators of sorts, with predefined 73 | logic where necessary. You can override this logic directly when instantiating 74 | the factories in your tests. They look something like this: 75 | 76 | ```ruby 77 | # spec/factories.rb 78 | FactoryBot.define do 79 | factory :link do 80 | title "Testing Rails" 81 | url "http://testingrailsbook.com" 82 | end 83 | end 84 | ``` 85 | 86 | ```ruby 87 | # In your test 88 | link = create(:link) 89 | 90 | # Or override the title 91 | link = create(:link, title: "TDD isn't Dead!") 92 | ``` 93 | 94 | Factories put the important logic right in your test. They make it easy to see 95 | what is happening at a glance and are more flexible to different scenarios you 96 | may want to set up. While factories can be slower than fixtures, we think the 97 | benefits in flexibility and readability outweigh the costs. 98 | 99 | #### Installing FactoryBot 100 | 101 | To install FactoryBot (formerly FactoryGirl), add `factory_bot_rails` to your 102 | `Gemfile`: 103 | 104 | ```ruby 105 | group :development, :test do 106 | ... 107 | gem "factory_bot_rails" 108 | ... 109 | end 110 | ``` 111 | 112 | We'll also be using Database Cleaner: 113 | 114 | ```ruby 115 | group :test do 116 | ... 117 | gem "database_cleaner" 118 | ... 119 | end 120 | ``` 121 | 122 | Install the new gems and create a new file `spec/support/factory_bot.rb`. 123 | 124 | **Note:** _ Older versions of this library were named `FactoryGirl` and the file 125 | was named `factory_girl.rb`. 126 | 127 | ` spec/support/factory_girl.rb@944b0967 128 | 129 | This file will lint your factories before the test suite is run. That is, it 130 | will ensure that all the factories you define are valid. While not necessary, 131 | this is a worthwhile check, especially while you are learning. It's a quick way 132 | to rest easy that your factories work. Since `FactoryBot.lint` may end up 133 | persisting some records to the database, we use Database Cleaner to restore the 134 | state of the database after we've linted our factories. We'll cover Database 135 | Cleaner in depth later. 136 | 137 | Now, this file won't require itself! In your `rails_helper` you'll find some 138 | commented out code that requires all of the files in `spec/support`. Let's 139 | comment that in so our FactoryBot config gets loaded: 140 | 141 | ```ruby 142 | # Uncomment me! 143 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 144 | ``` 145 | 146 | Last, we need to create our factories file. Create a new file at 147 | `spec/factories.rb`: 148 | 149 | ```ruby 150 | FactoryBot.define do 151 | end 152 | ``` 153 | 154 | This is where we'll define our factory in the next section. 155 | 156 | #### The test 157 | 158 | With FactoryBot set up, we can write our test. We start with a new file at 159 | `spec/features/user_views_homepage_spec.rb`. 160 | 161 | ```ruby 162 | require "rails_helper" 163 | 164 | RSpec.feature "User views homepage" do 165 | scenario "they see existing links" do 166 | end 167 | end 168 | ``` 169 | 170 | We require our `rails_helper` and create the standard feature and scenario 171 | blocks. 172 | 173 | ```ruby 174 | link = create(:link) 175 | ``` 176 | 177 | To setup our test, we create a link using FactoryBot's `.create` method, which 178 | instantiates a new `Link` object with our (currently non-existent) factory 179 | definition and persists it to the database. 180 | 181 | `.create` is loaded into the global context in `spec/support/factory_bot.rb`: 182 | 183 | ``` 184 | config.include FactoryBot::Syntax::Methods 185 | ``` 186 | 187 | While we'll be calling `.create` in the global context to keep our code cleaner, 188 | you may see people calling it more explicitly: `FactoryBot.create`. This is 189 | simply a matter of preference, and both are acceptable. 190 | 191 | Now, we'll need to add a factory definition for our `Link` class in 192 | `spec/factories.rb`: 193 | 194 | ` spec/factories.rb@944b0967:2,5 195 | 196 | We define a default title and URL to be created for all links created with 197 | FactoryBot. We only define defaults for fields that we [validate presence of]. If 198 | you add more than that, your factories can become unmanageable as all of your 199 | tests become coupled to data defined in your factories that isn't a default. 200 | Not following this advice is a common mistake in Rails codebases and leads to 201 | major headaches. 202 | 203 | [validate presence of]: https://github.com/thoughtbot/testing-rails/commit/5ed3981619066bb71c1b8f4b17647c57aebd2707#diff-594e2b1fb48290a8f5f695da1c1e9318R2 204 | 205 | The specific title and URL is unimportant, so we don't override the factories' 206 | defaults. This allows us to focus on what is important and makes the test easier 207 | to read. 208 | 209 | ```ruby 210 | visit root_path 211 | 212 | expect(page).to have_link link.title, href: link.url 213 | ``` 214 | 215 | Nothing novel here. Visit the homepage and assert that we see the title linking 216 | to the URL. 217 | 218 | #### Passing the test 219 | 220 | This is left as an exercise for the reader. Feel free to check out the 221 | [associated commit] to see what I did. 222 | 223 | [associated commit]: https://github.com/thoughtbot/testing-rails/commit/944b0967232fe7bb623adbb36482ce3f76c7a037 224 | -------------------------------------------------------------------------------- /book/types_of_tests/feature_specs/voting_on_links.md: -------------------------------------------------------------------------------- 1 | ### Voting on links 2 | 3 | One of the most important parts of Reddit is being able to vote for posts. Let's 4 | implement a basic version of this functionality in our app, where you can upvote 5 | and downvote links. 6 | 7 | Here's a basic test for upvoting links: 8 | 9 | ` spec/features/user_upvotes_a_link_spec.rb@d4001c148:3,15 10 | 11 | There are a couple new things in this test. First is the `within` block. 12 | `within` takes a selector and looks for a matching element on the page. It then 13 | limits the scope of everything within the block to elements inside the specified 14 | element. In this case, our page has a potential to have multiple links or other 15 | instances of the word "Upvote". We scope our finder to only look for that text 16 | within the list element for our link. We use the CSS id `#link_#{link.id}` which 17 | is given by `content_tag_for`. 18 | 19 | The second new method is `has_css`, which asserts that a given selector is on 20 | the page. With the `text` option, it ensures that the provided text is found 21 | within the given selector. The selector I use includes a data attribute: 22 | `[data-role=score]`. We'll frequently use `data-role`s to decouple our test 23 | logic from our presentation logic. This way, we can change class names and tags 24 | without breaking our tests! 25 | -------------------------------------------------------------------------------- /book/types_of_tests/helper_specs.md: -------------------------------------------------------------------------------- 1 | ## Helper Specs 2 | 3 | Helpers are generally one-off functions that don't really fit anywhere else. 4 | They can be particularly easy to test due to their small scope and lack of 5 | side-effects. 6 | 7 | We will add some formatting to the display of a link's score. While a high score 8 | means that a link is popular, a low score can have multiple meanings. Is it new? 9 | Is it controversial and thus has a high number of both positive and negative 10 | votes? Is it just boring? 11 | 12 | To make some of the context more obvious, we will format the score as `5 (+7, 13 | -2)` instead of just showing the net score. 14 | 15 | <<[types_of_tests/helper_specs/formatting_the_score.md] 16 | -------------------------------------------------------------------------------- /book/types_of_tests/helper_specs/formatting_the_score.md: -------------------------------------------------------------------------------- 1 | ### Formatting the score 2 | 3 | Formatting is not a model-level concern. Instead, we are going to implement it 4 | as a helper method. In TDD fashion we start with a test: 5 | 6 | ` spec/helpers/application_helper_spec.rb@6335cb210b8e83f5 7 | 8 | Since we don't need to persist to the database and don't care about validity, we 9 | are using `Link.new` here instead of `FactoryBot`. 10 | 11 | Helpers are modules. Because of this, we can't instantiate them to test inside a 12 | spec, instead they must be mixed into an object. RSpec helps us out here by 13 | providing the `helper` object that automatically mixes in the described helper. 14 | All of the methods on the helper can be called on `helper`. 15 | 16 | It is worth noting here that this is not a pure unit test since it depends on 17 | both the helper *and* the `Link` model. In a later chapter, we will talk about 18 | **doubles** and how they can be used to isolate code from its collaborators. 19 | -------------------------------------------------------------------------------- /book/types_of_tests/mailer_specs.md: -------------------------------------------------------------------------------- 1 | ## Mailer Specs 2 | 3 | As with every other part of your Rails application, mailers should be tested at 4 | an integration and unit level to ensure they work and will continue to work as 5 | expected. 6 | 7 | Say we send an email to moderators when a new link is added to the site. 8 | Following an outside-in development process, we'll start with an integration 9 | level test. In this case, a controller spec: 10 | 11 | ` spec/controllers/links_controller_spec.rb@db8110e40a4cc40a:15,25 12 | 13 | This test introduces some new methods. We'll discuss the intricacies of how this 14 | works in [testing side effects](#testing-side-effects). For now, just realize 15 | that we've set up an expectation to check that when a link is created, we call 16 | the method `LinkMailer#new_link`. With this in place, we can be comfortable that 17 | when we enter the conditional in our controller, that method is called. We'll 18 | test what that method does in our unit test. 19 | 20 | The above spec would lead to a controller action like this: 21 | 22 | ` app/controllers/links_controller.rb@b6755ba1b764d1d1:14,23 23 | 24 | This now forces us to write a new class and method `LinkMailer#new_link`. 25 | 26 | #### LinkMailer#new_link 27 | 28 | Before writing our test we'll install the [`email-spec`] gem, which provides a 29 | number of helpful matchers for testing mailers, such as: 30 | 31 | * `deliver_to` 32 | * `deliver_from` 33 | * `have_subject` 34 | * `have_body_text` 35 | 36 | [`email-spec`]: https://github.com/email-spec/email-spec 37 | 38 | With the gem installed and setup, we can write our test: 39 | 40 | ` spec/mailers/link_mailer_spec.rb@4c52a0167e5b5240 41 | 42 | This test confirms our `to`, `from`, `subject` and `body` are what we expect. 43 | That should give us enough coverage to be confident in this mailer, and allow us 44 | to write our mailer code: 45 | 46 | ` app/mailers/link_mailer.rb@4c52a0167e5b5240 47 | -------------------------------------------------------------------------------- /book/types_of_tests/model_specs.md: -------------------------------------------------------------------------------- 1 | ## Model Specs 2 | 3 | As you can probably guess, model specs are specs for testing your Rails models. 4 | If you've written unit tests before, they may seem similar, although many model 5 | specs will interact with the database due to the model's dependency on 6 | ActiveRecord, so they are not truly unit tests. 7 | 8 | <<[types_of_tests/model_specs/instance_methods.md] 9 | 10 | <<[types_of_tests/model_specs/class_methods.md] 11 | 12 | <<[types_of_tests/model_specs/validations_and_associations.md] 13 | -------------------------------------------------------------------------------- /book/types_of_tests/model_specs/class_methods.md: -------------------------------------------------------------------------------- 1 | ### Class Methods 2 | 3 | Testing class methods works similarly to testing instance methods. I [added some 4 | code] to sort the links from highest to lowest score. To keep our business logic 5 | in our models, I decided to implement a `.hottest_first` method to keep that 6 | logic out of the controller. 7 | 8 | [added some code]: https://github.com/thoughtbot/testing-rails/commit/688743177f5ba0c5c0a4a6fdf4446cf8aedcc4a1 9 | 10 | We order our model specs as close as possible to how we order our model's 11 | methods. Thus, I added the spec for our new class method under the validations 12 | tests and above the instance method tests. 13 | 14 | ` spec/models/link_spec.rb@ef04e8996:8,16 15 | 16 | This is a fairly common pattern, as many of our ActiveRecord model class methods 17 | are for sorting or filtering. The interesting thing to note here is that I 18 | intentionally scramble the order of the created links. I've also chosen numbers 19 | for the upvotes and downvotes to ensure that the test will fail if we 20 | incidentally are testing something other than what we intend. For example, if we 21 | accidentally implemented our method to sort by upvotes, the test would still 22 | fail. 23 | -------------------------------------------------------------------------------- /book/types_of_tests/model_specs/instance_methods.md: -------------------------------------------------------------------------------- 1 | ### Instance Methods 2 | 3 | In the last chapter, we added functionality for users to vote on links with some 4 | instance methods on our `Link` class to help with this. 5 | 6 | #### Link#upvote 7 | 8 | The first method is `#upvote`, which increments the `upvotes` count on the 9 | link by 1. A simple way to test this behavior is to instantiate an object with a 10 | known upvote count, call our `#upvote` method, and then verify that the new 11 | upvote count is what we expect. A test for that might look like this: 12 | 13 | ` spec/models/link_spec.rb@d4001c148:8,16 14 | 15 | `.describe` comes from RSpec and creates a group for whatever functionality you 16 | are describing. It takes a subject, in our case the `Link` class, and the 17 | behavior as a string. Typically, we'll use the name of our method, in this case 18 | `#upvote`. We prefix instance methods with a `#` and class methods with a `.`. 19 | 20 | ``` 21 | link = build(:link, upvotes: 1) 22 | ``` 23 | 24 | `.build` is another FactoryBot method. It's similar to `.create`, in that it 25 | instantiates an object based on our factory definition, however `.build` does 26 | not save the object. Whenever possible, we're going to favor `.build` over 27 | `.create`, as persisting to the database is one of the slowest operations in our 28 | tests. In this case, we don't care that the record was saved before we increment 29 | it so we use `.build`. If we needed a persisted object (for example, if we 30 | needed to query for it), we would use `.create`. 31 | 32 | You might ask, "Why not use `Link.new`?". Even though we don't save our record 33 | immediately, our call to `link.upvote` will, so we need a valid `Link`. Rather 34 | than worrying about what attributes need to be set to instantiate a valid 35 | instance, we depend on our factory definition as the single source of truth on 36 | how to build a valid record. 37 | 38 | Our _verify_ step is slightly different than we've seen in our feature specs. 39 | This time, we aren't asserting against the `page` (we don't even have access to 40 | the page, since this isn't a Capybara test). Instead, we're asserting against 41 | our system under test: the link. We're using a built in RSpec matcher `eq` to 42 | confirm that the *expected* value, `2`, matches the *actual* value of 43 | `link.upvotes`. 44 | 45 | With the test written, we can implement the method as such: 46 | 47 | ` app/models/link.rb@d4001c148:5,7 48 | 49 | #### Link#score 50 | 51 | Our score method should return the difference of the number of upvotes and 52 | downvotes. To test this, we can instantiate a link with a known upvote count and 53 | downvote count, then compare the expected and actual scores. 54 | 55 | ` spec/models/link_spec.rb@d4001c148:28,34 56 | 57 | In this test, you'll notice that we forgo FactoryBot and use plain ol' 58 | ActiveRecord to instantiate our object. `#score` depends on `#upvotes` and 59 | `#downvotes`, which we can set without saving our object. Since we never have to 60 | save our object, we don't need FactoryBot to set up a valid record. 61 | 62 | With a failing test, we can write our implementation: 63 | 64 | ` app/models/link.rb@d4001c148:13,15 65 | -------------------------------------------------------------------------------- /book/types_of_tests/model_specs/validations_and_associations.md: -------------------------------------------------------------------------------- 1 | ### Validations 2 | 3 | We use a library called [shoulda-matchers] to test validations. 4 | `shoulda-matchers` provides matchers for writing single line tests for common 5 | Rails functionality. Testing validations in your model is important, as it is 6 | unlikely validations will be tested anywhere else in your test suite. 7 | 8 | [shoulda-matchers]: https://github.com/thoughtbot/shoulda-matchers 9 | 10 | To use `shoulda-matchers`, add the gem to your Gemfile's `:test` group: 11 | 12 | ```ruby 13 | gem "shoulda-matchers" 14 | ``` 15 | 16 | After bundle installing, you can use the built in matchers ([see more 17 | online][shoulda-matchers]) like so: 18 | 19 | ```ruby 20 | RSpec.describe Link, "validations" do 21 | it { is_expected.to validate_presence_of(:title) } 22 | it { is_expected.to validate_presence_of(:url) } 23 | it { is_expected.to validate_uniqueness_of(:url) } 24 | end 25 | ``` 26 | 27 | `is_expected` is an RSpec method that makes it easier to write one line tests. 28 | The `it` these tests refer to is the test's `subject`, a method provided by 29 | RSpec when you pass a class as the first argument to `describe`. RSpec takes the 30 | subject you pass into `describe`, and instantiates a new object. In this case, 31 | `subject` returns `Link.new`. `is_expected` is a convenience syntax for 32 | `expect(subject)`. It reads a bit nicer when you read the whole line with the 33 | `it`. The following lines are roughly equivalent: 34 | 35 | ```ruby 36 | RSpec.describe Link, "validations" do 37 | it { expect(Link.new).to validate_presence_of(:title) } 38 | it { expect(subject).to validate_presence_of(:url) } 39 | it { is_expected.to validate_uniqueness_of(:url) } 40 | end 41 | ``` 42 | 43 | ### Associations 44 | 45 | While `shoulda-matchers` provides methods for testing associations, we've found 46 | that adding additional tests for associations is rarely worth it, as 47 | associations will be tested at an integration level. Since we haven't found them 48 | useful for catching regressions or for helping us drive our code, we have 49 | stopped using them. 50 | -------------------------------------------------------------------------------- /book/types_of_tests/request_specs.md: -------------------------------------------------------------------------------- 1 | ## Request Specs 2 | 3 | Request specs are integration tests that allow you to send a request and make 4 | assertions on its response. As end-to-end tests, they go through the entire 5 | Rails stack from route to response. Unlike feature specs, request specs do not 6 | work with Capybara. Instead of interacting with the page like you would with 7 | Capybara, you can only make basic assertions against the response, such as 8 | testing the status code, redirection, or that text appeared in the response 9 | body. 10 | 11 | Request specs should be used to test API design, as you want to be confident 12 | that the URLs in your API will not change. However, request specs can be used 13 | for any request, not just APIs. 14 | 15 | In this chapter, we'll add a basic API to our app to show how you might test one 16 | with request specs. 17 | 18 | <<[types_of_tests/request_specs/viewing_links.md] 19 | 20 | <<[types_of_tests/request_specs/creating_links.md] 21 | -------------------------------------------------------------------------------- /book/types_of_tests/request_specs/creating_links.md: -------------------------------------------------------------------------------- 1 | ### Creating links 2 | 3 | Next, we'll test creating a new link via our API: 4 | 5 | ` spec/requests/api/v1/links_spec.rb@8ca37400a:23,43 6 | 7 | `attributes_for` is another FactoryBot method, which gives you a hash of the 8 | attributes defined in your factory. In this case, it would return: 9 | 10 | ``` 11 | { title: "Testing Rails", url: "http://testingrailsbook.com" } 12 | ``` 13 | 14 | This time, we `POST` to `/api/v1/links`. `post` takes a second hash argument for 15 | the data to be sent to the server. We assert on the response status. `201` 16 | indicates that the request succeeded in creating a new record. We then check 17 | that the last `Link` has the title we expect to ensure it is creating a record 18 | using the data we submitted. 19 | 20 | In the second test, we introduce a new FactoryBot concept called traits. Traits 21 | are specialized versions of factories. To declare them, you nest them under 22 | a factory definition. This will give them all the attributes of the parent 23 | factory, as well as any of the modifications specified in the trait. With the 24 | new trait, our `Link` factory looks like this: 25 | 26 | ` spec/factories.rb@8ca37400a:2,9 27 | 28 | The `:invalid` trait nulls out the `title` field so we can easily create invalid 29 | records in a reusable manner. 30 | -------------------------------------------------------------------------------- /book/types_of_tests/request_specs/viewing_links.md: -------------------------------------------------------------------------------- 1 | ### Viewing links 2 | 3 | The first endpoint we'll create is for an index of all existing links, from 4 | hottest to coldest. We'll namespace everything under `/api/v1`. 5 | 6 | ` spec/requests/api/v1/links_spec.rb@f39adb6ff 7 | 8 | We name our request spec files after the paths they test. In this case requests 9 | to `/api/v1/links` will be tested in `spec/requests/api/v1/links_spec.rb`. 10 | 11 | After setting up our data, we make a `GET` request with the built-in `get` method. We 12 | then assert on the number of records returned in the JSON payload. Since all of 13 | our requests will be JSON, and we are likely to be parsing each of them, I've 14 | extracted a method `json_body` that parses the `response` object that is 15 | provided by `rack-test`. 16 | 17 | ` spec/support/api_helpers.rb@f39adb6ff 18 | 19 | I pulled the method out to its own file in `spec/support`, and include it 20 | automatically in all request specs. 21 | 22 | We could have tested the entire body of the response, but that would have been 23 | cumbersome to write. Asserting upon the length of the response and the structure 24 | of the first JSON object should be enough to have reasonable confidence that 25 | this is working properly. 26 | -------------------------------------------------------------------------------- /book/types_of_tests/testing_pyramid.md: -------------------------------------------------------------------------------- 1 | ## The Testing Pyramid 2 | 3 | The various test types we are about to look at fall along a spectrum. At one end 4 | are **unit tests**. These test individual components in isolation, proving that 5 | they implement the expected behavior independent of the surrounding system. 6 | Because of this, unit tests are usually small and fast. 7 | 8 | In the real world, these components don't exist in a vacuum: they have to 9 | interact with each other. One component may expect a collaborator to have a 10 | particular interface when in fact it has completely different one. Even though 11 | all the tests pass, the software as a whole is broken. 12 | 13 | This is where **integration tests** come in. These tests exercise the system as 14 | a whole rather than its individual components. They typically do so by 15 | simulating a user trying to accomplish a task in our software. Instead of being 16 | concerned with invoking methods or calling out to collaborators, integration 17 | tests are all about clicking and typing as a user. 18 | 19 | Although this is quite effective for proving that we have working software, it 20 | comes at a cost. Integration tests tend to be much slower and more brittle than 21 | their unit counterparts. 22 | 23 | Many test types are neither purely unit nor integration tests. Instead, they lie 24 | somewhere in between, testing several components together but not the full 25 | system. 26 | 27 | ![Rails Test Types](../images/rails-test-types.png) 28 | 29 | We like to build our test suite using a combination of these to create a 30 | [**testing pyramid**](http://martinfowler.com/bliki/TestPyramid.html). This is a 31 | suite that has a few high-level integration tests that cover the general 32 | functionality of the app, several intermediate-level tests that cover a 33 | sub-system in more detail, and many unit tests to cover the nitty-gritty details 34 | of each component. 35 | 36 | This approach plays to the strength of each type of test while attempting to 37 | minimize the downsides they have (such as slow run times). 38 | -------------------------------------------------------------------------------- /book/types_of_tests/view_specs.md: -------------------------------------------------------------------------------- 1 | ## View Specs 2 | 3 | View specs allow you to test the logic in your views. While this logic should be 4 | minimal, there are certainly times where you'll want to pull out the handy view 5 | spec to test some critical functionality. A common antipattern in test suites is 6 | testing too much in feature specs, which tend to be slow. This is especially a 7 | problem when you have multiple tests covering similar functionality, with minor 8 | variations. 9 | 10 | In this section, we'll allow image links to be rendered inline. The main 11 | functionality of displaying link posts was tested previously in a feature spec. 12 | Aside from the already tested logic for creating a link, rendering a link post 13 | as an inline image is mostly view logic. Instead of duplicating that 14 | functionality in another feature spec, we'll write a view spec, which should 15 | cover our use case and minimize test suite runtime. 16 | 17 | <<[types_of_tests/view_specs/rendering_images_inline.md] 18 | -------------------------------------------------------------------------------- /book/types_of_tests/view_specs/rendering_images_inline.md: -------------------------------------------------------------------------------- 1 | ### Rendering Images Inline 2 | 3 | In order to keep our link rendering logic DRY, I moved all of it into 4 | `app/views/links/_link.html.erb`. This way, we can reuse that partial anywhere 5 | we want to display a link, and it can correctly render with or without the image 6 | tag. 7 | 8 | The associated spec looks like this: 9 | 10 | ` spec/views/links/_link.html.erb_spec.rb@adc60fb1b9d83339 11 | 12 | In this spec, we build a link with an image URL, then `render` our partial with 13 | our link as a local variable. We then make a simple assertion that the image 14 | appears in the rendered HTML. 15 | 16 | When I initially implemented this partial, I had forgotten to also render the 17 | image on the link's show page. Since some functionality I expected to see wasn't 18 | implemented, I wrote a test to cover that case as well. 19 | 20 | ` spec/views/links/show.html.erb_spec.rb@adc60fb1b9d83339 21 | 22 | This test is similar to the previous one, but this time we are rendering a view 23 | as opposed to a partial view. First, instead of a local variable we need to 24 | assign an instance variable. `assign(:link, link)` will assign the value of the 25 | variable `link` to the instance variable `@link` in our rendered view. 26 | 27 | Instead of specifying the view to render, this time we let RSpec work its 28 | "magic". RSpec infers the view it should render based on the name of the file in 29 | the `describe` block. 30 | -------------------------------------------------------------------------------- /example_app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | !/log/.keep 17 | /tmp 18 | -------------------------------------------------------------------------------- /example_app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/.gitkeep -------------------------------------------------------------------------------- /example_app/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /example_app/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.0 2 | -------------------------------------------------------------------------------- /example_app/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "2.2.0" 4 | 5 | gem "coffee-rails", "~> 4.1.0" 6 | gem "jquery-rails" 7 | gem "rails", "4.2.1" 8 | gem "sass-rails", "~> 5.0" 9 | gem "sqlite3" 10 | gem "turbolinks" 11 | gem "uglifier", ">= 1.3.0" 12 | gem "active_model_serializers", "~> 0.8.0" 13 | 14 | group :development, :test do 15 | gem "byebug" 16 | gem "factory_bot_rails" 17 | gem "rspec-rails", "~> 3.0" 18 | gem "shoulda-matchers" 19 | gem "spring" 20 | gem "web-console", "~> 2.0" 21 | end 22 | 23 | group :test do 24 | gem "capybara" 25 | gem "database_cleaner" 26 | gem "email_spec" 27 | end 28 | -------------------------------------------------------------------------------- /example_app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.1) 5 | actionpack (= 4.2.1) 6 | actionview (= 4.2.1) 7 | activejob (= 4.2.1) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.1) 11 | actionview (= 4.2.1) 12 | activesupport (= 4.2.1) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.1) 18 | activesupport (= 4.2.1) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | active_model_serializers (0.8.3) 24 | activemodel (>= 3.0) 25 | activejob (4.2.1) 26 | activesupport (= 4.2.1) 27 | globalid (>= 0.3.0) 28 | activemodel (4.2.1) 29 | activesupport (= 4.2.1) 30 | builder (~> 3.1) 31 | activerecord (4.2.1) 32 | activemodel (= 4.2.1) 33 | activesupport (= 4.2.1) 34 | arel (~> 6.0) 35 | activesupport (4.2.1) 36 | i18n (~> 0.7) 37 | json (~> 1.7, >= 1.7.7) 38 | minitest (~> 5.1) 39 | thread_safe (~> 0.3, >= 0.3.4) 40 | tzinfo (~> 1.1) 41 | addressable (2.4.0) 42 | arel (6.0.0) 43 | binding_of_caller (0.7.2) 44 | debug_inspector (>= 0.0.1) 45 | builder (3.2.2) 46 | byebug (4.0.5) 47 | columnize (= 0.9.0) 48 | capybara (2.4.4) 49 | mime-types (>= 1.16) 50 | nokogiri (>= 1.3.3) 51 | rack (>= 1.0.0) 52 | rack-test (>= 0.5.4) 53 | xpath (~> 2.0) 54 | coffee-rails (4.1.0) 55 | coffee-script (>= 2.2.0) 56 | railties (>= 4.0.0, < 5.0) 57 | coffee-script (2.4.1) 58 | coffee-script-source 59 | execjs 60 | coffee-script-source (1.9.1) 61 | columnize (0.9.0) 62 | database_cleaner (1.4.0) 63 | debug_inspector (0.0.2) 64 | diff-lcs (1.2.5) 65 | email_spec (2.0.0) 66 | htmlentities (~> 4.3.3) 67 | launchy (~> 2.1) 68 | mail (~> 2.6.3) 69 | erubis (2.7.0) 70 | execjs (2.5.0) 71 | factory_bot (4.8.2) 72 | activesupport (>= 3.0.0) 73 | factory_bot_rails (4.8.2) 74 | factory_bot (~> 4.8.2) 75 | railties (>= 3.0.0) 76 | globalid (0.3.4) 77 | activesupport (>= 4.1.0) 78 | hike (1.2.3) 79 | htmlentities (4.3.4) 80 | i18n (0.7.0) 81 | jquery-rails (4.0.3) 82 | rails-dom-testing (~> 1.0) 83 | railties (>= 4.2.0) 84 | thor (>= 0.14, < 2.0) 85 | json (1.8.2) 86 | launchy (2.4.3) 87 | addressable (~> 2.3) 88 | loofah (2.0.1) 89 | nokogiri (>= 1.5.9) 90 | mail (2.6.3) 91 | mime-types (>= 1.16, < 3) 92 | mime-types (2.4.3) 93 | mini_portile (0.6.2) 94 | minitest (5.5.1) 95 | multi_json (1.11.0) 96 | nokogiri (1.6.6.2) 97 | mini_portile (~> 0.6.0) 98 | rack (1.6.0) 99 | rack-test (0.6.3) 100 | rack (>= 1.0) 101 | rails (4.2.1) 102 | actionmailer (= 4.2.1) 103 | actionpack (= 4.2.1) 104 | actionview (= 4.2.1) 105 | activejob (= 4.2.1) 106 | activemodel (= 4.2.1) 107 | activerecord (= 4.2.1) 108 | activesupport (= 4.2.1) 109 | bundler (>= 1.3.0, < 2.0) 110 | railties (= 4.2.1) 111 | sprockets-rails 112 | rails-deprecated_sanitizer (1.0.3) 113 | activesupport (>= 4.2.0.alpha) 114 | rails-dom-testing (1.0.6) 115 | activesupport (>= 4.2.0.beta, < 5.0) 116 | nokogiri (~> 1.6.0) 117 | rails-deprecated_sanitizer (>= 1.0.1) 118 | rails-html-sanitizer (1.0.2) 119 | loofah (~> 2.0) 120 | railties (4.2.1) 121 | actionpack (= 4.2.1) 122 | activesupport (= 4.2.1) 123 | rake (>= 0.8.7) 124 | thor (>= 0.18.1, < 2.0) 125 | rake (10.4.2) 126 | rspec-core (3.1.7) 127 | rspec-support (~> 3.1.0) 128 | rspec-expectations (3.1.2) 129 | diff-lcs (>= 1.2.0, < 2.0) 130 | rspec-support (~> 3.1.0) 131 | rspec-mocks (3.1.3) 132 | rspec-support (~> 3.1.0) 133 | rspec-rails (3.1.0) 134 | actionpack (>= 3.0) 135 | activesupport (>= 3.0) 136 | railties (>= 3.0) 137 | rspec-core (~> 3.1.0) 138 | rspec-expectations (~> 3.1.0) 139 | rspec-mocks (~> 3.1.0) 140 | rspec-support (~> 3.1.0) 141 | rspec-support (3.1.2) 142 | sass (3.4.13) 143 | sass-rails (5.0.3) 144 | railties (>= 4.0.0, < 5.0) 145 | sass (~> 3.1) 146 | sprockets (>= 2.8, < 4.0) 147 | sprockets-rails (>= 2.0, < 4.0) 148 | tilt (~> 1.1) 149 | shoulda-matchers (2.7.0) 150 | activesupport (>= 3.0.0) 151 | spring (1.3.4) 152 | sprockets (2.12.3) 153 | hike (~> 1.2) 154 | multi_json (~> 1.0) 155 | rack (~> 1.0) 156 | tilt (~> 1.1, != 1.3.0) 157 | sprockets-rails (2.2.4) 158 | actionpack (>= 3.0) 159 | activesupport (>= 3.0) 160 | sprockets (>= 2.8, < 4.0) 161 | sqlite3 (1.3.10) 162 | thor (0.19.1) 163 | thread_safe (0.3.5) 164 | tilt (1.4.1) 165 | turbolinks (2.5.3) 166 | coffee-rails 167 | tzinfo (1.2.2) 168 | thread_safe (~> 0.1) 169 | uglifier (2.7.1) 170 | execjs (>= 0.3.0) 171 | json (>= 1.8.0) 172 | web-console (2.1.2) 173 | activemodel (>= 4.0) 174 | binding_of_caller (>= 0.7.2) 175 | railties (>= 4.0) 176 | sprockets-rails (>= 2.0, < 4.0) 177 | xpath (2.0.0) 178 | nokogiri (~> 1.3) 179 | 180 | PLATFORMS 181 | ruby 182 | 183 | DEPENDENCIES 184 | active_model_serializers (~> 0.8.0) 185 | byebug 186 | capybara 187 | coffee-rails (~> 4.1.0) 188 | database_cleaner 189 | email_spec 190 | factory_bot_rails 191 | jquery-rails 192 | rails (= 4.2.1) 193 | rspec-rails (~> 3.0) 194 | sass-rails (~> 5.0) 195 | shoulda-matchers 196 | spring 197 | sqlite3 198 | turbolinks 199 | uglifier (>= 1.3.0) 200 | web-console (~> 2.0) 201 | 202 | BUNDLED WITH 203 | 1.11.2 204 | -------------------------------------------------------------------------------- /example_app/README.md: -------------------------------------------------------------------------------- 1 | # Reddat 2 | 3 | A Reddit clone for the Testing Rails book. 4 | 5 | ## Starting Reddat 6 | 7 | 1. Run `bin/setup` to install dependencies and setup the database. 8 | 1. Start app with `rails server`. 9 | 10 | Now you can visit http://localhost:3000 from your browser. 11 | 12 | ## Testing 13 | 14 | Run the entire suite with: 15 | 16 | ``` 17 | rspec 18 | ``` 19 | 20 | Run individual tests with: 21 | 22 | ``` 23 | rspec path/to/individual_spec.rb 24 | ``` 25 | -------------------------------------------------------------------------------- /example_app/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /example_app/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/app/assets/images/.keep -------------------------------------------------------------------------------- /example_app/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require turbolinks 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /example_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /example_app/app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BaseController < ApplicationController 2 | protect_from_forgery with: :null_session 3 | end 4 | -------------------------------------------------------------------------------- /example_app/app/controllers/api/v1/links_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::LinksController < Api::BaseController 2 | def index 3 | links = Link.hottest_first 4 | render json: links 5 | end 6 | 7 | def create 8 | link = Link.new(link_params) 9 | 10 | if link.save 11 | render json: link, status: :created 12 | else 13 | render json: { errors: link.errors.full_messages }, 14 | status: :unprocessable_entity 15 | end 16 | end 17 | 18 | private 19 | 20 | def link_params 21 | params.require(:link).permit(:title, :url) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /example_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /example_app/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /example_app/app/controllers/downvotes_controller.rb: -------------------------------------------------------------------------------- 1 | class DownvotesController < ApplicationController 2 | def create 3 | link = Link.find(params[:link_id]) 4 | link.downvote 5 | 6 | redirect_to :back 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /example_app/app/controllers/links_controller.rb: -------------------------------------------------------------------------------- 1 | class LinksController < ApplicationController 2 | def index 3 | @links = Link.hottest_first 4 | end 5 | 6 | def show 7 | @link = Link.find(params[:id]) 8 | end 9 | 10 | def new 11 | @link = Link.new 12 | end 13 | 14 | def create 15 | @link = Link.new(link_params) 16 | 17 | if @link.save 18 | LinkMailer.new_link(@link) 19 | redirect_to link_path(@link) 20 | else 21 | render :new 22 | end 23 | end 24 | 25 | private 26 | 27 | def link_params 28 | params.require(:link).permit(:title, :url) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /example_app/app/controllers/new_links_controller.rb: -------------------------------------------------------------------------------- 1 | class NewLinksController < ApplicationController 2 | def index 3 | @links = Link.newest_first 4 | 5 | render "links/index" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example_app/app/controllers/upvotes_controller.rb: -------------------------------------------------------------------------------- 1 | class UpvotesController < ApplicationController 2 | def create 3 | link = Link.find(params[:link_id]) 4 | link.upvote 5 | 6 | redirect_to :back 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /example_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def formatted_score_for(score) 3 | "#{score.value} (+#{score.upvotes}, -#{score.downvotes})" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /example_app/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/app/mailers/.keep -------------------------------------------------------------------------------- /example_app/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "noreply@example.com" 3 | end 4 | -------------------------------------------------------------------------------- /example_app/app/mailers/link_mailer.rb: -------------------------------------------------------------------------------- 1 | class LinkMailer < ApplicationMailer 2 | MODERATOR_EMAILS = "moderators@example.com" 3 | 4 | default from: "noreply@reddat.com" 5 | 6 | def new_link(link) 7 | @link = link 8 | mail(to: MODERATOR_EMAILS, subject: "New link submitted") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /example_app/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/app/models/.keep -------------------------------------------------------------------------------- /example_app/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/app/models/concerns/.keep -------------------------------------------------------------------------------- /example_app/app/models/link.rb: -------------------------------------------------------------------------------- 1 | class Link < ActiveRecord::Base 2 | IMAGE_FORMATS = %w(.jpg .gif .png) 3 | 4 | validates :title, presence: true 5 | validates :url, presence: true 6 | 7 | def self.hottest_first 8 | order("upvotes - downvotes DESC") 9 | end 10 | 11 | def self.newest_first 12 | order(created_at: :desc) 13 | end 14 | 15 | def upvote 16 | increment!(:upvotes) 17 | end 18 | 19 | def downvote 20 | increment!(:downvotes) 21 | end 22 | 23 | def score 24 | Score.new(self) 25 | end 26 | 27 | def image? 28 | url.end_with? *IMAGE_FORMATS 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /example_app/app/models/score.rb: -------------------------------------------------------------------------------- 1 | class Score 2 | CONTROVERSIAL_THRESHOLD = 0.2 3 | 4 | attr_reader :upvotes, :downvotes 5 | 6 | def initialize(link) 7 | @upvotes = link.upvotes 8 | @downvotes = link.downvotes 9 | end 10 | 11 | def value 12 | upvotes - downvotes 13 | end 14 | 15 | def controversial? 16 | score_delta_percentage < CONTROVERSIAL_THRESHOLD 17 | end 18 | 19 | def ==(other) 20 | other.upvotes == upvotes && other.downvotes == downvotes 21 | end 22 | 23 | alias :eql? :== 24 | 25 | def hash 26 | [upvotes, downvotes].hash 27 | end 28 | 29 | private 30 | 31 | def score_delta_percentage 32 | score_delta.to_f / high_score 33 | end 34 | 35 | def high_score 36 | [upvotes, downvotes].max 37 | end 38 | 39 | def score_delta 40 | value.abs 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /example_app/app/serializers/link_serializer.rb: -------------------------------------------------------------------------------- 1 | class LinkSerializer < ActiveModel::Serializer 2 | attributes :id, :title, :url, :upvotes, :downvotes 3 | end 4 | -------------------------------------------------------------------------------- /example_app/app/views/application/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if target.errors.any? %> 2 |
3 |

<%= pluralize(target.errors.count, "error") %> prohibited this record from being saved:

4 |
    5 | <% target.errors.full_messages.each do |message| %> 6 |
  • <%= message %>
  • 7 | <% end %> 8 |
9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /example_app/app/views/application/_navigation.html.erb: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /example_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reddat 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= render "navigation" %> 12 | 13 | <%= yield %> 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example_app/app/views/link_mailer/new_link.html.erb: -------------------------------------------------------------------------------- 1 |

A new link has been posted

2 |

3 | View it <%= link_to "here", @link %>! 4 |

5 | -------------------------------------------------------------------------------- /example_app/app/views/link_mailer/new_link.text.erb: -------------------------------------------------------------------------------- 1 | A new link has been posted 2 | 3 | View it <%= link_to "here", @link %>! 4 | -------------------------------------------------------------------------------- /example_app/app/views/links/_link.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to link.url do %> 2 | <%= link.title %> 3 | 4 | <% if link.image? %> 5 | <%= image_tag link.url %> 6 | <% end %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /example_app/app/views/links/_link_list_item.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_tag_for :li, link do %> 2 | <%= link_to "Upvote", [link, :upvote], method: :post %> 3 |
<%= formatted_score_for(link.score) %>
4 | <%= link_to "Downvote", [link, :downvote], method: :post %> 5 | 6 | <%= render link %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /example_app/app/views/links/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to "Submit a new link!", new_link_path %> 2 | 3 | 6 | -------------------------------------------------------------------------------- /example_app/app/views/links/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for @link do |form| %> 2 | <%= render "error_messages", target: @link %> 3 | 4 |
5 | <%= form.label :title %> 6 | <%= form.text_field :title %> 7 |
8 | 9 |
10 | <%= form.label :url %> 11 | <%= form.text_field :url %> 12 |
13 | 14 | <%= form.submit "Submit!" %> 15 | <% end %> 16 | -------------------------------------------------------------------------------- /example_app/app/views/links/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= render @link %> 2 | -------------------------------------------------------------------------------- /example_app/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /example_app/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /example_app/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require_relative '../config/boot' 7 | require 'rake' 8 | Rake.application.run 9 | -------------------------------------------------------------------------------- /example_app/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt > /dev/null 2>&1" 29 | end 30 | -------------------------------------------------------------------------------- /example_app/bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) 11 | Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } 12 | gem "spring", match[1] 13 | require "spring/binstub" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /example_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "sprockets/railtie" 12 | # require "rails/test_unit/railtie" 13 | 14 | # Require the gems listed in Gemfile, including any gems 15 | # you've limited to :test, :development, or :production. 16 | Bundler.require(*Rails.groups) 17 | 18 | module Reddat 19 | class Application < Rails::Application 20 | # Settings in config/environments/* take precedence over those specified here. 21 | # Application configuration should go into files in config/initializers 22 | # -- all .rb files in that directory are automatically loaded. 23 | 24 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 25 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 26 | # config.time_zone = 'Central Time (US & Canada)' 27 | 28 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 29 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 30 | # config.i18n.default_locale = :de 31 | 32 | # Do not swallow errors in after_commit/after_rollback callbacks. 33 | config.active_record.raise_in_transactional_callbacks = true 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /example_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /example_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /example_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /example_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | 42 | config.action_mailer.default_url_options = { host: "localhost:3000" } 43 | end 44 | -------------------------------------------------------------------------------- /example_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /example_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | 43 | config.action_mailer.default_url_options = { host: "localhost:3000" } 44 | end 45 | -------------------------------------------------------------------------------- /example_app/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /example_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /example_app/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /example_app/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /example_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /example_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /example_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_reddat_session' 4 | -------------------------------------------------------------------------------- /example_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /example_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /example_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :links, only: [:show, :new, :create] do 3 | resource :upvote, only: [:create] 4 | resource :downvote, only: [:create] 5 | end 6 | 7 | namespace :api do 8 | namespace :v1 do 9 | resources :links, only: [:index, :create] 10 | end 11 | end 12 | 13 | get "/new", to: "new_links#index", as: "new_links" 14 | 15 | root to: "links#index" 16 | end 17 | -------------------------------------------------------------------------------- /example_app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 2c1d987e4ea1df567f178ed2a5c2499b3166b6196bcc14e5ace6f00fb43c4cac9243aa3810c035973df7043aeae2937a17d18d7f54063d6c86751645aa68d8f7 15 | 16 | test: 17 | secret_key_base: 8465422ba59125852bacbef18c6cb14b667c731d8cd00ea0e49e03d104121c7b9297a38972189d6a78b7e94439cd23e2f410ca51491132214da45f4c7bcbbb4b 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /example_app/db/migrate/20150408194827_create_links.rb: -------------------------------------------------------------------------------- 1 | class CreateLinks < ActiveRecord::Migration 2 | def change 3 | create_table :links do |t| 4 | t.string :title, null: false 5 | t.string :url, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /example_app/db/migrate/20150504154305_add_upvotes_and_downvotes_to_links.rb: -------------------------------------------------------------------------------- 1 | class AddUpvotesAndDownvotesToLinks < ActiveRecord::Migration 2 | def change 3 | add_column :links, :upvotes, :integer, default: 0, null: false 4 | add_column :links, :downvotes, :integer, default: 0, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150504154305) do 15 | 16 | create_table "links", force: :cascade do |t| 17 | t.string "title", null: false 18 | t.string "url", null: false 19 | t.datetime "created_at", null: false 20 | t.datetime "updated_at", null: false 21 | t.integer "upvotes", default: 0, null: false 22 | t.integer "downvotes", default: 0, null: false 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /example_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /example_app/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/lib/assets/.keep -------------------------------------------------------------------------------- /example_app/lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/lib/tasks/.keep -------------------------------------------------------------------------------- /example_app/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/log/.keep -------------------------------------------------------------------------------- /example_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /example_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /example_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /example_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/public/favicon.ico -------------------------------------------------------------------------------- /example_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /example_app/spec/controllers/links_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe LinksController, "#create" do 4 | context "when the link is invalid" do 5 | it "re-renders the form" do 6 | invalid_link = double(save: false) 7 | allow(Link).to receive(:new).and_return(invalid_link) 8 | 9 | post :create, link: { attribute: "value" } 10 | 11 | expect(response).to render_template :new 12 | end 13 | end 14 | 15 | context "when the link is valid" do 16 | it "sends an email to the moderators" do 17 | valid_link = double(save: true) 18 | allow(Link).to receive(:new).and_return(valid_link) 19 | allow(LinkMailer).to receive(:new_link) 20 | 21 | post :create, link: { attribute: "value" } 22 | 23 | expect(LinkMailer).to have_received(:new_link).with(valid_link) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /example_app/spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :link do 3 | title "Testing Rails" 4 | url "http://testingrailsbook.com" 5 | 6 | trait :invalid do 7 | title nil 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /example_app/spec/features/user_downvotes_a_link_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.feature "User downvotes a link" do 4 | scenario "they see a decreased score" do 5 | link = create(:link, upvotes: 4) 6 | 7 | visit root_path 8 | 9 | within "#link_#{link.id}" do 10 | click_on "Downvote" 11 | end 12 | 13 | expect(page).to have_css "#link_#{link.id} [data-role=score]", text: "3" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example_app/spec/features/user_submits_a_link_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.feature "User submits a link" do 4 | scenario "they see the page for the submitted link" do 5 | link_title = "This Testing Rails book is awesome!" 6 | link_url = "http://testingrailsbook.com" 7 | 8 | visit root_path 9 | click_on "Submit a new link" 10 | fill_in "link_title", with: link_title 11 | fill_in "link_url", with: link_url 12 | click_on "Submit!" 13 | 14 | expect(page).to have_link link_title, href: link_url 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /example_app/spec/features/user_upvotes_a_link_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.feature "User upvotes a link" do 4 | scenario "they see an increased score" do 5 | link = create(:link) 6 | 7 | visit root_path 8 | 9 | within "#link_#{link.id}" do 10 | click_on "Upvote" 11 | end 12 | 13 | expect(page).to have_css "#link_#{link.id} [data-role=score]", text: "1" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example_app/spec/features/user_views_homepage_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.feature "User views homepage" do 4 | scenario "they see existing links" do 5 | link = create(:link) 6 | 7 | visit root_path 8 | 9 | expect(page).to have_link link.title, href: link.url 10 | end 11 | 12 | scenario "the links are sorted hottest to coldest" do 13 | create(:link, title: "Coldest", upvotes: 3, downvotes: 3) 14 | create(:link, title: "Hottest", upvotes: 5, downvotes: 1) 15 | create(:link, title: "Lukewarm", upvotes: 2, downvotes: 1) 16 | 17 | visit root_path 18 | 19 | expect(page).to have_css "#links li:nth-child(1)", text: "Hottest" 20 | expect(page).to have_css "#links li:nth-child(2)", text: "Lukewarm" 21 | expect(page).to have_css "#links li:nth-child(3)", text: "Coldest" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /example_app/spec/features/user_views_new_links_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.feature "User views new links" do 4 | scenario "the links are sorted newest to oldest" do 5 | create(:link, title: "Oldest", created_at: 1.year.ago) 6 | create(:link, title: "Middle", created_at: 1.week.ago) 7 | create(:link, title: "Newest", created_at: 1.day.ago) 8 | 9 | visit root_path 10 | click_on "new" 11 | 12 | expect(page).to have_css "#links li:nth-child(1)", text: "Newest" 13 | expect(page).to have_css "#links li:nth-child(2)", text: "Middle" 14 | expect(page).to have_css "#links li:nth-child(3)", text: "Oldest" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /example_app/spec/helpers/application_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe ApplicationHelper, "#formatted_score_for" do 4 | it "displays the net score along with the raw votes" do 5 | link = Link.new(upvotes: 7, downvotes: 2) 6 | score = Score.new(link) 7 | formatted_score = helper.formatted_score_for(score) 8 | 9 | expect(formatted_score).to eq "5 (+7, -2)" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example_app/spec/mailers/link_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe LinkMailer, "#new_link" do 4 | it "delivers a new link notification email" do 5 | link = build(:link) 6 | 7 | email = LinkMailer.new_link(link) 8 | 9 | expect(email).to deliver_to(LinkMailer::MODERATOR_EMAILS) 10 | expect(email).to deliver_from("noreply@reddat.com") 11 | expect(email).to have_subject("New link submitted") 12 | expect(email).to have_body_text("A new link has been posted") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /example_app/spec/models/link_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Link, "validations" do 4 | it { is_expected.to validate_presence_of(:title) } 5 | it { is_expected.to validate_presence_of(:url) } 6 | end 7 | 8 | RSpec.describe Link, ".hottest_first" do 9 | it "returns the links: hottest to coldest" do 10 | coldest_link = create(:link, upvotes: 3, downvotes: 3) 11 | hottest_link = create(:link, upvotes: 5, downvotes: 1) 12 | lukewarm_link = create(:link, upvotes: 2, downvotes: 1) 13 | 14 | expect(Link.hottest_first).to eq [hottest_link, lukewarm_link, coldest_link] 15 | end 16 | end 17 | 18 | RSpec.describe Link, ".newest_first" do 19 | it "returns the links: newest to oldest" do 20 | oldest_link = create(:link, created_at: 1.year.ago) 21 | middle_link = create(:link, created_at: 1.week.ago) 22 | newest_link = create(:link, created_at: 1.day.ago) 23 | 24 | expect(Link.newest_first).to eq [newest_link, middle_link, oldest_link] 25 | end 26 | end 27 | 28 | RSpec.describe Link, "#upvote" do 29 | it "increments upvotes" do 30 | link = build(:link, upvotes: 1) 31 | 32 | link.upvote 33 | 34 | expect(link.upvotes).to eq 2 35 | end 36 | end 37 | 38 | RSpec.describe Link, "#downvote" do 39 | it "increments downvotes" do 40 | link = build(:link, downvotes: 1) 41 | 42 | link.downvote 43 | 44 | expect(link.downvotes).to eq 2 45 | end 46 | end 47 | 48 | RSpec.describe Link, "#score" do 49 | it "returns the upvotes minus the downvotes" do 50 | link = Link.new(upvotes: 2, downvotes: 1) 51 | 52 | expect(link.score).to eq Score.new(link) 53 | end 54 | end 55 | 56 | RSpec.describe Link, "#image?" do 57 | %w(.jpg .gif .png).each do |extension| 58 | it "returns true if the URL ends in #{extension}" do 59 | link = Link.new(url: "http://example.com/a#{extension}") 60 | 61 | expect(link.image?).to be_truthy 62 | end 63 | end 64 | 65 | it "returns false if the URL does not have an image extension" do 66 | link = Link.new(url: "http://not-an-image") 67 | 68 | expect(link.image?).to be_falsey 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /example_app/spec/models/score_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Score do 4 | describe "#upvotes" do 5 | it "is the upvotes on the link" do 6 | link = instance_double(Link, upvotes: 10, downvotes: 0) 7 | score = Score.new(link) 8 | 9 | expect(score.upvotes).to eq 10 10 | end 11 | end 12 | 13 | describe "#downvotes" do 14 | it "is the downvotes on the link" do 15 | link = instance_double(Link, upvotes: 0, downvotes: 5) 16 | score = Score.new(link) 17 | 18 | expect(score.downvotes).to eq 5 19 | end 20 | end 21 | 22 | describe "#value" do 23 | it "is the difference between up and down votes" do 24 | link = instance_double(Link, upvotes: 10, downvotes: 3) 25 | score = Score.new(link) 26 | 27 | expect(score.value).to eq 7 28 | end 29 | end 30 | 31 | describe "#controversial?" do 32 | it "is true for posts where up/down votes are within 20% of each other" do 33 | controversial_link = instance_double(Link, upvotes: 10, downvotes: 9) 34 | score = Score.new(controversial_link) 35 | 36 | expect(score).to be_controversial 37 | end 38 | 39 | it "is false for posts where up/down votes have > 20% difference" do 40 | non_controversial_link = instance_double(Link, upvotes: 10, downvotes: 5) 41 | score = Score.new(non_controversial_link) 42 | 43 | expect(score).not_to be_controversial 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /example_app/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require 'spec_helper' 4 | require File.expand_path("../../config/environment", __FILE__) 5 | require 'rspec/rails' 6 | require 'capybara/rails' 7 | # Add additional requires below this line. Rails is not loaded until this point! 8 | 9 | # Requires supporting ruby files with custom matchers and macros, etc, in 10 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 11 | # run as spec files by default. This means that files in spec/support that end 12 | # in _spec.rb will both be required and run as specs, causing the specs to be 13 | # run twice. It is recommended that you do not name files matching this glob to 14 | # end with _spec.rb. You can configure this pattern with the --pattern 15 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 16 | # 17 | # The following line is provided for convenience purposes. It has the downside 18 | # of increasing the boot-up time by auto-requiring all files in the support 19 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 20 | # require only the support files necessary. 21 | 22 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 23 | 24 | # Checks for pending migrations before tests are run. 25 | # If you are not using ActiveRecord, you can remove this line. 26 | ActiveRecord::Migration.maintain_test_schema! 27 | 28 | RSpec.configure do |config| 29 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 30 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 31 | 32 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 33 | # examples within a transaction, remove the following line or assign false 34 | # instead of true. 35 | config.use_transactional_fixtures = true 36 | 37 | # RSpec Rails can automatically mix in different behaviours to your tests 38 | # based on their file location, for example enabling you to call `get` and 39 | # `post` in specs under `spec/controllers`. 40 | # 41 | # You can disable this behaviour by removing the line below, and instead 42 | # explicitly tag your specs with their type, e.g.: 43 | # 44 | # RSpec.describe UsersController, :type => :controller do 45 | # # ... 46 | # end 47 | # 48 | # The different available types are documented in the features, such as in 49 | # https://relishapp.com/rspec/rspec-rails/docs 50 | config.infer_spec_type_from_file_location! 51 | end 52 | -------------------------------------------------------------------------------- /example_app/spec/requests/api/v1/links_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "GET /api/v1/links" do 4 | it "returns a list of all links, hottest first" do 5 | coldest_link = create(:link) 6 | hottest_link = create(:link, upvotes: 2) 7 | 8 | get "/api/v1/links" 9 | 10 | expect(json_body["links"].count).to eq(2) 11 | 12 | hottest_link_json = json_body["links"][0] 13 | expect(hottest_link_json).to eq({ 14 | "id" => hottest_link.id, 15 | "title" => hottest_link.title, 16 | "url" => hottest_link.url, 17 | "upvotes" => hottest_link.upvotes, 18 | "downvotes" => hottest_link.downvotes, 19 | }) 20 | end 21 | end 22 | 23 | RSpec.describe "POST /api/v1/links" do 24 | it "creates the link" do 25 | link_params = attributes_for(:link) 26 | 27 | post "/api/v1/links", link: link_params 28 | 29 | expect(response.status).to eq 201 30 | expect(Link.last.title).to eq link_params[:title] 31 | end 32 | 33 | context "when there are invalid attributes" do 34 | it "returns a 422, with errors" do 35 | link_params = attributes_for(:link, :invalid) 36 | 37 | post "/api/v1/links", link: link_params 38 | 39 | expect(response.status).to eq 422 40 | expect(json_body.fetch("errors")).not_to be_empty 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example_app/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 4 | # file to always be loaded, without a need to explicitly require it in any files. 5 | # 6 | # Given that it is always loaded, you are encouraged to keep this file as 7 | # light-weight as possible. Requiring heavyweight dependencies from this file 8 | # will add to the boot time of your test suite on EVERY test run, even for an 9 | # individual file that may not need all of that loaded. Instead, consider making 10 | # a separate helper file that requires the additional dependencies and performs 11 | # the additional setup, and require it from the spec files that actually need it. 12 | # 13 | # The `.rspec` file also contains a few flags that are not defaults but that 14 | # users commonly want. 15 | # 16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 17 | RSpec.configure do |config| 18 | # rspec-expectations config goes here. You can use an alternate 19 | # assertion/expectation library such as wrong or the stdlib/minitest 20 | # assertions if you prefer. 21 | config.expect_with :rspec do |expectations| 22 | # This option will default to `true` in RSpec 4. It makes the `description` 23 | # and `failure_message` of custom matchers include text for helper methods 24 | # defined using `chain`, e.g.: 25 | # be_bigger_than(2).and_smaller_than(4).description 26 | # # => "be bigger than 2 and smaller than 4" 27 | # ...rather than: 28 | # # => "be bigger than 2" 29 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 30 | end 31 | 32 | # rspec-mocks config goes here. You can use an alternate test double 33 | # library (such as bogus or mocha) by changing the `mock_with` option here. 34 | config.mock_with :rspec do |mocks| 35 | # Prevents you from mocking or stubbing a method that does not exist on 36 | # a real object. This is generally recommended, and will default to 37 | # `true` in RSpec 4. 38 | mocks.verify_partial_doubles = true 39 | end 40 | 41 | # These two settings work together to allow you to limit a spec run 42 | # to individual examples or groups you care about by tagging them with 43 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 44 | # get run. 45 | # config.filter_run :focus 46 | # config.run_all_when_everything_filtered = true 47 | 48 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 49 | # For more details, see: 50 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 51 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 52 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 53 | config.disable_monkey_patching! 54 | 55 | # Many RSpec users commonly either run the entire suite or an individual 56 | # file, and it's useful to allow more verbose output when running an 57 | # individual spec file. 58 | if config.files_to_run.one? 59 | # Use the documentation formatter for detailed output, 60 | # unless a formatter has already been configured 61 | # (e.g. via a command-line flag). 62 | config.default_formatter = 'doc' 63 | end 64 | 65 | # Print the 10 slowest examples and example groups at the 66 | # end of the spec run, to help surface which specs are running 67 | # particularly slow. 68 | # config.profile_examples = 10 69 | 70 | # Run specs in random order to surface order dependencies. If you find an 71 | # order dependency and want to debug it, you can fix the order by providing 72 | # the seed, which is printed after each run. 73 | # --seed 1234 74 | config.order = :random 75 | 76 | # Seed global randomization in this process using the `--seed` CLI option. 77 | # Setting this allows you to use `--seed` to deterministically reproduce 78 | # test failures related to randomization by passing the same `--seed` value 79 | # as the one that triggered the failure. 80 | Kernel.srand config.seed 81 | end 82 | -------------------------------------------------------------------------------- /example_app/spec/support/api_helpers.rb: -------------------------------------------------------------------------------- 1 | module ApiHelpers 2 | def json_body 3 | JSON.parse(response.body) 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include ApiHelpers, type: :request 9 | end 10 | -------------------------------------------------------------------------------- /example_app/spec/support/email_spec.rb: -------------------------------------------------------------------------------- 1 | require "email_spec" 2 | 3 | RSpec.configure do |config| 4 | config.include(EmailSpec::Helpers) 5 | config.include(EmailSpec::Matchers) 6 | end 7 | -------------------------------------------------------------------------------- /example_app/spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryBot::Syntax::Methods 3 | 4 | config.before(:suite) do 5 | begin 6 | DatabaseCleaner.start 7 | FactoryBot.lint 8 | ensure 9 | DatabaseCleaner.clean 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /example_app/spec/views/links/_link.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "links/_link.html.erb" do 4 | context "if the url is an image" do 5 | it "renders the image inline" do 6 | link = build(:link, url: "http://example.com/image.jpg") 7 | 8 | render partial: "links/link.html.erb", locals: { link: link } 9 | 10 | expect(rendered).to have_selector "img[src='#{link.url}']" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example_app/spec/views/links/show.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "links/show.html.erb" do 4 | context "if the url is an image" do 5 | it "renders the image inline" do 6 | link = build(:link, url: "http://example.com/image.jpg") 7 | assign(:link, link) 8 | 9 | render 10 | 11 | expect(rendered).to have_selector "img[src='#{link.url}']" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /example_app/vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /example_app/vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/example_app/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /release/cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/cover.pdf -------------------------------------------------------------------------------- /release/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/cover.png -------------------------------------------------------------------------------- /release/images/cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/images/cover.pdf -------------------------------------------------------------------------------- /release/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/images/cover.png -------------------------------------------------------------------------------- /release/images/coverage-report-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/images/coverage-report-index.png -------------------------------------------------------------------------------- /release/images/coverage-report-show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/images/coverage-report-show.png -------------------------------------------------------------------------------- /release/images/rails-test-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/images/rails-test-types.png -------------------------------------------------------------------------------- /release/images/tdd-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/images/tdd-cycle.png -------------------------------------------------------------------------------- /release/tdd-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/tdd-cycle.png -------------------------------------------------------------------------------- /release/testing-rails.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/testing-rails.epub -------------------------------------------------------------------------------- /release/testing-rails.mobi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/testing-rails.mobi -------------------------------------------------------------------------------- /release/testing-rails.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/testing-rails/c07317c0329dd833166d17efb353e201d1942a04/release/testing-rails.pdf -------------------------------------------------------------------------------- /release/testing-rails.toc.html: -------------------------------------------------------------------------------- 1 |
2 |

Table of Contents

3 |
    4 |
  • Introduction
  • 5 |
  • Types of Tests
  • 6 |
  • Intermediate Testing
  • 7 |
  • Antipatterns
  • 8 |
  • Conclusion
  • 9 |
10 |
11 | --------------------------------------------------------------------------------