I'm using the wicked_pdf gem, and I'm currently trying to speed up my spec suite.
I realised that some of my specs are generating PDF in my tmp folder, which is quite time-consuming.
Is there any way to completely stub the wicked_pdf generation, so it don't actually generate the PDF?
It depends on how you are using it, and how much you want to cut out during your test suite, but probably a good place is WickedPdf::PdfHelper#make_pdf, which you could stub with something like this in an RSpec test:
describe MyController do
describe '#show.pdf' do
it 'creates a PDF'
let(:tiny_pdf) do
# What is the smallest possible valid PDF?
# https://stackoverflow.com/a/17280876/23915
"%PDF-1.4\ntrailer<</Root<</Pages<</Kids[<</MediaBox[0 0 3 3]>>]>>>>>>"
end
# Here is the actual stubbing
allow(WickedPdf::PdfHelper).to receive(:make_pdf).and_return tiny_pdf
get :show, params: { id: 1, format: :pdf }
expect(response.status).to eq 200
end
end
end
Or you could override it globally by reopening the class and changing the definition:
if Rails.env.test?
class WickedPdf
class PdfHelper
def make_pdf(options = {})
"%PDF-1.4\ntrailer<</Root<</Pages<</Kids[<</MediaBox[0 0 3 3]>>]>>>>>>"
end
end
end
end
Stubbing out WickedPdf::PdfHelper#make_pdf as in the other answer prevents testing what's being rendered with e.g. expect(response).to render_template("show").
To find which methods lead to writing to files I temporarily added expect(File).not_to receive(:open) before the get ... format: :pdf and found:
Failure/Error: render pdf: "...", template: "show"
(File (class)).open("/tmp/wicked_pdf20200304-24076-r7r1eh.html", 194, {:perm=>384})
expected: 0 times with any arguments
received: 1 time with arguments: ("/tmp/wicked_pdf20200304-24076-r7r1eh.html", 194, {:perm=>384})
# /usr/local/bundle/gems/wicked_pdf-1.4.0/lib/wicked_pdf/tempfile.rb:10:in `initialize'
# /usr/local/bundle/gems/wicked_pdf-1.4.0/lib/wicked_pdf.rb:58:in `new'
# /usr/local/bundle/gems/wicked_pdf-1.4.0/lib/wicked_pdf.rb:58:in `pdf_from_string'
# /usr/local/bundle/gems/wicked_pdf-1.4.0/lib/wicked_pdf/pdf_helper.rb:91:in `make_pdf'
# /usr/local/bundle/gems/wicked_pdf-1.4.0/lib/wicked_pdf/pdf_helper.rb:113:in `make_and_send_pdf'
# /usr/local/bundle/gems/wicked_pdf-1.4.0/lib/wicked_pdf/pdf_helper.rb:40:in `render_with_wicked_pdf'
# /usr/local/bundle/gems/wicked_pdf-1.4.0/lib/wicked_pdf/pdf_helper.rb:30:in `render'
# ./app/controllers/reports_controller.rb:21:in `block (2 levels) in show'
# ./app/controllers/reports_controller.rb:11:in `show'
I then experimented with stubbing at different places in this backtrace and found that the following makes the test as fast as the usual "html" controller tests while still allowing to test for render_template("show"):
it "returns http success" do
allow_any_instance_of(WickedPdf::PdfHelper).to receive(:make_and_send_pdf)
get :show, params: { id: 1, format: :pdf }
expect(response).to have_http_status(:success)
end
it "renders the show template" do
allow_any_instance_of(WickedPdf::PdfHelper).to receive(:make_and_send_pdf)
get :show, params: { id: 1, format: :pdf }
expect(response).to render_template("show")
end
this also makes returning tiny pdf unnecessary.
Related
I have this api endpoint wot get all the blogs from my database that works id the user pass an api_key. This works correctly and now I'm trying to testing this endpoint.
Routes:
Rails.application.routes.draw do
get 'blogs', to: 'blogs#index'
end
Blogs controller:
class BlogsController < ApplicationController
def index
if params[:api_key]
user = User.find_by(api_key: params[:api_key])
if user.present?
#blogs = Blog.all
return render json: #blogs, status: :ok
end
end
render json: { error: "Unauthorized!" }, status: :bad_request
end
end
I'm new to rspec and tests in general, I watched a couple videos and tutorials and this is what I have so far:
spec/requests/blogs_spec.rb
require 'rails_helper'
RSpec.describe 'Blogs API', type: :request do
let!(:blogs) { Blog.limit(10) }
describe 'GET /blogs' do
before { get '/blogs' }
it 'returns status code 400' do
expect(response).to have_http_status(400)
end
context 'when the request is valid' do
before { get '/blogs', params: { api_key: '123123'} }
it 'returns status code 400' do
expect(response).to have_http_status(200)
end
end
end
end
I can't seem to make the last test work and I don't know why. My guess is that I'm not passing api_key correctly, but I don't know how
1) Blogs API GET /blogs when the request is valid returns status code 400
Failure/Error: expect(response).to have_http_status(200)
expected the response to have status code 200 but it was 400
# ./spec/requests/blogs_spec.rb:28:in `block (4 levels) in <top (required)>'
Ok, so accordingly to your question + comments, I can assume you are running your tests within test environment, but you are expecting to find a User existing in development database.
FactoryBot
You might wanna use FactoryBot to create records for your testing suite.
Add to your Gemfile:
group :development, :test do
gem 'factory_bot_rails'
end
In rails_helper.rb, add:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
Now you should create your User factory. Create a new file spec/factories/user.rb with the following:
FactoryBot.define do
factory :user do
api_key { '123123' }
# You should define every any other required attributes here so record can be created
end
end
Finally, in your spec file:
....
context 'when the request is valid' do
before { get '/blogs', params: { api_key: user.api_key} }
let!(:user) { create(:user) }
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
...
Now your test should pass. Notice that in testing database there is no Blog created also, so:
let!(:blogs) { Blog.limit(10) }
Will return an empty array. You will need to create a Blog factory too, and create blogs like:
let!(:blogs) { create_list(:blog, 2) }
Bonus
As soon as you start improving your tests, you may wanna take a look at Faker and Database Cleaner for ActiveRecord
In my Rails 5 app I have this:
class InvoicesController < ApplicationController
def index
#invoices = current_account.invoices
respond_to do |format|
format.csv do
invoices_file(:csv)
end
format.xml do
invoices_file(:xml)
end
end
end
private
def invoices_file(type)
headers['Content-Disposition'] = "inline; filename=\"invoices.#{type.to_s}\""
end
end
describe InvoicesController, :type => :controller do
it "renders a csv attachment" do
get :index, :params => {:format => :csv}
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(response).to render_template :index
end
end
My problem is that my Spec always passes (!), even when I put a bunch of crap into my index.csv.erb file. It seems that the view file isn't even evaluated / tested by RSpec.
How is this possible? What am I missing here?
Controller tests/specs are these weird stubbed creations born out of the idea of unit testing controllers in isolation. That idea turned out to be pretty flawed and has really fallen out of vogue lately.
Controller specs don't actually make a real HTTP request to your application that passes through the routes. Rather they just kind of fake it and pass a fake request through.
To make the tests faster they also don't really render the views either. Thats why it does not error out as you have expected. And the response is not really a real rack response object either.
You can make RSpec render the views with render_views.
describe InvoicesController, :type => :controller do
render_views
it "renders a csv attachment" do
get :index, format: :csv
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(response).to render_template :index
end
end
But a better and more future proof option is using a request spec.
The official recommendation of the Rails team and the RSpec core team
is to write request specs instead. Request specs allow you to focus on
a single controller action, but unlike controller tests involve the
router, the middleware stack, and both rack requests and responses.
This adds realism to the test that you are writing, and helps avoid
many of the issues that are common in controller specs.
http://rspec.info/blog/2016/07/rspec-3-5-has-been-released/
# spec/requests/invoices
require 'rails_helper'
require 'csv'
RSpec.describe "Invoices", type: :request do
let(:csv) { response.body.parse_csv }
# Group by the route
describe "GET /invoices" do
it "renders a csv attachment" do
get invoices_path, format: :csv
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(csv).to eq ["foo", "bar"] # just an example
end
end
end
The format option should be specified outside of the params, i.e. get :index, params: {}, format: :csv}.
Regarding RSpec evaluating views, no, in controller tests, it doesn't, regardless of the format. However, it's possible to test views with RSpec: https://relishapp.com/rspec/rspec-rails/v/2-0/docs/view-specs/view-spec
I am new to RSpec but here I am trying to create tests based on this code and I am keep on getting this error. Any suggestions?
CODE:
serialization_scope nil
before_action :set_list, only: [:show, :destroy, :update]
before_action :verify_user, only: :show
def create
#list = current_user.lists.build(list_params)
if #list.save
render json: {message: ['Success']}, status: 200
else
render json: {errors:[#list.errors.full_messages]}, status: 400
end
end
Here is the RSpec file that I started :
require "rails_helper"
RSpec.describe V1::ListsController, :type => :controller do
describe "POST create" do
it "returns HTTP status" do
expect(post :create).to change(#list, :count).by(+1)
expect(response).to have_http_status :success #200
end
end
describe 'GET status if its not created' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
expect(response.status).to eq 400
end
end
end
And the error that I got is :
Failures:
1) V1::ListsController GET status if its created returns HTTP status
Failure/Error: expect(post :create).to change(#list, :count).by(+1)
expected #count to have changed by 1, but was not given a block
# ./spec/controllers/lists_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
2) GET status if its not created return HTTP status - reports BAD REQUEST (HTTP status 400)
Failure/Error: expect(response.status).to eq 400
expected: 400
got: 200
(compared using ==)
Try this code.
require 'rails_helper'
RSpec.describe V1::ListsController, type: :request do
describe 'valid request' do
it 'returns HTTP status' do
post '/list', params: { list: { list_name: 'xyz' } }
expect(response.status).to eq 201
end
end
describe 'invalid request' do
it "should return unauthorized" do
post '/list'
assert_response :unauthorized
end
end
end
In params you need to pass your list_params.
Spec would look like:
describe "POST create" do
context 'valid request' do
it 'should increase #list item' do
expect { post :create }.to change(List, :count).by(1)
end
it "returns HTTP status" do
post :create
expect(response).to have_http_status :success #200
end
end
context 'invalid request' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
get :create
expect(response.status).to eq 400
end
end
end
Cheers!
You can test an object not being created by intentionally causing some of its validations to fail e.g. you can pass a mandatory attribute as nil from the RSpec.
Sample request: post :create, { title: nil }.
But as per your RSpec code, it seems there are no validations on List model. So, lets try to stub save and return false for this particular test.
describe 'GET status if its not created' do
# Assuming your model name is `List`
before { allow_any_instance_of(List).to receive(:save) { false } }
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
post :create
expect(response.status).to eq 400
end
end
Please post your model for list and i can update the answer with more appropriate test.
Ishika, let me see if I can help you :)
RSpec official documentation recommends you to use request specs instead of controller specs. That is recommended because Rails 5 deprecated some methods used on controller testings. You can read more about this here at RSpec blog
ps.: You can use controller tests so far, but it can be deprecated in a future major version of RSpec.
There are some notes I left after the code, please read them also.
I would write a request spec like this:
# spec/requests/v1/lists_controller_create_spec.rb
require "rails_helper"
RSpec.describe V1::ListsController do
describe 'success' do
it 'returns ok and creates a list', :aggregate_failures do # :aggregate_failures is available only for RSpec 3.3+
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.to change { List.count }.from(0).to(1)
expect(response).to have_http_status(:ok)
end
end
describe 'bad request' do
before do
# This is needed because your controller is not validating the object, but look at my
# comment below (out of the code), to think about this behavior, please.
allow_any_instance_of(List).to receive(:save).and_return(false)
end
it 'returns a bad request and does not create a list' do
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.not_to change { List.count }
expect(response).to have_http_status(:bad_request)
end
end
end
Notes:
I suggested using more than 1 expectation by example, that is ok in this spec because they are simple and because I'm using :aggregate_failures option. With this option, if the first expectation fails, the next expectations will also be executed, considering that in this case, the following expectations does not depend on the first one, it is ok to use more than 1 expectation for the example.Reference
You are returning a bad request if the object is not saved, but you are not validating it. If your model has validations that will validate the object there, please adjust the specs to fail the save (instead of using the mock I used) and consider rendering an error message in the response
If you think that making the post inside a expect block, you can do different: Store the count of Lists in a variable before making the post and after the post you test if the variable has changed or not, maybe you think it will be more clear and it will do exactly the same thing in the background.
I posted a question (Rails XML builder not rendering) regarding rendering XML using XmlBuilder. That particular issue was resolved but I’ve run into another issue testing this controller action.
The issue seems to be with Rspec as because the same test written in Minitest works fine and using cURL works too.
While debugging this, Rspec never tries to render the xml builder template. It just skips over the template and renders a http status 200 with a blank body.
Here are the controller, xml builder, tests, and test results:
controllers/bars_controller.rb
require 'builder'
class BarsController < ApplicationController
def show
#bar = Bar.find(params[:id])
render template: 'bars/show.xml.builder', formats: [:xml]
end
end
/views/bars/show.xml.builder
xml.instruct!
xml.bar do
xml.foo(#bar.foo)
xml.bar(#bar.bar)
end
/test/controllers/bars_controller_test.rb
require 'test_helper'
class BarsControllerTest < ActionController::TestCase
setup do
#bar = Bar.create(foo: 'bar', bar: 'foo')
end
test "should show bar" do
get :show, id: #bar
assert_response :success
assert_match "<bar>", response.body
end
end
/spec/controllers/bars_controller_spec.rb
require_relative '../rails_helper'
describe BarsController do
describe 'a POST to :show' do
before do
#bar = Bar.create(foo: 'bar', bar: 'foo')
post :show, id: #bar
end
it 'should show bar' do
expect(response).to be_success
expect(response.body).to include("<bar>")
end
end
end
Test results
> rake test
Run options: --seed 50688
# Running:
.
Finished in 0.096901s, 10.3198 runs/s, 30.9593 assertions/s.
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips
> rspec spec/controllers/bars_controller_spec.rb
F
Failures:
1) BarsController a POST to :show responds with xml bar
Failure/Error: expect(response.body).to include("<bar>")
expected "" to include "<bar>"
# ./spec/controllers/bars_controller_spec.rb:13:in `block (3 levels) in <top (required)>'
Finished in 0.01726 seconds (files took 1.53 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/bars_controller_spec.rb:11 # BarsController a POST to :show responds with xml bar
RSpec controller tests do not render views by default. In order to render the views, add render_views to the describe block:
describe 'a POST to :show' do
render_views
before do
#bar = Bar.create(foo: 'bar', bar: 'foo')
post :show, id: #bar
end
it 'should show bar' do
expect(response).to be_success
expect(response.body).to include("<bar>")
end
end
I am writing some specs and the following is failing but the page /menus/1 is loading fine in a browser. This is a port of a php app and is first time I've used RSpec. Any thoughts as to why it might not be working.
The error is:
1) MenusController GET 'show' should be succesful
Failure/Error: get :show, :id => 1
ActiveRecord::RecordNotFound:
Couldn't find MenuHeader with id=1
# ./app/controllers/menus_controller.rb:18:in `show'
# ./spec/controllers/menus_controller_spec.rb:7:in `block (3 levels) in <top (required)>'
but that specific MenuHeader does exist based upon all normal criteria (console, mysql, browser). I'm 99% sure I have a mistake in my spec:
require 'spec_helper'
describe MenusController do
describe "GET 'show'" do
it "should be succesful" do
get :show, :id => 1
response.should be_success
end
end
end
here is the menus_controller.rb
def show
#menu_header_data=MenuHeader.find(params[:id])
respond_to do |format|
format.html # show.html.erb
# format.json { render json: #menu } to do
end
end
thx
When testing a controller with Rspec or TestUnit I would use a Factory or Fixture to pass the id rather than setting up a test database with data. It's better to test with something like:
Using FactoryGirl (My Recommendation but everyone has their own tastes):
describe MenusController do
describe "GET 'show'" do
it "should be succesful" do
get :show, :id => Factory(:menu).id
response.should be_success
end
end
end
The test is mainly just to make sure the controller responds properly when provided valid data, and using Factories or Fixtures is much less brittle. It will become a pain to maintain your test suite if it's based on hard data like fixtures or a db backup, and that could ultimately lead to you giving up on Test Driven Development rather than embracing it.