Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

How to tweak RSpec to make API testing more productive? by Andrii Baran


Published on

During the presentation I'll tell about the project I work on.
In particular how do I dealt with zero test coverage and my pain when I understand that I'm the only developer who fix this.
Briefly I'll show how does the testing process looks right now.
Afterwards I'll share with you technical implementation details and a ruby gem for adopting this solution in other project.

Published in: Engineering
  • Be the first to comment

  • Be the first to like this

How to tweak RSpec to make API testing more productive? by Andrii Baran

  1. 1. How to tweak RSpecHow to tweak RSpec to make API testingto make API testing more productivemore productive
  2. 2. Hello World!Hello World! I am a Ruby backend developer at Softbisrto.  You can find me at: MY NAME IS ANDRII BARAN
  3. 3. IN BRIEFIN BRIEF 1. What problem I faced 2. Which solution I came up with 3. How it is implemented 4. Questions
  4. 4. Submitting orders Packing orders Delivery Managing employees Managing vendors Analytics 
  5. 5. No tests Rails 3.2
  6. 6. FactoryBot + Paranoia
  7. 7. Finished in 13 minutes 22 seconds (files took 16.89 seconds to load) 610 examples, 0 failures, 3 pending   Coverage report generated for RSpec to /home/circleci/coverage. 8743 / 21784 LOC (40.13%) covered. Current coverage
  8. 8.
  9. 9.
  10. 10. # spec/rails_helper.rb RSpec.configure do |config| config.include Helpers # This will tell RSpec to collect all failures in test example config.define_derived_metadata do |meta| meta[:aggregate_failures] = true if meta[:type] == :request end end ENTRY POINT
  11. 11. %i(get post put patch delete).each do |http_verb| define_method :"do_#{http_verb}" do if Rails::VERSION::MAJOR >= 5 public_send(http_verb, path, params: params, headers: heade else public_send(http_verb, path, params, headers) end end end do_post post(path, params, headers)
  12. 12. configuration.content_types.each do |type, mime_type| configuration.symbols_with_status_codes.each do |status, code| define_method :"assert_#{code}_#{type}" do expect(response).to have_http_status(status) expect(response.content_type).to eq mime_type end end end assert_200_json class Configuration attr_accessor :content_types, :status_codes def initialize @content_types = { json: 'application/json' } @status_codes = [404, 401, 422, 200, 201] end end
  13. 13. assert_200_json_parsed_body def assert_body expect(response.body).to eq expected_response end def assert_response_object expect(response_object).to have_attributes(expected_response) end def assert_parsed_body expect(parsed_body).to eq(expected_response) end
  14. 14. %i(body response_object parsed_body).each do |resp_assertion| define_method :"do_#{http_verb}_and_assert_#{code}_#{type}_#{re public_send(:"do_#{http_verb}") public_send(:"assert_#{code}_#{type}_#{resp_assertion}") end end assert_200_json_parsed_body %i(body response_object parsed_body).each do |resp_assertion| define_method :"assert_#{code}_#{type}_#{resp_assertion}" do public_send(:"assert_#{code}_#{type}") public_send(:"assert_#{resp_assertion}") end end do_post_and_assert_200_json_parsed_body
  15. 15. Naming convention module ClassMethods def path(&block) let(:path, &block) end def params(&block) let(:params, &block) end def headers(&block) let(:headers, &block) end def expected_response(&block) let(:expected_response, &block) end end def self.included(klass) klass.extend ClassMethods end
  16. 16. Vars DSL def vars(&block) end class VarsDSL def initialize(klass) @klass = klass end def method_missing(method_name, *args, &block) case method_name when /!$/ @klass.class_eval { let!(:"#{method_name.to_s.sub(/!$/,'')}", &block) } else @klass.class_eval { let(method_name, &block) } end end end
  17. 17. Database DSL require 'parser/current' def database(&block) parser = rewriter = buffer ='(string)') buffer.source = Parser::CurrentRuby.parse(block.source) .children.last.loc.expression.source rspec_factory = rewriter.rewrite(buffer, parser.parse(buffer)) self.class_eval(rspec_factory) end FactoryBot.create( :spree_variant, :with_stock_item, upccode: barcode, # variable defined via let purchase_upc: purchase_upc, # variable defined via let case_single: VARIANT_SINGLE_UNIT, case_single_value: 1 )
  18. 18. Database DSL class DatabaseDSLTranslator < ::Parser::TreeRewriter def on_send(node) _, method_name, *args = node.children factory_attrs = { |e| e.loc.expression.source }.join case method_name # ... omitted when /!$/ replace(node.loc.expression, "let!(:#{method_name.to_s.sub(/!$/,'')}) { FactoryBot.create(#{factory_attrs}) }") else replace(node.loc.expression, "let(:#{method_name}) { FactoryBot.create(#{factory_attrs}) }") end end end
  19. 19. RUBY GEM
  20. 20. PROSPROS Less typing More readability Flexibility Less stress Maybe increase productivity CONSCONS Very specific It's DSL
  21. 21. Thank YouThank You Questions?Questions?