Contexts are a powerful way to make your tests clear and well organized. They should start with when.
it'has 200 status code if logged in'doexpect(response.code).toeq('200')enddescribe'it returns 401 status code if not logged in'doit{expect(response.code).toeq('401')}end
context'when logged in'doit'returns 200 status code'doexpect(response.code).toeq('200')endendcontext'when logged out'doit'returns 401 status code'doexpect(response.code).toeq('401')endend
Tests are not supposed to be DRY, but that doesn't mean we need to repeat ourselves in vain. Leverage described_class to make your tests maintainable over the years.
describePilotdodescribe'.most_successful'doit'returns the most successful pilot'dosenna=create(:pilot,name: 'Ayrton Senna')create(:pilot,name: 'Alain Prost')create(:race,winner: senna)most_successful_pilot=Pilot.most_successfulexpect(most_successful_pilot.name).toeq('Ayrton Senna')endendend
describePilotdodescribe'.most_successful'doit'returns the most successful pilot'dosenna=create(:pilot,name: 'Ayrton Senna')create(:pilot,name: 'Alain Prost')create(:race,winner: senna)most_successful_pilot=described_class.most_successfulexpect(most_successful_pilot.name).toeq('Ayrton Senna')endendend
If the class Pilot ever gets renamed, one just need to change it at the top level describe.
A spec description must not be longer than 100 characters neither span many lines. If this happens you should split it using context.
it'returns 422 when user first_name is missing from params and when user last_name is missing from params'doendit'returns 422 when user first_name is missing from params \
and when user last_name is missing from params'doend
context'when user first_name is missing from params'doit'returns 422'doendendcontext'when user last_name is missing from params'doit'returns 422'doendend
Having an it for each expectation can lead to a terrible test performance. Group expectations that use a similar data setup to improve performance and make the tests more readable.
it'responds with 200 http status and a JSON content type'douser=create(:user)getuser_path(user)expect(response.code).toeq('200')expect(response.content_type).toeq('application/json')end
describe'#destroy'docontext'when the product exists'doit'deletes the product'doendendend
describe'#destroy'docontext'when the product exists'doit'deletes the product'doendendcontext'when the product does not exist'doit'raises 404'doendendcontext'when user is not authenticated'doit'raises 404'doendendend
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. In Rails 5, request specs are significantly faster than either request or controller specs were in Rails 4.
Do not use let / let!. These tend to turn your tests very complicated over time as one needs to look up variables defined then apply deltas to figure their current state. Understand more here.
Tests are not supposed to be DRY, but easy to read and maintain.
Avoid hooks since they usually cause your tests to become more complex in the long run.
describe'#index'docontext'when user is authenticated'dobeforedo@user=create(:user)sign_in@usergetprofile_pathendcontext'when user has a profile'doit'returns 200'docreate(:profile,user: @user)expect(response.code).toeq('200')endendcontext'when user does not have a profile'doit'returns 404'doexpect(response.code).toeq('404')endendendend
describe'#index'docontext'when user is authenticated'docontext'when user has a profile'doit'returns 200'douser=create(:user)create(:profile,user: user)sign_inusergetprofile_pathexpect(response.code).toeq('200')endendcontext'when user does not have a profile'doit'returns 404'douser=create(:user)sign_inusergetprofile_pathexpect(response.code).toeq('404')endendendend
describe'#github_stars'doit'displays the number of stars'doexpect(subject.github_stars(1)).toeq('Stars: 10')endend
describe'#github_stars'doit'displays the number of stars'doexpect(Github).toreceive(:fetch_repository_stars).with(1).and_return(10)expect(subject.github_stars(1)).toeq('Stars: 10')endend
Unless you intentionally want to test the underlying dependencies. This type of test must be limited to very few cases though otherwise it will make your test suite slow and hard to maintain.
describe'#process'doit'creates a user and a product'dopayload={user: {name: 'Isabel'},product: {name: 'Book'}}subject.process(payload)expect(User.count).toeq(1)expect(Product.count).toeq(1)endend
Sometimes you need to access external services. In these cases you can't rely on the real service but you should stub it with solutions like webmock or VCR.
describe'#notify_sales_team'doit'prepares the email'dosubject=described_class.notify_sales_teamexpect(subject.to).toeq(['sales-group@company.com'])expect(subject.subject).toeq('New sale')endend
describe'#notify_sales_team'doit'prepares the email'dostub_env('SALES_TEAM_EMAIL','sales-group@company.com')subject=described_class.notify_sales_teamexpect(subject.to).toeq(['sales-group@company.com'])expect(subject.subject).toeq('New sale')endend
Create only the data you need for each test. Having more data than necessary might make your suite slow.
describe'.featured_product'doit'returns the featured product'docreate_list(:product,5)product_featured=create(:product,featured: true)expect(described_class.featured_product).toeq(product_featured)endend
describe'.featured_product'doit'returns the featured product'docreate(:product,featured: false)product_featured=create(:product,featured: true)expect(described_class.featured_product).toeq(product_featured)endend