Rails book review app RSpec introduction

Introducing RSpec

Instead of the Rails standard minitest, we will introduce RSpec, which has a higher adoption rate than minitest in practice. We will learn more about ** TDD (Test Driven Development) and BDD (Behavior Driven Development) **, which are test methods.

various settings

Capybara ・ ・ ・ Simulates browser operation of web applications FactoryBot ・ ・ ・ A gem that supports the creation of a database for testing

Each Gem installation

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails',       '~> 4.0.0'
  gem 'factory_bot_rails', '~> 4.11'
  gem 'capybara',          '~> 2.13'
end

$bundle $bin/rails g rspec:install Delete minitest directory $rm -r ./test Write the following description in .rspec so that the execution result of test can be seen easily. --format documentation coloring --color

Initial preparation for Capybar

spec/spec_helper.rb


require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end

webdrivers settings

Install webdrivers that will easily install Chrome Driver Added gem'webdrivers' $ mkdir spec/support $ touch spec/support/driver_setting.rb

spec/support/driver_setting.rb


RSpec.configure do |config|
  config.before(:each, type: :system) do
    #When executing Spec, the browser can automatically start up and check the behavior.
    # driven_by(:selenium_chrome)

    #Browser OFF when Spec is executed
    driven_by(:selenium_chrome_headless)
  end
end

Prepare to create test data in FactoryBot

Normally you can't call a method without a FactoryBot user = FactoryBot.create(:user)

By adding the above settings, the description of FactoryBot can be omitted. user = create(:user)

spec/rails_helper.rb


require "support/factory_bot"

spec/support/factory_bot.rb


RSpec.configure do |config|
  #FactoryBot call simplification
  config.include FactoryBot::Syntax::Methods

  #Prevent factory from not loading properly due to spring
  config.before :all do
    FactoryBot.reload
  end
end

spec/rails_helper.rb


#Uncomment this code
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

** Define a user factory **

spec/factories/users.rb


FactoryBot.define do
  factory :user do
    name { 'Test user' }
    email { '[email protected]' }
    password { 'password' }  
  end
end

** Define a Review factory **

spec/factories/reviews.rb


FactoryBot.define do
  factory :review do
    title { "MyString" }
    author { "MyString" }
    description { "MyText" }
    user
  end
end

Introduce spring-commands-rspec

This gem speeds up test startup time

group :development do
  gem 'spring-commands-rspec'
end

bundle install and then bundle exec spring binstub rspec will generate an rspec file in the bin directory

#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
require 'bundler/setup'
load Gem.bin_path('rspec-core', 'rspec')

Shoulda Matchers settings

Shoulda Mathers is a gem that allows you to write tests concisely and intuitively by adding one-liner test syntax to RSpec and Minitest.

group :test do
  gem 'shoulda-matchers'

  #For rails5 or later
  gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
end

spec/support/shoulda_matchers.rb


Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

spec/rails_helper.rb


require "support/shoulda_matchers"

config/application.rb


require_relative 'boot'
require 'rails/all'
require 'uri'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module BookReviewApp
  class Application < Rails::Application
    config.load_defaults 5.1
    config.generators.template_engine = :slim
    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]

    config.generators do |g| 
      g.test_framework :rspec,
        fixtures: false, 
        view_specs: false, 
        helper_specs: false, 
        routing_specs: false
    end
  end
end

Write the code for the model test

Create a folder called models in the spec folder, create a user_spec.rb file in the models folder, and write the following code $ mkdir spec/models $ touch spec/models/user_spec.rb

spec/mogels/user_spec.rb


require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"

  context "if name is entered" do              
    it 'if user registration is successful' do 
    end
  end

  context "if name is not entered" do          
    it 'if user registration fails' do
    end
  end
  
  context 'if email is entered' do
    it 'if user registration is successful' do
    end
  end

  context 'if email is not entered' do
    it 'if user registration fails' do
    end
  end

  context "if password is entered" do
    it 'if user registration is successful' do
    end
  end

  context "if password is not entered" do
    it 'if user registration fails' do
    end
  end
end
 #The language of the test description changes depending on the syntax

 #example syntax
example 'Japanese' do
  #Write code
end

  #it syntax
it 'English' do
  #Write code
end

If you want to test only this file, you need to specify the file $ bundle exec rspec spec/models/user_spec.rb

examples: Tests that exist / failures: Tests that failed / pending: Tests that did not run

Review list display function System Spec

$ mkdir spec/system/ $ touch spec/system/reviews_spec.rb

First of all, make a outline of Spec with comments

spec/system/reviews_spec.rb


require 'rails_helper'

describe 'Review management function', type: :system do
    describe 'List display function' do
        before do 
        #Create user A
        #Create a review whose author is User A
        end

context 'When the user is logged in' do
    before do 
        #User A logs in
    end

    it 'Reviews created by user A are displayed' do
        #Confirm that the title of the created review is displayed on the screen
    end
  end
 end
end

In describe, describe" what you are trying to describe the specification for "(test target). The outermost describe describes the subject of the entire Spec file. Describe a more detailed theme in the deeper description.

context is used to classify the test contents according to the variation of" status / state ". You can organize and make the tests easier to see if you need to test whether they work properly under various conditions.

before is where you write the code to execute the" prerequisites "for the entire area. If you write before in describe or context, the code written in the before block will be executed before executing the test code in the corresponding describe or context area.

it describes the expected behavior in sentences and code in the block. If the target works as expected in it, the test is successful.



Replace each comment with the actual code

spec/system/reviews_spec.rb


require 'rails_helper'

describe 'Review management function', type: :system do
    describe 'List display function' do
        before do 
        #Create user A
        user_a = create(:user, name: 'User A', email: '[email protected]')
        #Create a review whose author is User A
        create(:review, name: 'First review', user: user_a)
        end

context 'When the user is logged in' do
    before do 
        #User A logs in
        visit login_path
        fill_in 'mail address', with: '[email protected]'
        fill_in 'password', with: 'password'
        click_button 'log in'
    end 

    it 'Reviews created by user A are displayed' do
        #Confirm that the title of the created review is displayed on the screen
        expect(page).to have_content 'First review'
    end
  end
 end
end
 #If the test fails
$ bundle exec rspec spec/system/reviews_spec.rb

Review management function
List display function
When the user is logged in
Reviews created by user A are displayed(FAILED - 1)

Failures:

  1)Review management function List display function Reviews created by user A are displayed when the user is logged in.
     Failure/Error: user_a = FactoryBot.create(user, name: 'User A', email: '[email protected]')
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::Nested::Nested::Nested:0x00007fed26346f28>
     
     [Screenshot]: tmp/screenshots/failures_r_spec_example_groups_nested_nested_nested_Reviews created by user a are displayed_412.png


     
     # ./spec/system/reviews_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 4.19 seconds (files took 5.32 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/system/reviews_spec.rb:21 #Review management function List display function Reviews created by user A are displayed when the user is logged in.

System Spec for new review function

spec/system/reviews_spec.rb


.
.
.
describe 'New review function' do
    let(:login_user) { :user_a }

    before do 
        visit new_review_path
        fill_in 'title', with: review_name
        click_button 'Post'
    end

    context 'When you enter a title on the new post screen' do
        let(:review_name) { 'Write a test for a new post' }

        it 'Posted successfully' do
            expect(page).to have_selector 'alert-success', text: 'Write a test for a new post'
        end
    end

    context 'When you did not enter the title on the new post screen' do
        let(:review_name) { '' }

        it 'Get an error' do
            within '#error_explanation' do
            expect(page).to have_content 'Please enter a title'
        end
      end
    end
  end
end

let is a function that can be used in an image similar to "assigning a value to a variable in test scope in before processing" (it does not actually create a variable) let (definition name) {definition content}

You can use a matcher called have_selector to specify a specific element in HTML with a selector (CSS selector). have_selector

By using the method called within, you can narrow the search range to a specific range on the screen by inspecting the contents of the page inside the within block. within'specify a range' do

User model testing

Make a rough outline with comments

spec/models/user_spec.rb


 # validations
 describe "validations" do
   #Existence
   describe "presence" do
     #Name, email address
     it "name and email should not ro be empty/falsy"
     #Password, password confirmation
     context "when password and confirmation is not present" do
       it "@user is invalid"
     end
    end
   #Number of characters characters
   describe "charesters" do
     #Name: up to 20 characters
     context "when name is too long"
       it "@user is inavlid"
     #Password, password confirmation: minimum 6 characters
     describe "when password is too short"
       it "@user is inavlid"
   end
 #Email format
 describe "email format" do
   #invalid format
   context "when invalid format" do
     it "@user is inavlid"
   #valid format
   context "when valid format" do
     it "@user is valid"
   end
   end
  end
end

Fill in the comments

spec/models/user_spec.rb


RSpec.describe User, type: :model do
  #Pending template
  pending "add some examples to (or delete) #{__FILE__}"

 # validations
 describe "validations" do
   #Existence
   describe "presence" do
     #Name, email address
     it { should validate_presence_of :name }
     it { should validate_presence_of :email }
     #Password, password confirmation
     context "when password and confirmation is not present" do
       before { user.password = user.password_confirmation = " " } 
       it { should_not be_valid }
     end
    end
   #Number of characters characters
   describe "charesters" do
     #Name: up to 20 characters,Password: Minimum 6 characters
     it { should validate_length_of(:name).is_at_most(20) }
     it { should validate_length_of(:password).is_at_least(6) }
   end
 #Email format
 describe "email format" do
   #invalid format
   context "when invalid format" do
     #Invalid object
     it "should be invalid" do
       invalid_addr = %w[user@foo,com user_at_foo.org example.user@foo. foo@bar_baz.com foo@bar+baz.com]
       invalid_addr.each do |addr|
         user.email = addr
         expect(user).not_to be_valid
       end
      end
    end
  end
   #valid format
   context "when valid format" do
     #Valid objects
     it "should be valid" do
       valid_addr = %w[[email protected] [email protected] [email protected] [email protected]]
       valid_addr.each do |addr|
         user.email = addr
         expect(user).to be_valid
       end
     end
   end
  end
end

↓ Test result ↓

console.log


$ bundle exec rspec spec/models/user_spec.rb

User
  add some examples to (or delete) /Users/kiyokawakouji/Rails-app/book_review_app/spec/models/user_spec.rb (PENDING: Not yet implemented)
  validations
    presence
      is expected to validate that :name cannot be empty/falsy
      is expected to validate that :email cannot be empty/falsy
      when password and confirmation is not present
        example at ./spec/models/user_spec.rb:17 (FAILED - 1)
    charesters
      is expected to validate that the length of :name is at most 20
      is expected to validate that the length of :password is at least 6
    email format
      when invalid format
        should be invalid (FAILED - 2)
    when valid format
      should be valid (FAILED - 3)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User add some examples to (or delete) /Users/kiyokawakouji/Rails-app/book_review_app/spec/models/user_spec.rb
     # Not yet implemented
     # ./spec/models/user_spec.rb:5


Failures:

  1) User validations presence when password and confirmation is not present 
     Failure/Error: before { user.password = user.password_confirmation = " " }
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::Presence::WhenPasswordAndConfirmationIsNotPresent:0x00007fd1586e2770>
     # ./spec/models/user_spec.rb:16:in `block (5 levels) in <top (required)>'

  2) User validations email format when invalid format should be invalid
     Failure/Error: user.email = addr
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::EmailFormat::WhenInvalidFormat:0x00007fd15831d6f8>
     # ./spec/models/user_spec.rb:34:in `block (6 levels) in <top (required)>'
     # ./spec/models/user_spec.rb:33:in `each'
     # ./spec/models/user_spec.rb:33:in `block (5 levels) in <top (required)>'

  3) User validations when valid format should be valid
     Failure/Error: user.email = addr
     
     NameError:
       undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::WhenValidFormat:0x00007fd1582d5790>
     # ./spec/models/user_spec.rb:46:in `block (5 levels) in <top (required)>'
     # ./spec/models/user_spec.rb:45:in `each'
     # ./spec/models/user_spec.rb:45:in `block (4 levels) in <top (required)>'

Finished in 0.61427 seconds (files took 5.94 seconds to load)
8 examples, 3 failures, 1 pending

Failed examples:

rspec ./spec/models/user_spec.rb:17 # User validations presence when password and confirmation is not present 
rspec ./spec/models/user_spec.rb:31 # User validations email format when invalid format should be invalid
rspec ./spec/models/user_spec.rb:43 # User validations when valid format should be valid

** Explanation of test results below ** As written in the comment out, panding is a template that shows how to write RSpec (this time I wake it up without erasing it), so it's through. (FAILED-number) indicates that the test code failed. This time, three test codes have failed. For Failures: ~, the corresponding code that failed the test and the error content are output. Failures example: ~ outputs the line of the corresponding code in the file where the test failed and the expected behavior. Since this is a model test, we will check the validates

app/models/user.rb


class User < ApplicationRecord
 devise :database_authenticatable, :rememberable, 
        :validatable, :timeoutable, :registerable, :confirmable

 #Is the value of name empty?
 validates :name, presence: true
 #Is the value of name 20 characters or less?
 validates :name, length: { maximum: 20 }
 #Make email addresses lowercase when saving email addresses to the database
 before_save { self.email = email.downcase }
 VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
 #Does the email match the confirmation input?
 validates :email, { presence: true, format: { with: VALID_EMAIL_REGEX } }
 validates :email, :encrypted_password, confirmation: true
 #Is the password value empty?
 validates :password, presence: true
 #Is the password value more than 6 characters?
 validates :password, length: { minimum: 6 }
 #Associate User and Review on the database
 has_many :reviews, dependent: :destroy, foreign_key: :review_user_id
end

Extra edition

** Setting to rerun only failed tests (--only-failures option) **

spec_helper.rb


RSpec.configure do |config|
  config.example_status_persistence_file_path = "./spec/examples.txt"
end

------------------------------------------------------------------

If you are using git.Add the following settings to gitignore as well.
spec/examples.txt

$ bundle exec rspec --only-failures

rspec options

Rspec option list display
$ rspec --help

Output without running a list of tests
$ rspec --dry-run

Output test warning
$ rspec --warnings
This command displays a warning in the spec file. If there is a spelling mistake, put it in the warning and output it

When I tried $ rsoec --warnings, the following output was output

WARN: Unresolved or ambiguous specs during Gem::Specification.reset:
      diff-lcs (>= 1.2.0, < 2.0)
      Available/installed versions of this gem:
      - 1.4
      - 1.3
WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'
Please report a bug if this causes problems.
~~abridgement~~

It is said that the same gem is installed in multiple versions and it is not possible to decide which one to handle on the system. You need to remove the old version of the gem and narrow it down to one or decide on a specific version.

Test type

** Overall test ** ・ System Spec / Feature Spec ・ Integration test (Request Spec) ・ Functional test (Controller Spec) ** Individual parts test ** ・ Model Spec -Routing Spec · View Spec ・ Helper Spec ・ Mailer (Mailer Spec) -Job (job Spec)

Reliability of the test code itself

You have to make sure that the test code works as intended. This is also known as the "exercising the code under test". To prove that it is not a false positive, for example, change to to to_not and invert the expectation.

spec/models/user_spec.rb


 #If there is no name, it is invalid(success)
 it "is invalid without a first name" do
   user = User.new(first_name: nil)
   user.valid?
   expect(user.errors[:first_name]).to include("can't be blank")
 end

 #If there is no name, it is invalid(Failure)
 it "is invalid without a first name" do
   user = User.new(first_name: nil)
   user.valid?
   expect(user.errors[:first_name]).to_not include("can't be blank")
 end

reference

Rails RSpec Preparation and Test Code Basics [Rails] "RSpec + FactoryBot + Capybara + Webdrivers" introduction & initial setting to test writing Building SystemSpec environment with Rails + Selenium + Docker https://github.com/everydayrails/everydayrails-rspec-2017 Introduction to RSpec that can be used, Part 1 "Understanding the basic syntax and useful functions of RSpec" RSpec Settings Useful options for Rspec How to use Rails callbacks that I finally understood after suffering

Recommended Posts

Rails book review app RSpec introduction
Rails Review 1
[Rails] RSpec Kihon no Ki [Introduction]
Rails Review 2
Rspec introduction memo_Rails
[Rails 6] cocoon_ introduction
Rspec Basics [Rails]
[Rails5] Rspec -validation-
About RSpec (Rails)
[Ruby on Rails] Until the introduction of RSpec
Introduction to RSpec 1. Test, RSpec
[Rails] Introduction of PAY.JP
Heroku app moving (rails)
[Rails] Test with RSpec
[Rails] devise introduction method
Ruby cherry book review
Rails, RSpec installation procedure
Introduction to RSpec 2. RSpec setup
I tried unit testing Rails app using RSpec and FactoryBot
[Rails] Introduction of devise Basics
Introduction to RSpec 5. Controller specs
Introduction to RSpec 6. System specifications
[RSpec] Let's use FactoryBot [Rails]
[Rails] Test code using Rspec
[Rails] About Rspec response test
[Rails] New app creation --Notes--
Incorporate circleCI into CircleCI Rails app
[Rails 6] Star-shaped review using Raty.js
Introduction to RSpec-Everyday Rails Summary-