In development, soon or later we need to do some integration with a third-party service and usually this means perform http requests.
As a good developer, we'll add specs in our codebase to make sure that everything is well tested.
In order to keep our test suite faster and consistent, we need to mock our http requests. A simple and good way to achieve this is using theWebmock gem which allow easily mock the http responses for your requests.
# spec/webmock_spec.rbstub_request(:get,'https://example.com/service').to_return(status:200body:'{"a": "abc"}')
Example of mock using Webmock
And this works well, but if we need perform many requests to accomplish a task or business logic, just mocking become complicated and hard to maintain.
Example: Lets imagine that we need import some products in our system, but these products are separeted in different categories and to get the products we need get the all the categories, iterate over them to retrieve the products:
let(:categories_endpoint){"https://example.com/categories"}let(:categories_response)do[{id:'category-one-id'},{id:'category-two-id',name:''}].to_jsonendlet(:first_category_endpoint){"#{categories_endpoint}/category-one-id"}let(:first_category_response)do{products_links:[{link:'https://example.com/categories/categor-one-id/product-one'}]}.to_jsonendlet(:second_category_endpoint){"#{categories_endpoint}/category-two-id"}let(:second_category_response)do{products_links:[{link:'https://example.com/categories/categor-two-id/other-product'}]}.to_jsonendlet(:first_product_endpoint){'https://example.com/categories/categor-one-id/product-one'}let(:first_product_response){read_fixture('fixtures/products/first_product.json')}let(:second_product_endpoint){'https://example.com/categories/categor-two-id/other-product'}let(:second_product_response){read_fixture('fixtures/products/second_product.json')}beforedostub_request(:get,categories_endpoint).to_return(status:200,body:categories_response)stub_request(:get,first_category_endpoint).to_return(status:200,body:first_category_response)stub_request(:get,second_category_endpoint).to_return(status:200,body:second_category_response)stub_request(:get,first_product_endpoint).to_return(status:200,body:first_product_response)stub_request(:get,second_product_endpoint).to_return(status:200,body:second_product_response)end
A possible mock for the example
As we can see the configurations to mock is huge, and if we need add a thirdy category, these configs will increase even more, becoming hard to read and maintain.
Another problem is that we are spending a time creating that could be use in other places in our application.
A way to avoid this is using theVCR gem.
The VCR gem
The VCR is a gem that record http interactions in our test suite and allow replay these interactions in future runs.
VCR usage
The VCR usage is pretty simple, as follows:
# Configure itVCR.configuredo|config|config.cassette_library_dir="fixtures/vcr_cassettes"config.hook_into:webmockend# Uses itit'does something'doVCR.use_cassette('my_cassete')doexpect(do_request.body).toeql({success:true}.to_json)endend
The configuration is simple but the usage is a bit annoing, requiring us wrap all the HTTP interactions in blocks, its also break the TDD "configure, execute and assert" pattern.
VCR with RSpec
The VCR provides an integration with RSpec that uses the RSpec's metadata to configure itself. The configuration required to use this feature ishere. But the idea of this article is have fine-grained control over VCR, so we'll not use it.
Fine-grained control of VCR with RSpec
Let's configure the VCR with RSpec to give to us a fine-grained control for us. The configuration follow these steps:
- VCR regular configuration
- Configure VCR to don't run on specs which its not required
- Configure the VCR using RSpec
shared_contexts
VCR regular configuration
Just the regular configuration, as follows the VCR docs.
require'vcr'VCR.configuredo|c|c.cassette_library_dir='spec/vcr_cassettes'c.hook_into:webmockend
Disable VCR in specs which its not required
This is not exactly required, but its a good practice, also will make our tests fail if they perform some HTTP requests or use regular mocks without problems.
The VCR has an methodturned_off
that accepts a block of code which to be executed without the VCR. So to disable the VCR on specs which its not required, we'll use the RSpec hookaround
:
# specs/spec_helper.rbRSpec.configuredo|config|config.arounddo|example|# Just disable the VCR, the configuration for its usage# will be done in a shared_contextifexample.metadata[:vcr]example.runelseVCR.turned_off{example.run}endendend
Configure VCR's shared_context
The RSpec'sshared_context
will allow us enable the VCR only when we need:
shared_context'with vcr',vcr:truedoarounddo|example|VCR.turn_on!VCR.use_cassette(cassette_name)doexample.runendVCR.turn_off!endend
With thisshared_context
we can use it as follow and the http will be recorded:
describe'using vcr',vcr:truedo# Configure the cassete namelet(:cassete_name){'path/to/the/interaction'}it'record the http interaction'doexpect(do_request.body).toeql({success:true}.to_json)endit'reuse the same cassete here'doexpect(do_request.headers).toinclude('x-custom-header'=>'abc')endend
Improving shared_context
The VCRuse_cassete
method accepts many other options, like therecord_mode
for example. Using theshared_context
andlet
allow us configure the VCR to record new interactions in development but raise an error on CI, for example:
shared_context'with vcr',vcr:truedo# Disable new records on CI. Most of the CI providers# configure environment variable called CI.let(:cassette_record){ENV['CI']?:none::new_episodes}arounddo|example|VCR.turn_on!VCR.use_cassette(cassette_name,{record:cassette_record})doexample.runendVCR.turn_off!endend
Creating specific shared_contexts
It is possible create specificshared_context
that configure the VCR for specific cases. For example, imagine that you need not ignore the headers for some specific requests.
shared_context'with vcr matching headers',vcr_matching_headers:truedoarounddo|example|VCR.turn_on!VCR.use_cassette(cassette_name,{match_requests_on:[:method,:uri,:headers]})doexample.runendVCR.turn_off!endend
Conclusion
There are more options on VCR that we could add, but the examples gives the idea about how to control the VCR in our test suite.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse