Assumptions • We’re talking about business code, most probably Rails code • We like to have fun with our work • Code that’s easy to change is the most fun to work on • Tests should make our code easier to change Thursday, 22 August 13
Why we test • Fix bugs and prevent regressions • Improve design and expose design flaws • Provide documentation • Defer design decisions • Support abstractions Thursday, 22 August 13
Change • OO code is easiest to change when each class has • A single responsibility • As few dependencies as possible • See POODR Thursday, 22 August 13
Dependency when an object • Knows the name of another class • Knows the name of a message it intends to send to someone other than self • Knows the arguments a message requires • Knows the order of those arguments Thursday, 22 August 13
Tests • Depend on the code they test (duh!) • We want to minimise dependencies • So we want as few tests as possible • And they should test behaviour rather than implementation Thursday, 22 August 13
Two approaches to testing • State verification tests (black box) • Interaction verification (white box / mocks) • Prefer state as it doesn't couple you to implementation • Mockist style can be great when don't know the end state • You’ll end up using both Thursday, 22 August 13
Testing drives design • So don’t need to test design that is already setup for you • e.g. anything Rails • Tests should be telling you something Thursday, 22 August 13
Bad Tests (imho) 9 # bad 10 describe Project do 11 it { should have_many :users } 12 it { should validate_presence_of :owner } 13 it { should validate_presence_of :name } 14 it { should have_scope(:active).where(deleted: false) } 15 end • Don’t tell me anything or drive design • Completely coupled to implementation Thursday, 22 August 13
Good Tests (imo) 19 describe Project, "#name" do 20 it "is required" do 21 expect(subject.errors.on(:name)).to eql(I18n.t('errors.messages.empty')) 22 end 23 end 24 25 describe Project, ".active" do 26 it "doesn't return deleted projects" do 27 live = FactoryGirl.create(:project) 28 FactoryGirl.create(:project, deleted: false) 29 expect(Project.active).to eq([live]) 30 end 31 end • Don’t test things that other tests should cover • Do test things that are important (imho) Thursday, 22 August 13
What to write • Golden path acceptance test for the critical part of your application • Integration tests for every important feature • Unit tests for classes you create yourself to document public apis Thursday, 22 August 13
Acceptance test 1 # encoding: utf-8 2 require 'spec_helper' 3 4 describe "Billing" do 5 let(:email) { “[email protected]” } 6 7 before do 8 @student = create_student email 9 end 10 11 it "allows the student to add funds" do 12 add_funds(50, @student.email) 13 within "#user_balance" do 14 page.should have_text("€50") 15 end 16 end 17 end 18 Thursday, 22 August 13
Integration tests • Just a check for something you’d expect whenever everything is wired up correctly • Don’t use them as unit tests (testing for negatives) • Don’t use them as acceptance tests Thursday, 22 August 13
Shared integration tests 1 shared_examples_for "student only action" do |action| 2 let(:tutor) { FactoryGirl.create(:tutor) } 3 let(:student) { FactoryGirl.create(:student) } 4 5 before do 6 activate_authlogic 7 end 8 9 it "redirects when no user" do 10 do_action 11 response.should redirect_to("/user_sessions/new") 12 end 13 14 it "redirects when user is tutor" do 15 UserSession.create(tutor) 16 do_action 17 response.should redirect_to("/") 18 flash[:error].should == "That page is for students only" 19 end 20 21 it "performs action when user is a student" do 22 UserSession.create(student) 23 do_action 24 flash[:error].should be_nil 25 end 26 end Thursday, 22 August 13
Shared integration specs 3 describe Settings::BillingController, "#show" do 4 def do_action 5 get :show 6 end 7 8 it_behaves_like "student only action" 9 end Thursday, 22 August 13
Acceptance, Integration, or Unit? 1 Feature: Adding a translation from the command line 2 3 Scenario: Running add 4 In order to add a key and translation content 5 When I have a valid project on localeapp.com with api key "MYAPIKEY" 6 And an initializer file 7 When I run `localeapp add foo.baz en:"test en content" es:"test es content"` 8 Then the output should contain: 9 """ 10 Localeapp Add 11 12 Sending key: foo.baz 13 Success! 14 """ 15 16 Scenario: Running add with no arguments 17 In order to add a key and translation content 18 When I have a valid project on localeapp.com with api key "MYAPIKEY" 19 And an initializer file 20 When I run `localeapp add` 21 Then the output should contain: 22 """ 23 localeapp add requires a key name and at least one translation 24 """ Thursday, 22 August 13
Views • If they need tests they’re too complicated • Move logic out into presenter style class and test that • Integration test should be enough to show everything is wired together Thursday, 22 August 13
Controllers • Shouldn’t be doing any work themselves • Move logic out into domain logic classes and test those • Integration tests enough to show everything is wired together correctly • Shared tests to check security etc. Thursday, 22 August 13
Unit Tests • Stealing next slide from Sandi Metz • Really... Just go watch her video from RailsConf 2013 • “Magic Tricks of Testing” Thursday, 22 August 13
Query Command Assert result Assert direct public side e ects Ignore Expect to send The Unit Testing Minimalist Incoming Type @sandimetz Apr 2013 Message Ignore Sent to Self Outgoing Origin Saturday, April 27, 13 Thursday, 22 August 13
Use VCR • Recording allows you to keep your tests fast • Have an easy to way to remove recordings so you can always run against the endpoint in CI (if this makes sense) Thursday, 22 August 13
Tools I use • RSpec for unit tests • RSpec, Capybara, and Poltergeist for integration tests • VCR for testing API integration • Timecop for when time is important • Don’t have a go to for acceptance tests. Trying Mechanize, PhantomJS, bash & curl Thursday, 22 August 13