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.
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 beforeit
.after
Runs afterit
.around
Runs before and afterit
.
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 it
s for each test seems like good, but it is not necessarily so, because the more it
s are written the more tests will be run for each of it
s and the longer test time will take.
I find it enough to write comments without separating the it
s, 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 let
s 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
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
, useshared_context
/include_context
. - To make common code in
it
, useshared_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.
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