The very first integrated (feature) specs I setup it is to test the CRUD. If you don’t do anything else with feature specs, please, at least do these tests. You will be amazed at how much of your application is actually exercised and covered just with these simple tests.
Yes, you can and should test much more. And they don’t replace good unit and integrated tests. But you will be surprised how many bugs these basic tests will catch. For example, my nemesis, the dreaded undefined method `blahblahblah' for nil:NilClass in views.
These test will ensure the Adult model will:
- Display all views (this alone is huge)
- Data can be entered, displayed, updated, and destroyed (CRUD)
- Model validations are respected
- Potentially test model relations
I am going to show you how to test #index, #show, #edit, #update, #new, #create, and #destroy of a resource, an Adult model. I’ll break it down because the full spec file is long.
Here is the entire spec in it’s original project: adults_spec.rb
Setup
A little pre-knowledge would help; we are using Devise (for authentication), Rspec (for testing framework), Capybara (for integration tests). Note, we are not testing JavaScript in this test, and therefore invoke the RackTest as the Capybara driver for maximum speed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
require 'rails_helper' include Warden::Test::Helpers RSpec.describe 'Adults' do before(:all) { Capybara.default_driver = :rack_test } before(:all) do Warden.test_mode! seed_all_records end before(:each) do Warden.test_reset! login_as(@user, scope: :user) visit unit_adults_path(@unit) end # tests go hear... end |
Warden.test_mode! gives us some nice Warden helper methods to use during testing. Notably the login_as(user, scope) method. Warden is used under-the-hood by Devise.
seed_all_records is just a method that will create a complete structure of records so that we have something to find and display. For example, it could create a user, company, orders, invoices, inventory, etc.
IMPORTANT: I am loading these records in a before(:all) do block so that they are not recreated for every test. If seed_all_records creates a long and complex amount of records, as this real-life version does, it can dramatically speed up the test.
CAVEAT: If we need to create other new records, we need to manually keep track of them to delete them after use. See the #edit example.
For every test, the first thing we do is view the index, visit unit_adults_path(@unit) . I like to start here and “click” the appropriate links to edit/show/delete tests.
#index
So we take a look at the index page, a table with a list of names (adults) and we check that we see some basic data (links and text).
1 2 3 4 5 6 7 8 9 |
context 'when viewing a list of unit adults' do it 'shows the adult name, leadership position, and scouts' do within 'body .container-fluid table' do expect(page).to have_link('Karl, Tara') expect(page).to have_text('Committee Chair') expect(page).to have_link("Aydan Russel") end end end |
#show
We view the show page by clicking a link from the index page (Adult name). First, we make sure we are looking at the right show page. Then we click the “Return To List” link and make sure we go back to the index page. Finally, we try the “Edit” link of the Adult name and make sure were are taken to the edit form.
We look at the page.current_path and test it to make sure we are on the right page. This may not work in your application as some paths may contain query values or other errata. But for simple specs with standard Rails routes, it works.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
context 'when showing an existing adult' do before do click_link 'Karl, Tara' end it 'clicking the name of the users goes to the edit view' do expect(page.current_path).to eq(unit_adult_path(@unit, @user)) expect(page).to have_text('Adult: Tara Karl') end it 'clicking Return returns to adult list' do click_link 'Return To List' expect(page).to have_text('Adults in Pack 134') expect(page.current_path).to eq(unit_adults_path(@unit)) end it 'clicking Edit edits the adult' do click_link "Edit" expect(page).to have_text('Edit Adult') expect(page.current_path).to eq(edit_unit_adult_path(@unit, @user)) end end |
#edit, #update
Like I noted above, we will need to create an Adult because we are going to change their data and we don’t want to affect the existing records. In an after do block, we will delete this Adult.
We create this new Adult, then we make some changes in the edit form. But don’t submit yet! We’ll do that later.
In the first test, we submit the change, and make sure that any form changes we make are reflected on the Adult’s show page. Success! We can edit and submit Adult records. This tests a significant amount of the Adult model, validations, and potentially more (relations, counters, etc).
Lastly, we test that if the user cancels the edit, they are returned to the Adult show page and none of the entered changes are saved.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
context 'editing an adult' do before do @user2 = FactoryGirl.create(:adult, first_name: 'Bonnie', last_name: 'Doom') @user2.units << @unit visit unit_adult_path(@unit, @user2) click_link "Edit" within "form#edit_adult_#{@user2.id}" do fill_in 'adult_first_name', with: 'Fionula' fill_in 'adult[unit_positions_attributes][0][additional]', with: 'Jester' end end after do @user2.destroy end it 'updates fields when changed and returns to the show page' do click_button 'Update Adult' expect(page.current_path).to eq(unit_adult_path(@unit, @user2)) expect(page).to have_text('Fionula Doom was successfully updated') expect(page).to have_text('Fionula Doom') expect(page).to_not have_text('Bonnie Doom') expect(page).to have_text('Jester') end it 'canceling update does not change data and returns to show page' do click_link 'Cancel' expect(page.current_path).to eq(unit_adult_path(@unit, @user2)) expect(page).to have_text('Bonnie Doom') expect(page).to_not have_text('Fionula Doom') end end |
#destroy
“I brought you into this world, I can take you out.”
Create a new Adult, then revisit the index page and click on the new Adult’s destroy link, and make sure they are gone and return to the index page. We also check that the flash notification is displayed.
Note: if you use a confirm: 'Are you sure?' like Rails suggests as a default for destroy links, because we are using RackTest, it will not execute that JavaScript. Thus there is not confirmation dialog to click.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
context 'deleting an adult' do before do @user2 = FactoryGirl.create(:adult, first_name: 'Bonnie', last_name: 'Doom') @user2.units << @unit visit unit_adult_path(@unit, @user2) end it 'deletes the user and returns to the adult list' do click_link 'Destroy Bonnie' expect(page.current_path).to eq(unit_adults_path(@unit)) expect(page).to have_text('Bonnie Doom, and all associated data, was permanently deleted') expect(page).to_not have_text('Doom, Bonnie') end end |
#new, #update
Saved the longest for last. Also, the most important.
We are going to test two contexts: entering valid data, and entering invalid data.
Enter Valid Data
Simple, we click on the New Adult link, enter a bunch of data into the form, and save it. Then we verify that we are taken to the show page and can see all the data we entered.
This particular form has several select elements that allow the user to select from a list of options, as well as establish relations to other models. This is a good place to test this, but it could also be tested in #edit, #update. We verify that this Adult can have related Scouts, and no related Scouts. This also tests your models, validations, and relations. Good tests to have.
Enter Invalid Data
We want to see that when we enter invalid data, that model validations are respected, and that the record is not saved. When trying to submit invalid data, we return to the new form and check that the proper flash message is displayed.
In this example, I only test one field’s validation, that the Adult’s first_name can’t be blank. This is an excellent place to add more test for more validations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
context 'when adding a new adult' do before { click_link 'New Adult' } context 'with valid data' do before do within 'form#new_adult' do fill_in 'adult_first_name', with: 'Rusty' fill_in 'adult_last_name', with: 'Balls' end end it 'enter new adult info, submit, goto to adult show and display show adult page' do expect(page).to have_text('New Adult in Cub Scout Pack 134') within 'form#new_adult' do fill_in 'Email', with: '[email protected]' select 'Webmaster', from: 'adult_unit_positions_attributes_0_leadership' fill_in 'adult_unit_positions_attributes_0_additional', with: 'Joker' select '1955', from: 'adult_birth_1i' select 'February', from: 'adult_birth_2i' select '14', from: 'adult_birth_3i' fill_in 'Address1', with: '3660 N Lake Shore Dr' fill_in 'adult_city', with: 'Chicago' select 'Illinois', from: 'adult_state' fill_in 'adult_zip_code', with: '60657' # this should be tested later with javascript select 'Russel, Aydan', from: 'adult_scout_ids' end click_button 'Create Adult' rusty_balls = User.find_by_first_name('Rusty') expect(page.current_path).to eq(unit_adult_path(@unit, rusty_balls)) expect(page).to have_text('Rusty Balls was successfully created') expect(page).to have_text('Adult: Rusty Balls') expect(page).to have_text('Cub Scout Pack 134') expect(page).to have_text('[email protected]') expect(page).to have_text('Webmaster, Joker') expect(page).to have_text('3660 N Lake Shore Dr') expect(page).to have_text('Chicago, IL 60657') expect(page).to have_link('Aydan Russel') end it 'allows the adult to have multiple related scouts' do within 'form#new_adult' do select 'Russel, Aydan', from: 'adult_scout_ids' select 'Jones, Bocephus', from: 'adult_scout_ids' end click_button 'Create Adult' expect(page).to have_text('Rusty Balls was successfully created') expect(page).to have_link('Aydan Russel') expect(page).to have_link('Bocephus Jones') end it 'allows an adult to have no related scouts' do click_button 'Create Adult' expect(page).to have_text('Rusty Balls was successfully created') expect(page).to_not have_link('Aydan Russel') expect(page).to_not have_link('Bocephus Jones') end end context 'with invalid data' do it 'displays flash messages with errors' do click_button 'Create Adult' expect(page).to have_text('First name can\'t be blank') expect(page).to have_text('Last name can\'t be blank') end it 'entered data is persisted' do within 'form#new_adult' do fill_in 'adult_last_name', with: 'Eustus' end click_button 'Create Adult' expect(page).to have_text('First name can\'t be blank') expect(page).to have_field('adult_last_name', with: 'Eustus') end end context 'clicking cancel' do it 'returns to adult list' do click_link 'Cancel' expect(page.current_path).to eq(unit_adults_path(@unit)) end end end |
Conclusion
Put it all together, and you are testing a lot. I know they seem like simple tests, maybe too simple for some. But I think they are boilerplate for much more.
I said it before, but it bears repeating: setting up these basic CRUD integrated tests will catch a surprising amount of bugs.
Before you remind me that “you didn’t test this” or “didn’t test that” or if you just added “this test” it would be so much better. I agree!
But that is not the point of this. Many devs, especially new ones, are mystified by integrated tests, even testing alone. The point is that if you just test the CRUD, you test a surprising amount of your application. And it might just save your ass one day.