📜 ⬆️ ⬇️

Test Recipes for Ruby and Rails Applications

image

In this post I would like to talk about the approaches, techniques and means of test preparation. I’ll tell you how not to write too much, duplicate code less, write tests so that they are easy to maintain, and how to gain performance in some situations.

Who will it be interesting?
')


I will give examples of code for RSpec, but most of them will work with MiniTest (some will need to be brought by a file). Those who use RSpec, but have not yet read betterspecs.org , I advise you to look at it - it shows how to write well and how to write it with examples.

On every day


RSpec DSL

RSpec actually creates a class for each context. This means that you can declare instance and class methods in the spec text and use them in all nested contexts. There are several helper who will help with this.

let

In RSpec, 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 

When using 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

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 

Using 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 

In `type:: request` tests, 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 

Prior to this, we placed an expression in the 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 

Lambdas are also useful in cases where you need to check the result of a method on a large amount of input data, and it makes no sense to create a context for each option:

 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 

Note that in all examples we do not refer to the method being tested, except through subject .

its

For RSpec, there is an excellent rspec-its plugin, which they brought to a separate gem in version 3. With this thing, the tests can become even more compact and expressive. Here is an example where its exactly would come in handy.

Not quite obvious, but very useful trick - using 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 

Another situation where 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

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 .

Variable naming

Try using common neutral names for some variables in all tests. We, for example, use 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

Local 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 

Sometimes when using 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 

Accelerate Tests


I will not write about spring / zeus / spork / etc., but I will tell you how to reduce testing time in some other situations.

Disable long tests

:) But not really. Of course, this approach may not be suitable for many reasons, but if you have tasks that require long calculations, mark them with an RSpec tag and disable them when you start 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 

With such settings, Joba tests will not be executed every time, but will be executed if you run:

Disable image processing

I guess it will suit everyone who uses it. If you do not use fixtures, and you have a mandatory image field in the model, then in each test that creates an instance of this class, the image will be processed.

The way to disable processing depends on the library used. In tests, the approach will be the same: disable processing for all tests and enable it by tag or in around-hook.

Example for carrierwave
 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 


Useful stuff



Thanks for attention! Please share your recipes in comments.

Source: https://habr.com/ru/post/269723/


All Articles