What’s a pirate’s favorite testing framework? ArrrrrrrrSpec!
This past week I’ve been working through Aaron Sumner’s “Everyday Rails Testing with RSpec” in order to bone up on my TDD. This is the CliffsNotes version since, unlike ‘Romeo and Juliet’, you can’t run down to Blockbuster and rent the movie. Mostly because good luck finding a Blockbuster and also, not a lot of movies about Rails… or RSpec… I didn’t notice any model specs in the 1995 classic ‘Hackers’.
So here it is, my notes, chapter by chapter.
- 01. Introduction / 02. Setup
- 04. Generating Test Data with Factories
- 05. Basic Controller Specs
- 06. Advanced Controller Specs
- 07. Controller Spec Cleanup
- 08. Feature Specs
- 09. Speeding up Specs
- 10. Testing the Rest
01. Introduction / 02. Setup
Tests should be:
- Reliable
- Easy to write
- Easy to read
Steps for Setup:
- Add gems
- Add test DB
- config/database.yml (pdf pg 24)
- Instal Rspec
- bin/rails g rspec:install
- Configure RSepc
- In ‘.rspec’ add ‘—format documentation’
- Other configurations in ‘/spec/rails_helper.rb’ and ‘/spec/spec_helper.rb’
- Create Spec executable
- ‘bundle binstubs rspec-core’
03. Model Specs
Describe a set of expectations
- Each example only expects one thing
- Each example is explicit
- Each example’s description begins with a verb, not should
Describe, Context, Before and After hooks
- can have nested describe blocks and separate contexts within describe blocks.
- describe and context technically interchangeable.
- good style is to use describe to outline general functionality and context to outline a specific state.
- describe and context technically interchangeable.
- before block is run BEFORE each example in its describe block
- before :each is default behavior of before (may omit the each if desired)
- after for cases such as disconnecting from a service
- Some developers use method names for the description of nested describe blocks
- ex) #by_letter
Summary:
- Active and explicit expectations
- Test for what should as well as what should not happen
- Test edge cases
- Organize for readability
04. Generating Test Data with Factories
- Locate files in spec/factories directory
1 2 3 4 5 6 7 |
|
- Can pass ruby code into FactoryGirl to dynamically assign values.
- pass in within a block similar to example above
- Filenaming convention not as strict as specs (could place all factories in single file if desired) but typically save as a filename that is the plural of the model name.
- Using FactoryGirl in tests
- .create persists the instance
- .build does not (sim to .new for regular ruby objects)
1
|
|
- Override attributes:
1
|
|
Simplifying Syntax
spec/rails_helper.rb
1 2 3 4 5 |
|
- allows use of “build(:contact), create(:contact), attributes_for(:contact), build_stubbed(:contact)”
Inherited Factories
- nested ‘factory’ blocks are factories inheriting from top level factory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
- “association” creates a new ‘Contact’ on the fly for instance of a phone to belong to if one isn’t specifically passed to the factory method
1 2 |
|
Fake Data
- Faker
1 2 3 4 5 6 7 |
|
Advanced Associations
- FactoryGirl callbacks
1 2 3 4 5 6 7 8 9 10 11 12 |
|
05. Basic Controller Specs
- place in ‘spec/controllers’
Why test controllers?
- Controllers are classes with methods too
- Controller specs can often be written more quickly than their integration spec counterparts
- Controller specs usually run more quickly than integration specs
Syntax
- Takes http method (get/post), controller method (:index, :create, :new, :show, :edit, :update, :destroy), and (optional) parameters passed to the method
- attributes_for
- ‘FactoryGirl.attributes_for(:contact)’ produces hash
- Create factories for invalid data to test controllers
1 2 3 4 5 6 7 8 9 10 11 |
|
Controller Spec DSL
1 2 3 4 5 6 7 8 9 10 11 12 |
|
- expects(assigns(:contact)).to eq contact checks that @contact varible created in the controller is equal to contact created in test.
- expect(response).to render_template :show checks that the response from controller back to browser should render using show.html.er
- Can pass procs to expect
- expect{ post :create, contact: attributes_for(:contact, phones_attributes: @phones)}.to change(Contact, :count).by(1)
06. Advanced Controller Specs
- Testing authentication/login, authorization/roles
07. Controller Spec Cleanup
- Reducing redundancy and brittleness w/o sacrificing readability
- share examples across describe and context blocks
- helper macros to reduce repetition
- create custom RSpec matchers
Shared Examples
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Helper Macros
- Place macro files in ‘spec/support’ as a module to be included in RSpec’s configuration
- Inside spec/rails_helper.rb -> RSpec.configure block, add “config.include ModuleName”, and require the file at the top
- when using authentication through devise or similar refer to their docs for incorporation into test suite
Using Custom RSpec Matchers
- place custom matchers in ‘spec/support/matchers’, one matcher per file. Should detect files by default
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
- In Spec
1 2 3 4 |
|
08. Feature Specs
- Feature specs/ integration testing, aka. acceptance tests
- Represents how users will interact with your code
- within feature specs it is ok to have multiple expectations for a given scenario.
- can also have expectations mid-test
- Launchy- saves the feature spec’s current HTML to a temp file and renders in default browser to see result of previous step in spec insert ‘save_and_open_page’ into spec.
Including Javascript Interactions
- Default Capybara web-driver (Rack::Test) cannot handle javascript, so ignores it.
- Use Selenium instead.
- js: true takes into account javascript by running a javascript capable web-driver
1 2 3 4 5 |
|
- If firefox hangs at a black page and the spec fails and returns the error:
1
|
|
- check gem file and remove version numbers (if present) from selenium web-driver to use the latest version.
- if that fails ‘bundle update selenium-webdriver’
- Configure database cleaner in ‘RSpec.configure’
1 2 3 4 5 6 7 8 9 10 11 12 |
|
- Next, monkey patch ActiveRecord to use threads
- Add additional file spec/support/shared_db_connection.rb
- Needed to share data state across the Selenium web server and the test code itself
1 2 3 4 5 6 7 8 |
|
- Headless options for Javascript (run test without waiting for firefox to launch):
- capybara-webkit
- Poltergeist
Waiting for Javascript
- In ‘rails_helper.rb’ set Capybara.default_wait_time = ## to change the amount of time that Capybara will wait before giving up on finding an item from default value of 2 to desired number of seconds.
09. Speeding up Specs
- Refactoring for speed
- Amount of time it takes to run specs
- How quickly you can create meaningful, clear specs
Optional Terse Syntax
- let() in place of before :each
- caches the value without assigning it to an instance variable
- lazily evaluated. does not get assigned until a spec calls upon it.
- let(:smith){create(:contact, firstname: ‘Lawrence’, lastname: ‘Smith’)}
- refer to with non-symbol version of item inside let(), i.e.) :smith to smith
- for tests requiring persisted data call the variable before the expects to instantiate the object and persist it to the db
- let()! forces the var to be assigned prior to each example
- subject{}
- it{} and specify{} (synonymous)- one line versions of it statements
- subject{ build(:user, name1: ‘J’, name2: ‘Doe’) }; it{ is_expected.to be_named ‘J Doe’}
- Shoulda - shoulda-matcher gem
- include gem in :test group for access to many helpful matchers
Mocks and Stubs
- Mock- some object representing a real object for testing purposes. aka test doubles. Mocks do not touch the database- thus less time to set up in a test
- use FactoryGirl build_stubbed() method to generate a fully-stubbed fake. Does not persist.
- Stub- overrides a method call on a given object and returns a predetermined value for it.
- allow(Contact).to receive(:order).with(‘lastname, firstname’).and_return([contact])
Automation with Guard and Spring
- Guard watches specified files and executes actions based on what it observes
- add guard-spec to test and development groups
- create Guardfile from the command line: guard init spec
- run ‘guard’ in terminal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
- Spring keeps rails app running after 1st run of test suite to eliminate spin-up time and make test suite run faster on subsequent executions
- add ‘spring-commands-rspec’ to gemfile :development
- make the new ‘bin/rspec’ available as a binstub
- ‘spring binstub rspec’
- ‘spring stop’
Add Tags to Tests
1 2 3 |
|
1
|
|
- runs only test tagged with focus: true
- add to rails_helper:
- ‘config.run_all_when_everything_filtered = true’
- Can configure RSpec to only run/never run examples with specific tags
1 2 3 4 |
|
- Skipping unneeded tests
1 2 3 |
|
10. Testing the Rest
- Email delivery, file uploads, manipulating time within specs, testing against external web services, how to test your application’s API, testing rake tasks.
- Emails: ‘email_spec’ gem
- add to rails helper:
- ‘config.include(EmailSpec::Helpers)’
- ‘config.include(EmailSpec::Matchers)’
- add to rails helper:
- Allows addition to spec such as:
- ‘expect( open_last_email ).to be_delivered_from sender.email’
- ‘expect( open_last_email ).to have_reply_to sender.email’
- ‘expect( open_last_email ).to be_delivered_to recipient.email’
- ‘expect( open_last_email ).to have_subject message.subject’
- ‘expect( open_last_email ).to have_body_text message.message’
- open_last_email is helper that opens the most recently sent email and gives you access to its attributes
- Can also create new email objects and work directly with them
1 2 3 |
|
Can also access and test against messages without extra dependency
- Each time a message is sent out it gets pushed to ActionMailer::Base.deliveries
- Can access latest by ActionMailer::Base.deliveries.last
- clear delivered emails by ActionMailer::Base.deliveries = []
- Testing: expect(ActionMailer::Base.deliveries.last).to include user.email
Testing the Time
- Timecop gem - freeze time
1 2 3 4 5 6 |
|
- Testing Web Services
- VCR gem- watches for external http requests coming from your code. When it encounters one it causes the test to fail. In order to make it pass you must create a ‘cassette’ onto which to record the http transaction. Run the test again, and the vcr captures the request and response into a file. Now future test making the same request will use data from the file instead of making another network request to the api.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
- Testing Your Application’s API
- API specific test go in ‘spec/requests’
- use simple get, post, delete, patch http verbs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Quick Ref:
Matchers:
- .to, .not_to, .to_not
- be_valid
- include()
- eq / eql
- not==
- match_array - ignores order in array, contents only
- render_template
- redirect_to
- be_a_new()
- change(Contact, :count)
- match - used whenever a regex is being compared to the actual result
Gems Used:
- Launchy
- opens default web browser to show what the application is rendering
- Faker
- generates names, emails, other placeholders for factories
- Bundler
- VCR for testing
- Factory Girl
- Replaces Rails’ default fixtures for feeding test data to the test suite
- https://github.com/thoughtbot/factory_girl
- https://robots.thoughtbot.com/get-your-callbacks-on-with-factory-girl-3-3
- github.com/railsapps/rails-composer
- rails application template to automatically add spec and related config to gem file and application config files, create test db
- RSPEC-expectation repository on github
- Faker
- http://www.rubydoc.info/gems/faker
- Forgery- Faker alternative: https://github.com/sevenwire/forgery
- ffaker- faster rewrite of faker: https://github.com/ffaker/ffaker