In general we favor testing behavior and functionality over implementation details. This means we favor writing feature and request specs over unit/model/controller/view specs. For additional testing guidance, see also the READMEs in the spec directories:
These are some of the conventions we follow:
-
Avoid using
subject
,described_class
,shared_examples
,shared_context
link explanation -
Avoid using
context
link explanationExample
## Bad context "when condition A" do before do # setup for condition A end it "does something a" do # ... end it "does something b" do # ... end context "and another condition B" do before do # setup for condition B end it "does something c" do # ...where am I? What setup has been done? end end end ## Good it "does something a when condition A" do # setup for condition A # expect a end it "does something b when condition A" do # setup for condition A # expect b end it "does something c when condition A and condition B" do # setup for condition A # setup for condition B # expect c end
If setup is tedious, then we can introduce a helper method:
it "does something a when condition A" do setup_condition_a # expect a end it "does something b when condition A" do setup_condition_a # expect b end private def setup_condition_A # ... end
-
Avoid using
let
link explanationExample
## Bad let(:recipe) { create(:recipe) } it "does something" do end it "does something" do end it "does something" do end it "does something" do end it "spec faaaar down the page" do recipe.publish # huh? where does this recipe come from? end ## Good it "does something" do recipe = create(:recipe) recipe.publish end
-
Avoid adding dummy values that are not asserted in the spec link
Example
## Bad user = create(:user, name: "Name", profile_message: "Not Asserted") login(user) expect(page).to have_text("Name") ## Good user = create(:user, name: "Name") login(user) expect(page).to have_text("Name")
-
Only enable
:js
when the feature absolutely requires javascript link -
Prefer
have_text
overhave_content
link -
Prefer
click_on
overclick_link
orclick_button
link -
Prefer "descriptive" dummy data over realistic dummy data link
Example
## Bad alice = create(:user, name: "Alice") alice.friends << create(:user, name: "Bob") create(:user, name: "Mary") visit friends_path(alice) expect(page).to have_text("Bob") expect(page).to_not have_text("Mary") # Who is Bob/Mary and how are they related again? ## Good user = create(:user) user.friends << create(:user, name: "Friend of User") create(:user, name: "Not Friend of User") visit friends_path(user) expect(page).to have_text("Friend of User") expect(page).to_not have_text("Not Friend of User")
-
Do not use should when describing your tests link
Example
## Bad it "should deliver email" do end it "should not deliver email when user prints a recipe" do end it "should only send email that has been activated" do end it "should be enabled" do end it "should have custom headers" do end it "should by default be true" do end ## Good it "delivers email" do end it "does not deliver email when user prints a recipe" do end it "only sends email that has been activated" do end it "is enabled" do end it "has custom headers" do end it "defaults to true" do end
-
Avoid assertions tied to html classes or JavaScript-specific attributes link explanation
Example
## Bad create_list(3, :recipe) visit recipes_path expect(page).to have_css(".recipe", count: 3) ## Good create_list(3, :recipe, title: "Recipe Title") visit recipes_path expect(page).to have_text("Recipe Title", count: 3) ## Bad find("[data-action='replies#clearForm']").click ## Good click_on("Clear") # add title or aria-label if needed ## Bad within(".user-info") do click_on("Edit") end ## Better within("#user_info") do click_on("Edit") end ## Best click_on("Edit User Info") # f.ex. using unambiguous title="Edit User Info" or aria-label="Edit User Info" attribute
-
Use
actor_does_what_spec.rb
naming convention for feature specs linkExample
## Bad # /features/recipes_spec.rb feature "Recipes" do scenario "Author viewing own recipe shows welcome message" do # ... end scenario "Author has already seen welcome message" do # ... end end ## Good # /features/author_views_recipe_spec.rb feature "Author views recipe" do scenario "Shows welcome message" do # ... end scenario "Already seen welcome message" do # ... end end
-
Prefer multiple short & focused specs, over fewer long specs link
Example
## Bad scenario "Bookmarking and unbookmarking a recipe" do recipe = create(:recipe) visit recipe_path(recipe) click_on t("recipes.show.bookmark_link") expect(page).to have_text(t("bookmarks.bookmarked")) click_on t("recipes.show.unbookmark_link") expect(page).to have_text(t("bookmarks.unbookmarked")) end ## Good scenario "Bookmarking a recipe" do recipe = create(:recipe) visit recipe_path(recipe) click_on t("recipes.show.bookmark_link") expect(page).to have_text(t("bookmarks.bookmarked")) end scenario "Unbookmarking a recipe" do bookmark = create(:bookmark) visit recipe_path(bookmark.recipe) click_on t("recipes.show.unbookmark_link") expect(page).to have_text(t("bookmarks.unbookmarked")) end
-
Use I18n helpers when clicking/asserting link
Example
## Bad click_on "Sign up" expect(page).to have_text("Welcome!") ## Good click_on t("signups.new.signup_button") expect(page).to have_text t("signups.create.welcome_message")
-
Use minutes instead of seconds when setting timestamps for the purpose of asserting the order of results link
Example
## Bad: Causes random failures depending on the timing of each step older_recipe = create(:recipe, published_at: 2.seconds.ago) newest_recipe = create(:recipe, published_at: 1.second.ago) expect(Recipe.recently_published.first).to eq(newest_recipe) expect(Recipe.recently_published.last).to eq(older_recipe) ## Good older_recipe = create(:recipe, published_at: 2.minutes.ago) newest_recipe = create(:recipe, published_at: 1.minute.ago) expect(Recipe.recently_published.first).to eq(newest_recipe) expect(Recipe.recently_published.last).to eq(older_recipe)
-
Prefer splitting up longer specs and give each scenario its own spec, even if they require a similar setup link
Example
## Bad it "sanitizes recipe titles" do recipe = build(:recipe, title: "recipe title without capital letter") expect(recipe.title).to eq("Recipe title without capital letter") recipe = build(:recipe, title: "Recipe title with period Brand Name v1.0. ") expect(recipe.title).to eq("Recipe title with period Brand Name v1.0") recipe = build(:recipe, title: "Recipe title ( very good )") expect(recipe.title).to eq("Recipe title (very good)") recipe = build(:recipe, title: 'Recipe title " very good "') expect(recipe.title).to eq('Recipe title "very good"') end ## Good it "capitalizes the first letter in recipe titles" do recipe = build(:recipe, title: "recipe title without capital letter") expect(recipe.title).to eq("Recipe title without capital letter") end it "removes full stop from recipe titles" do recipe = build(:recipe, title: "Recipe title with period Brand Name v1.0. ") expect(recipe.title).to eq("Recipe title with period Brand Name v1.0") end it "removes spaces inside parens from recipe titles" do recipe = build(:recipe, title: "Recipe title ( very good )") expect(recipe.title).to eq("Recipe title (very good)") end it "removes spaces inside quotes from recipe titles" do recipe = build(:recipe, title: 'Recipe title " very good "') expect(recipe.title).to eq('Recipe title "very good"') end
-
Prefer using more descriptive RSpec matchers over the general
be_truthy
oreql(true)
. linkExample
## Bad expect(list.include?("soup")).to be_truthy # expected: truthy value # got: false expect(Achievement.unseen.exist?).to eql(false) # expected: false # got: true ## Good expect(list).to include("soup") # expected ["pizza", "pasta"] to include "soup" expect(Achievement.unseen).not_to exist # expected #<ActiveRecord::Relation [#<Achievement id: 1, user_id: 104, ...>]> not to exist
-
Lay out specs according to the Arrange, Act, Assert pattern. Each test should group these functional sections, separated by blank lines. You may also need to add a fourth teardown phase. If so, seperate this section with another blank line. link
Example
## Bad it "does something" do user = create(:user) recipe = create(:recipe, user: user) recipe.delete expect(recipe).to be_deleted end ## Good it "does something" do user = create(:user) recipe = create(:recipe, user: user) recipe.delete expect(recipe).to be_deleted end
-
Be methodical towards flaky specs. link explanation
-
Explicitly setup values used in assertions. link explanation
Example
## Bad it "does something" do recipe = create(:recipe) expect(recipe.title).to eq("Recipe Title") end ## Good it "does something" do recipe = create(:recipe, title: "Recipe Title") expect(recipe.title).to eq("Recipe Title") end