[RUBY] Introduction to RSpec 3. Model specs

Continuation of Last time.

Testing against the model. We will test model validation, class methods, and instance methods.

Model spec structure

The model specs include at least the following three tests.

  1. If initialized with a valid attribute, the model state is valid.
  2. If the data fails validation, the model state is not valid
  3. Class and instance methods work as expected

Model spec creation

First, let's test the User model. Last time, I set the automatic generation of the spec file, but this time it is a test for the existing model, so I will generate the spec file manually.

$ rails g rspec:model user

This command will generate spec/models/user_spec.rb, so write the specs in this file.

spec/models/user_spec.rb


repuire 'rails_helper'  #Loading helpers

RSpec.describe User, type: :model do  #The specifications of the User model are summarized in the block

  #name, email,Must be valid if you have a password
  it 'is valid with a name, email, and password' do  #Each spec is described in a block starting with it

    user = User.new(
      name:     "Zeisho",
      email:    "[email protected]",
      password: "hogehoge"
    )
    expect(user).to be_valid  #user is valid(valid)Then pass
  end
  
end

In RSpec, you pass the value you want to test to expect () and call the matching matcher to do the test.

In this case, we are testing user with a matcher called be_valid. Also, the to after (user) returns success if the value to be tested matches the matcher, or failed as the test result. I often use to_not, which has the opposite meaning of to, so it's a good idea to remember it.

Validation test

Now that we've tested whether the model is valid when given the correct values, it's time to test if the model is in an invalid state when given data that fails validation.

spec/models/user_spec.rb


  #Invalid without name
  it 'is invalid without a name' do
    user = User.new(
      name:     nil
      email:    "[email protected]"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

In this spec, the validity of the user is verified by using the valid? Method with the user's name set to nil, and finally the user.errors [: name] contains "can't be blank". If it is included, it will pass. I'm testing the content of the error that occurred because I want to know if the lack of a name is the cause of the validation failure. Please note that you will not get an error message unless you verify the validity with user.valid ?.

Let's write the specifications of other validations along this method.

spec/models/user_spec.rb


  #Invalid if email is duplicated
  it 'is invalid without a duplicate email address' do
    User.create(
      neme:     "zeisho"
      email:    "[email protected]"
      password: "hogehoge"
    )
    user = User.new(
      name:     "Skywalker"
      email:    "[email protected]"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("has already been taken")
  end

It is a spec that if you save a user using create and then generate a user with the same email, you will get a duplicate email error.

Let's complete the specifications of the User model with this condition.

When completed, we will also make specifications for the Project model.

$ rails g rspec:model project

spec/models/project_spec.rb


require 'rails_helper'

RSpec.describe Project, type: :model do
  #Do not allow duplicate project names on a per-user basis
  it "does not allow duplicate project names per user" do
  user = User.create(
    name:     "Zeisho",
    email:    "[email protected]",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  new_project = user.projects.build(
    name: "Test Project"
  )
  new_project.valid?
  expect(new_project.errors[:name]).to include("has already been taken")
  end

  #Allow two users to use the same name
  it "allows two users to share a project name" do
  user = User.create(
    name:     "Zeisho",
    email:    "[email protected]",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  other_user = User.create(
    name:     "Skywalker",
    email:    "[email protected]",
    password: "hogehoge"
  )
  other_project = other_user.projects.build(
    name: "Test Project"
  )
  expect(other_project).to be_valid
  end
end

Since Project is tied to User, with the current writing method, the code became redundant just by creating the instance required for testing. Problems around here will be resolved later.

Test of class method and instance method

In this app, there is a Note linked to the Project model, and it is possible to store a character string as a memo of the project, and the Note model implements a search function.

app/model/note.rb


scope :search, ->(term) {
  where("LOWER(message) LIKE ?", "%#{term.downcase}%")
}

This time we will test this class method and scope.

$ rails g rspec:model note

spec/models/note_spec.rb


require 'rails_helper'

RSpec.describe Note, type: :model do
  #Returning notes that match the search string
  it "returns notes that match the search term" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "[email protected]"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("first")).to include(note1, note3)
    expect(Note.search("first")).to_not include(note2)
  end

  #Return an empty collection if no search results are found
  it "returns an empty collection when no results are found" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "[email protected]"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("message")).to be_empty
  end
end

Learn more about Matcher

So far, we've used three matchers (be_valid, include, be_enpty), but if you want to know more about the matchers provided by RSpec, you can refer to rspec-expectations.

Set the spec to DRY

Use describe, context, before, after to make the spec DRY.

describe, context You can classify the spec group and put them together in a block. Let's take the specifications of the Note model as an example.

spec/models/note_spec.rb


require 'rails_helper'

RSpec.describe Note, type: :model do
  
  #Specs for validation

  #Message search function specifications
  describe "search message for a term" do
    #When finding matching data
    context "when a match is found" do
      #Example group when matching
    end
    #When no matching data is found
    context "when no match is found" do
      #Example group when they do not match
    end
  end

end

before, after You can put together the test data used in all tests in one place.

spec/models/note_spec.rb


require 'rails_helper'

RSpec.describe Note, type: :model do

  before do
    @user = User.cerate(
      name:     "Zeisho"
      email:    "[email protected]"
      password: "hogehoge"
    )
    @project = user.projects.create(
      name: "Test Project"
    )
  end
  
  #Specs for validation

  #Message search function specifications

end

The following options can be set for before.

before(:each) Run before each (each) test in a describe or context block

before(:all) Run only once before all tests in a describe or context block

before(suite) Run before running all files in the entire test suite

If you need to clean up after the example, you can use after.

The test is just DRY!

Unlike the development / production environment, the test prioritizes readability and makes it DRY, so if you frequently scroll the editor or go back and forth between multiple files to check the contents of the spec file, It's too DRY. Consider duplicating code as needed, and try to name variables and methods that understand their role without having to go back and forth between files.

Continued

Recommended Posts

Introduction to RSpec 3. Model specs
Introduction to RSpec 5. Controller specs
Introduction to RSpec 1. Test, RSpec
Introduction to RSpec 2. RSpec setup
Introduction to RSpec 6. System specifications
Rspec introduction memo_Rails
Introduction to SWING
Introduction to web3j
Introduction to Micronaut 1 ~ Introduction ~
[Java] Introduction to Java
Introduction to migration
Introduction to java
Introduction to Doma
Introduction to JAR files
Introduction to Ratpack (8)-Session
Introduction to bit operation
Introduction to Ratpack (6) --Promise
Set RSpec to DRY
Introduction to Ratpack (9) --Thymeleaf
Testing model with RSpec
Introduction to PlayFramework 2.7 ① Overview
Introduction to Android Layout
Introduction to design patterns (introduction)
Introduction to Practical Programming
Introduction to javadoc command
Introduction to jar command
Introduction to Ratpack (2)-Architecture
Introduction to lambda expression
Introduction to java command
Introduction to Keycloak development
Introduction to javac command
[Rails] From test preparation to model unit testing [RSpec]
Introduction to RSpec 4. Create test data with Factory Bot
Introduction to Design Patterns (Builder)
Introduction to Android application development
Introduction to Ratpack (5) --Json & Registry
Introduction to Metabase ~ Environment Construction ~
Introduction to Ratpack (7) --Guice & Spring
(Dot installation) Introduction to Java8_Impression
Introduction to Design Patterns (Composite)
Introduction to Micronaut 2 ~ Unit test ~
Introduction to JUnit (study memo)
Introduction to Spring Boot ① ~ DI ~
Introduction to design patterns (Flyweight)
[Java] Introduction to lambda expressions
Introduction to Spring Boot ② ~ AOP ~
Introduction to Apache Beam (2) ~ ParDo ~
[Ruby] Introduction to Ruby Error statement
Introduction to EHRbase 2-REST API
Introduction to design patterns Prototype
GitHub Actions Introduction to self-made actions
[Java] Introduction to Stream API
Introduction to Design Patterns (Iterator)
Introduction to Spring Boot Part 1
Introduction to Ratpack (1) --What is Ratpack?
XVim2 introduction memo to Xcode12.3
Introduction to RSpec-Everyday Rails Summary-
Introduction to Design Patterns (Strategy)
[Introduction to rock-paper-scissors games] Java
Introduction to Linux Container / Docker (Part 1)
Introduction to swift practice output Chapter5