Skip to content

Latest commit

 

History

History

rspec

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Testing

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 explanation

    Example
    ## 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 explanation

    Example
    ## 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 over have_content link

  • Prefer click_on over click_link or click_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 link

    Example
    ## 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 or eql(true). link

    Example
    ## 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