let
is the preferred way to set local "variables." It defines a new instance method that returns a block result calculated in the context of the test. The result is calculated lazily, so there is nothing to worry about if you define `let` and do not use it in the part of the tests - this will not affect the testing time. A block of one let
can use the result of another let
or a result from an external context through super
: let(:project) { Project.new(project_attrs) } let(:project_attrs) { {name: 'new_name', description: 'new_description'} } context 'when empty name is given' do let(:project_attrs) { super().merge!(name: '') } # ruby super , . # , (). end
let
should note that the result of the block is cached for the duration of the example. The values determined by him are, in fact, constants at the time of the test. Therefore, let
not suitable for creating shortcuts. If it is assumed that the value will change, then it will be correct to declare the method.subject
is the "special" let
. The main feature is that all matchers who do not have a recipient are applied to the subject
. describe '#valid?' do subject { user.valid? } it { should eq true } # it { is_expected.to eq true } # subject c context 'when name is empty' do it 'adds error' do expect { subject }.to change(user.errors, :any?).to true end end end
subject
reduces the amount of duplicated code, increases readability and allows you to determine situations when you are testing a functionality other than that specified in describe
. In most cases, you will be able to use a single subject
on the describe
, influencing its behavior with the help of let
in nested contexts: # RSpec.describe ProjectsController do describe '#index' do subject { get :index } it { should redirect_to new_user_session_path } context 'for signed in user' do sign_in { create(:user) } # , it { should be_forbidden } context 'with permissions' do add_permissions(:manager) # it { should be_ok } end end end # describe '#create' do subject { post :create, project: resource_params } let(:resource_params) { {name: 'new_project'} } it 'creates resource and redirects to its page' do expect { subject }.to change(Project, :count).by(1) resource = Project.last expect(project.name).to eq 'new_name' expect(subject).to redirect_to project_path(resource) end context 'when params are invalid' do let(:resource_params) { super().merge!(name: '') } it { should render_template :new } it 'doesnt create resource' do expect { subject }.to_not change(Project, :count) end end end end
get
, post
and other methods do not return a response
. But you can slightly correct our subject
to use the same approach in them: subject do get '/projects' response end
subject
, the result of which was checked. It often happens that the result checks are much less than the checks that the state of the system has changed. In such cases, help lambda: describe '#like!' do subject { -> { user.like! post } } it { should change(post, :likes_count).by(1) } it { should change(user, :favorite_posts).by(post) } context 'when post is already favorite' do before { subject.call } it { should raise_error /Already favorite/ } end end
describe '.cleanup_str' do subject { ->(*args) { described_class.cleanup_str(*args) } } it 'removes non-word symbols' do expect(subject.call('xY12')).to eq 'xY12' expect(subject.call('x+Y-1_2')).to eq 'xY12' expect(subject.call('xY 1;2')).to eq 'xY12' end end
subject
.its
exactly would come in handy.its
with lambdas: RSpec.describe ProjectsController do let(:resource) { create(:project) } describe '#update' do subject { -> { patch :update, id: project, project: resource_params } } let(:resource_params) { {name: 'updated_name'} } it { should change { resource.reload.name }.to 'updated_name' } its(:call) { should redirect_to project_path(resource) } end end
its
very useful - when checking JSON responses. If you define the ActionDispatch::TestResponse#json_body
, which passes #body
via JSON.parse
and turns the result into Mash
(for example, this way ), then it becomes very convenient to check the fields: RSpec.describe UsersController do let(:resourse) { create(:user) } describe '#show' do subject { get :show, id: resource, format: :json } its('json_body.keys') { should contain_exactly(*w(name projects avatar)) } its('json_body.avatar.keys') { should contain_exactly(*w(url size)) } its('json_body.projects.first.keys') { should contain_exactly(*w(name created_at)) } end end
described_class
is another “special” let
. This is the helper for accessing the object that you specified in RSpec.describe
. When using it instead of explicitly specifying the module, the code is “detached”: the class as a describe
argument, and it is accessed as an argument. Such code is more suitable for reuse, for example, it is easier to allocate it in shared_examples
. described_class
works without problems with constants: should raise_error described_class::Error
, or described_class::LIMIT
.instance
to denote instances of tested classes and resource
to denote the resource being processed in controller / query tests. I cannot name the objective advantages of this approach, but subjectively, tests are written and read faster.shared_examples
can be defined within any context, and they will not be accessible outside this context. This is useful when you need to repeat checks in several nested contexts: RSpec.describe ProjectsController do let(:resource) { create(:project) } shared_examples 'rendering resource' do it { should be_ok } its(:json_body) { should include 'id' => resource.id, 'name' => resource.name } end describe '#show' do subject { get :show, id: resource.id } include_examples 'rendering resource' end describe '#search' do subject { get :search, q: resource.name } include_examples 'rendering resource' end
shared_examples
it is necessary to add special checks only in certain cases or to give the opportunity to replace some checks with others for some tests. In such cases, you can split large blocks of shared_examples
into smaller ones and copy contexts between files. But you can pass the necessary checks in the parameters to include_examples
: RSpec.shared_examples 'hooks controller #create' do |**options| describe '#create' do subject { post :create, params } context 'on success' do let(:params) { valid_params } # it { should change { something } } # instance_exec(&options[:on_success]) if options[:on_success] end context 'on failure' do let(:params) { invalid_params } it { should_not change { something } } if options[:on_failure] instance_exec(&options[:on_failure]) else its(:status) { should eq 422 } # end end end end RSpec.describe BrandedHooksController do include_examples 'hooks controller #create', on_success: -> { its(:json_body) { should eq 'status' => 'ok' } }, on_failure: -> { its(:json_body) { should eq 'status' => 'rejected' } } do let(:valid_params) { {type: 'hook'} } let(:invalid_params) { {type: 'unsupported'} } end end
rspec
. These can be calls to external applications, long queries to the database, work with large files. # spec_helper.rb # Exclude some tags by default. Running 1 file won't use exclusions. # Use `FULL=true bin/rspec` to disable filters. if (!ENV.key?('FULL') || !ENV.key?('CI')) && config.files_to_run.size > 1 config.filter_run_excluding :external, :elastics end # some_job_spec.rb describe '.process_file', :external do it 'does somithing heavy' end
some_job_spec.rb
module SpecHelpers # All image uploaders are descendants of ImageUploader. This module # toggles <code>enable_processing</code> of it and all its descendants. module ImageProcessing module_function # Overwrites cached values in ancestors. def enable_processing=(val) ImageUploader.enable_processing = val ImageUploader.descendants.each { |x| x.enable_processing = val } end def with_processing(val) old_value = ImageUploader.enable_processing self.enable_processing = val yield ensure self.enable_processing = old_value unless old_value == ImageUploader.enable_processing end end end # rails_helper.rb around process_images: true do |ex| SpecHelpers::ImageProcessing.with_processing(true) { ex.run } end
.rspec
to set default flags. After installation, it has the line `--require spec_helper`. If we replace it with `--require rails_helper`, then it will be possible not to write` require 'rails_helper' `in each spec. # spec_helper.rb require 'bigdecimal' BigDecimal.class_eval do alias_method :inspect_orig, :inspect alias_method :inspect, :to_s end
Source: https://habr.com/ru/post/269723/
All Articles