Chapter 11. RSpec - 1. Types of specs and how to use them

This chapter explains the files under the spec directory and their contents.

Specs Types

Factory (factories directory)

Use for test data creation.

When creating test data in any specs, it is better to create with FactoryBot instead of Model.create. If there are multiple data patterns, define each pattern as a trait so that others can quickly grasp the data structure.

Use build or build_stubbed to create them if possible.

If you do not need to store data in the DB, such as for model validation, use FactoryBot.build or FactoryBot.build_stubbed instead of FactoryBot.create to avoid running INSERT and to avoid slowing down in the future.

Model Spec (models directory)

Use for Model testing.

It is mainly used to ensure that no illegal values are registered in the DB. It is also means the following.

  • The factory exists without errors.
  • Error messages are prepared.

This is also a preliminary in making the request specs.

When creating a new table, it is recommended to create Model with validation, Model spec and Factory, and update the Schema file, and commit these at the same time.

Request Spec (requests directory)

Use for Controller testing. Controller spec is deprecated, but it is basically the same. It is mainly used to verify a single action.

It guarantees that

  • If GET, the screen should be open and displayed data correctly.
  • If others (POST/PATCH/PUT/DELETE), that data should be created, updated or deleted correctly.

This is also a preliminary in making the system specs. Conversely, it is better to prepare Factory, Model spec, and Request spec first before considering System spec. Before creating integration tests, prepare unit tests first.

Easier to determine the problem

In the case of a screen need many DOM operations with Javascript, if request spec has been made, it can be determined that the server side is fine, but there seems to be a problem with the parameters being sent.

System Spec (system directory)

Use for Integration Testing. Feature spec is deprecated, but it is basically the same. It is also said System Testing or E2E(End-to-end) Testing (although strictly these may mean different), and specifically refers to multiple Controllers/Actions testing.

For screen operation testing, capybara is used, which has been the default since Rails 5.1. The writing method is unique and may be a little difficult at first, but there are a lot of advantages in using it. In my opinion, it will be needed for a good project in monolithic Rails.

The next chapter will explain some gems for system spec.

Can use system spec for controller unit testing

It is possible to choose for Controller unit tests using system spec instead of request spec I heard that a company have such a policy. In API mode, request spec is enough.

Advantages of System Spec

System reliability and stability can be increased because detailed testing including Javascripts. Engineers can have a sense of security that the system has been tested properly.

Disadvantages of System Spec

Setting js: true in system spec takes longer to run than request spec, and tends to make tests unstable due to unexplained failures.

If there are no members who have built system spec before, the hurdle is a bit higher due to the additional costs of building the environment and learning costs.

Fixture (fixtures directory)

Place external files to be used in spec such as jpg, png, csv, txt, pdf and so on. If a lot of the files, you can make the directories further under fixtures.

Use fixture_file_upload to read the files my way. In places where fixture_file_upload is not available, such as factory or rails console, I use the following code to retrieve and test the files.

file_path = Rails.root.join('spec/fixtures/test.png')
file = ActionDispatch::Http::UploadedFile.new(
  filename: File.basename(file_path),
  type: 'image/png',
  tempfile: File.open(file_path)
)

* In case of a png file

In capybara, use attach_file for uploading.

Note that huge size files are not suitable for normal git management, so you can use Git LFS. It may be installed by default in some environments.

Git Large File Storage

support directory

Place common functions used in spec.

Testing for other classes

It is recommended to create directories for job and other service class tests. Basically, it will be used for unit testing, if you want multiple features test, it might be good to use system spec, but there is no strict rule.

Javascript-side testing

A little explanation in the frontend chapter.

How to write

Basic Syntaxes

  • describe Test target.
  • context Test condition.
  • it Test result.
  • let Variable definition in spec.
  • subject Test content.
  • before Runs before it.
  • after Runs after it.
  • around Runs before and after it.

describe

The topmost description is mainly the target class name, but there is no strict rule. If the file very larger, you can separate some files.

The following are often used.

  • Use .method_name for class method.
  • Use #method_name for instance methods.

context

None in particular.

it

Inside it block, write like the following.

expect({test target}).to {matcher} {comparison target}

e.g.

expect(model).to be_invalid
expect(model.errors.full_messages).to eq ['Please enter your username'].

Separating its for each test seems like good, but it is not necessarily so, because the more its are written the more tests will be run for each of its and the longer test time will take.

I find it enough to write comments without separating the its, so I only separate them when I really need to.

Note that the writing method should is old and now deprecated.

let / let!

let(:user){ create(:user) }

It can also be written in block.

let(:user) do
  create(:user)
end

The above code means the following.

def user
  @user ||= create(:user)
end

In the case without !, note that create(:user) is not executed until user is called, which is called lazy evaluation. Therefore, even with this description is present, the users table will remain empty if user is not used.

It may be difficult to figure out the evaluation timing at first, so it is better to write all lets with ! . If you want to write rspec more cleanly, you can consider using it without !.

subject / subject!

The usage is the same as let, write the test contents.

If it is difficult to summarize the test content in the subject, you can put the test content in before or it.

before

Write test preparation such as allow, stub_const and data generation that does not require variable definitions with let.

after

It is occasionally used to delete special data or to roll back constant values that were changed in before.

Since Data created in tests is rolled back, there is no need to clear data for each test. Uploaded files should be deleted in rails_helper.rb.

around

It is also occasionally used to manipulate the date/time or to temporarily change a constant value.

def

There are no strict rules for def, but it is better to use let if it is related to the test definition as much as possible.

It is able to use def for things that are not directly related to the test content, such as ajax wait process in system spec.

Matcher

expect(model).to be_invalid
expect(model.errors.full_messages).to eq ['Please enter your username'].

In this code, be_invalid and eq are called Matcher. Although it is not necessary to be overly concerned about which one to choose for the matcher, choosing a more suitable matcher will make it more intuitive and easier to read what is being verified.

Basically, ? methods are replaced by be_ matchers like the following.

nil? → be_nil
valid? → be_valid
present? → be_present

RSpec Style Guide - matchers

Login session

Sessions in each RSpec are often faked. Replace required session values such as current_user with allow. If the gem has a function for RSpec, you can use it. For devise, the following link has a description

Github - devise#controller-tests

The reason to fake it may be to write tests that do not depend on the session state (means unit tests), and also because the setting is complicated with little benefit. Of course, it is better to have a test for the login.

File upload

Explained in the Fixture section.

External HTTP request

Basically, the test is written so that external HTTP requests are not executed. The gem webmock can be used to detect external HTTP requests. This gem will be described in the next chapter.

Reusing code (DRY)

  • To make common code in context, use shared_context / include_context.
  • To make common code in it, use shared_examples_for / it_behaves_like.

In either case, the code should be placed in

  • If it will be used in only one file, the file itself.
  • If it will be used in multiple files, the file in spec/support.

By the way, shared_context and shared_examples_for are both aliases of shared_examples, and their behavior seems to be the same.

Note that, the idea seems to be that RSpec code should not be too DRY compared to under app.

When you want to commit temporarily skipped code.

Use skip: true or xdescribe, xcontext, xit (prefix x to describe, context, it) instead of commenting out, the result will show that it was skipped, which is easy to know.

Also, leaving the reason why you skipped it will make it easier for other members to fix it.

To replace global values such as constants and config

Use stub_const. If it can't be used, use around or before/after and replace values temporarily.

RSpec Style Guide - Declare Constants

Randomize order

Adding config.order = 'random' to spec_helper.rb can randomize the order.

Randomization will be able to detect bugs that depend on the execution order, such as rewriting config values. This will cause the seed value to be output to the result, so it can be rerun with an option like rspec --seed 12345.

If you are using a gem faker, you will need to pass the seed value to it as well.

# spec_helper.rb
config.order = :random
Faker::Config.random = Random.new(config.seed)

Testing for paging and large data

Can use FactoryBot.build_list or FactoryBot.create_list for large data generation. The page size will be replaced by stub_const.

Testing for Manipulating date/time

Use travel_to and around. No need for the gem timecop for simple time manipulation.

Rails API - travel_to

Should not write it at the same level as describe or context

describe do
  it do
    ...
  end

  context 'When xxx is false' do
    it do
      ...
    end
  end
end

In this case, it will be necessary to care about the execution order, so it would be better to put it as follows. The context is also shown explicitly, which makes it easier to read.

describe do
  context 'when xxx is true' do
    it do
      ...
    end
  end

  context 'When xxx is false' do
    it do
      ...
    end
  end
end

Github - rspec-style-guide