class: center, middle # RSpec: Best practices Paweł Gościcki @ [GetAirHelp.com](https://www.getairhelp.com/) Gdańsk, 2014-08-19 --- # Use `--format documentation` And use a global `~/.rspec` file ```bash ☺ cat ~/.rspec --color --format documentation --profile 5 ``` --- # Don't use the `should` keyword Not in docstrings ```ruby it 'creates a new claim' do ``` Nor in specs ```ruby it { is_expected.to be_valid } ``` --- # Use the [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers) gem Relationships: ```ruby it { is_expected.to belong_to(:flight) } it { is_expected.to have_one(:passenger) } ``` Validations: ```ruby it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:email) } ``` Controller responses: ```ruby it { is_expected.to respond_with(:success) } it { is_expected.to render_template('new') } it { is_expected.to redirect_to(new_claim_path) } ``` --- # Use proper docstrings When writing specs for methods: ```ruby describe '#instance_method' do describe '.class_method' do ``` For other types of specs: ```ruby it 'creates a new claim' do ``` --- # Use `context` properly `context` should be used mainly to denote two different code path executions (side note: having two code paths execution is a code smell - there should be only one; there should be no `if` statements). It's good to start it with *when*: ```ruby context 'when there is an existing claim by this user' do it ... context 'when there are no existing claims by this user' do it ... ``` --- # Use `let() { ... }` ```ruby describe CreateAccount do describe "#call" do let(:user_id) { rand(100) + 1 } let(:params) do { email: "test@test.com", name: 'Test Test' user_id: user_id, } end let(:create_account) { CreateAccount.new(params) } it 'creates Account with user_id set' do account = create_account.call expect(account.user_id).to eql(user_id) end ``` `let` provides an indirection in code (you have to scroll constantly to understand the spec), but if you have many similar examples, it's advised to use `let.` --- # Use `let!` If you have to, that is if you need the code in the `let` block to be executed before the spec is executed. For example we might need to have a record in the database before the spec is run: ```ruby let!(airline) { create(:airline) } it 'updates Claim with ...' do ... expect(claim).to ... ``` `let` is lazily evaluated, so if you don't call it directly, it will not be there. While when using `let!` it will be called just like if it were in a `before` block. --- # But don't overuse `let` It's ok to write a comprehensive spec with all variables declared inside the spec: ```ruby it 'does not save the account if there are validation errors' do account_params = { user_id: nil } create_account = CreateAccount.new(account_params) account = create_account.call expect(account).to be_a(Account) expect(account).not_to be_valid expect(account).not_to be_persisted end ``` --- # Write unit specs at unit level In other words - stub all external dependencies (external to the class currently being tested): ```ruby class CreateAccount def call create_account end def create_account Account.create(...) end end ``` `Account` is an external dependency here (it's a method execution on an external class), so in order to test it at a unit level, calls to it should be mocked out (using `expect().to receive()`) Internal (local to the class) methods should not be mocked out. --- # Divide specs into parts If your specs are longer, divide them into parts like `setup`, `expectations` and `execution`. ```ruby it 'calls CreateAccount if account does not exist' do user_id = 12211 create_account = double 'CreateAccount' create_account_params = { user_id: user_id } expect(Account).to receive(:find_by).with(user_id: user_id).and_return(nil) expect(CreateAccount).to receive(:new).with(create_account_params). and_return(create_account) expect(create_account).to receive(:call) FindOrCreateAccount.new(create_account_params).call end ``` --- # Don't write specs for private methods ```ruby class CreateAccount def call create_account end private def account_params { email: email, name: name, user_id: user_id, } end def create_account Account.create(account_params) end end ``` It's sufficient to test public methods (`#call` in this case). Private methods will be tested indirectly anyway. --- # Test for all possible cases ```ruby describe '#show' do context 'when claim is found' do it 'shows the claim' do ... context 'when claim is not found' do it 'responds with :not_found error' do ... context 'when claim is not owned' do it 'responds with :unauthorized error' do ... ``` It might be hard to see all the possible cases, but you should try to think of what could go wrong, which params could be set incorrectly and write a spec for that case. --- # Use shared examples In `spec/support/shared_examples/model_with_attachment.rb`: ```ruby shared_examples 'model with attached' do |attribute| it 'allows adding an attachment' do file = File.join(...) attachment = Rack::Test::UploadedFile(file) model_name = described_class.name.downcase model = create(model_name, attribute => attachment) expect(model).to be_valid expect(model.public_send(attribute).file.path).to be_identical_to(file) end end ``` And then in the spec: ```ruby describe Message do it_behaves_like 'a model with attached', :attachment ``` --- # Write controller specs at unit level ```ruby describe ClaimsController do describe '#create' do let(:airport) { double 'Airport', icao: 'GDN' } let(:create_claim) { double 'CreateClaim' } let(:claim_params) do { email: ..., ... } end before do expect(Airport).to receive(:find).with(icao: icao).and_return(airport) expect(CreateClaim).to receive(:new).with(claim_params).and_return(claim) post :create, claim: claim_params end it { is_expected.to respond_with(:success) } it { is_expected.to render_template(:show) } ``` We don't test that the object was created in the database. A simple expect on `CreateClaim` is enough. The proper way is to write an acceptance spec, which will actually test that the datbase records are created. --- # Use `factory_girl` If you need to setup your database with data, using `factory_girl` (or some other factory gem) is the recommended way of doing it. The proper place for using it are acceptance specs (Capybara): ```ruby describe 'Claims' do background do @airline = create(:airline, :lot) @airport_gdn = create(:airport, :gdn) @airport_waw = create(:airport, :waw) @flight = create(:flight, :gdn_waw, :cancelled) end scenario 'Filing a claim for a cancelled flight' do visit ... ``` I prefer not to use `let(..)` inside acceptance specs because I'd have to use `let!` all over the place. I also think they read better as instance variables in a `before` block (but only for acceptance specs!). --- # Stub HTTP requests All HTTP requests should be stubbed (using the [webmock](https://github.com/bblimke/webmock) gem). ```ruby describe PayoneerGateway do describe '#sign_up_url' do let(:url) { '...' } let(:payoneer_uri) { 'https://api.payoneer.com/sign_up_url?user_id=151' } let(:body) { Hash[url: url].to_json } before do stub_request(:get, payoneer_uri).to_return(status: :success, body: body) end it 'calls Payoneer API to fetch the sign up url' do payoneer_gateway = PayoneerGateway.new(...) response = JSON.parse(payoneer_gateway.sign_up_url) sign_up_url = response["url"] expect(sign_up_url).to eql(url) ``` --- class: center, middle # And that's it! Thank you! ### Questions?