For Heaven’s Sake, At Least Test the CRUD

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
undefinedmethod`blahblahblah'fornil: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.

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.

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,
visitunit_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).tohave_link('Karl, Tara')

expect(page).tohave_text('Committee Chair')

expect(page).tohave_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

beforedo

click_link'Karl, Tara'

end

it'clicking the name of the users goes to the edit view'do

expect(page.current_path).toeq(unit_adult_path(@unit,@user))

expect(page).tohave_text('Adult: Tara Karl')

end

it'clicking Return returns to adult list'do

click_link'Return To List'

expect(page).tohave_text('Adults in Pack 134')

expect(page.current_path).toeq(unit_adults_path(@unit))

end

it'clicking Edit edits the adult'do

click_link"Edit"

expect(page).tohave_text('Edit Adult')

expect(page.current_path).toeq(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
afterdo 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.

#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.

expect(page).tohave_text('Bonnie Doom, and all associated data, was permanently deleted')

expect(page).to_nothave_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

beforedo

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).tohave_text('New Adult in Cub Scout Pack 134')

within'form#new_adult'do

fill_in'Email',with:'rusty.balls@aol.com'

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).toeq(unit_adult_path(@unit,rusty_balls))

expect(page).tohave_text('Rusty Balls was successfully created')

expect(page).tohave_text('Adult: Rusty Balls')

expect(page).tohave_text('Cub Scout Pack 134')

expect(page).tohave_text('rusty.balls@aol.com')

expect(page).tohave_text('Webmaster, Joker')

expect(page).tohave_text('3660 N Lake Shore Dr')

expect(page).tohave_text('Chicago, IL 60657')

expect(page).tohave_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).tohave_text('Rusty Balls was successfully created')

expect(page).tohave_link('Aydan Russel')

expect(page).tohave_link('Bocephus Jones')

end

it'allows an adult to have no related scouts'do

click_button'Create Adult'

expect(page).tohave_text('Rusty Balls was successfully created')

expect(page).to_nothave_link('Aydan Russel')

expect(page).to_nothave_link('Bocephus Jones')

end

end

context'with invalid data'do

it'displays flash messages with errors'do

click_button'Create Adult'

expect(page).tohave_text('First name can\'t be blank')

expect(page).tohave_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).tohave_text('First name can\'t be blank')

expect(page).tohave_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).toeq(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.

Share this:

Related

One Response

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.