├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── TODO.md ├── bin └── publish ├── code └── sinatra │ ├── app.rb │ ├── app_spec.rb │ ├── config.ru │ ├── feature_spec.rb │ ├── member.rb │ ├── member_validator.rb │ ├── members.txt │ ├── spec_helper.rb │ └── views │ ├── delete.erb │ ├── edit.erb │ ├── index.erb │ ├── layout.erb │ ├── new.erb │ └── show.erb ├── config.rb ├── config.ru ├── data ├── book.yml └── toc.yml └── source ├── 01-preface.md ├── 02-testing.md ├── 02-testing ├── 01-output.md ├── 02-separation.md ├── 03-computed.md ├── 04-assert.md ├── 05-stages.md └── 06-classes.md ├── 03-libraries.md ├── 04-minitest.md ├── 05-rspec.md ├── 05-rspec ├── 01-basics.md ├── 02-matchers.md ├── 03-format.md ├── 04-advanced.md ├── 05-custom_matchers.md └── 06-filtering.md ├── 06-rack_test.md ├── 06-rack_test ├── 01-rack.md └── 02-sinatra.md ├── 07-headless.md ├── 07-headless ├── 01-phantomjs.md ├── 02-capybara.md ├── 03-features.md └── 04-test_types.md ├── 08-test_doubles.md ├── 09-analysis.md ├── 10-services.md ├── CNAME ├── assets ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── images │ └── favicon.png ├── javascripts │ ├── modernizr.js │ └── monstas.js └── stylesheets │ ├── _fonts.scss │ ├── _html5-reset.css │ ├── _layout.scss │ ├── _response.scss │ ├── _style.scss │ ├── monstas.scss │ └── syntax.css.erb ├── index.md ├── layouts └── layout.erb └── sitemap.xml.erb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | vendor/bundle 3 | build 4 | .sass-cache 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'middleman', '3.3.7' 4 | gem 'middleman-syntax', '2.0.0' 5 | gem 'middleman-toc' 6 | gem 'redcarpet' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.1.16) 5 | i18n (~> 0.6, >= 0.6.9) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.1) 9 | tzinfo (~> 1.1) 10 | celluloid (0.16.0) 11 | timers (~> 4.0.0) 12 | chunky_png (1.3.6) 13 | coffee-script (2.4.1) 14 | coffee-script-source 15 | execjs 16 | coffee-script-source (1.10.0) 17 | compass (1.0.3) 18 | chunky_png (~> 1.2) 19 | compass-core (~> 1.0.2) 20 | compass-import-once (~> 1.0.5) 21 | rb-fsevent (>= 0.9.3) 22 | rb-inotify (>= 0.9) 23 | sass (>= 3.3.13, < 3.5) 24 | compass-core (1.0.3) 25 | multi_json (~> 1.0) 26 | sass (>= 3.3.0, < 3.5) 27 | compass-import-once (1.0.5) 28 | sass (>= 3.2, < 3.5) 29 | erubis (2.7.0) 30 | execjs (2.7.0) 31 | ffi (1.9.14) 32 | haml (4.0.7) 33 | tilt 34 | hike (1.2.3) 35 | hitimes (1.2.4) 36 | hooks (0.4.1) 37 | uber (~> 0.0.14) 38 | i18n (0.6.11) 39 | json (1.8.3) 40 | kramdown (1.12.0) 41 | listen (2.10.1) 42 | celluloid (~> 0.16.0) 43 | rb-fsevent (>= 0.9.3) 44 | rb-inotify (>= 0.9) 45 | middleman (3.3.7) 46 | coffee-script (~> 2.2) 47 | compass (>= 1.0.0, < 2.0.0) 48 | compass-import-once (= 1.0.5) 49 | execjs (~> 2.0) 50 | haml (>= 4.0.5) 51 | kramdown (~> 1.2) 52 | middleman-core (= 3.3.7) 53 | middleman-sprockets (>= 3.1.2) 54 | sass (>= 3.4.0, < 4.0) 55 | uglifier (~> 2.5) 56 | middleman-core (3.3.7) 57 | activesupport (~> 4.1.0) 58 | bundler (~> 1.1) 59 | erubis 60 | hooks (~> 0.3) 61 | i18n (~> 0.6.9) 62 | listen (>= 2.7.9, < 3.0) 63 | padrino-helpers (~> 0.12.3) 64 | rack (>= 1.4.5, < 2.0) 65 | rack-test (~> 0.6.2) 66 | thor (>= 0.15.2, < 2.0) 67 | tilt (~> 1.4.1, < 2.0) 68 | middleman-sprockets (3.4.2) 69 | middleman-core (>= 3.3) 70 | sprockets (~> 2.12.1) 71 | sprockets-helpers (~> 1.1.0) 72 | sprockets-sass (~> 1.3.0) 73 | middleman-syntax (2.0.0) 74 | middleman-core (~> 3.2) 75 | rouge (~> 1.0) 76 | middleman-toc (0.0.7) 77 | titleize 78 | minitest (5.9.0) 79 | multi_json (1.12.1) 80 | padrino-helpers (0.12.8) 81 | i18n (~> 0.6, >= 0.6.7) 82 | padrino-support (= 0.12.8) 83 | tilt (~> 1.4.1) 84 | padrino-support (0.12.8) 85 | activesupport (>= 3.1) 86 | rack (1.6.4) 87 | rack-test (0.6.3) 88 | rack (>= 1.0) 89 | rb-fsevent (0.9.7) 90 | rb-inotify (0.9.7) 91 | ffi (>= 0.5.0) 92 | redcarpet (3.3.4) 93 | rouge (1.11.1) 94 | sass (3.4.22) 95 | sprockets (2.12.4) 96 | hike (~> 1.2) 97 | multi_json (~> 1.0) 98 | rack (~> 1.0) 99 | tilt (~> 1.1, != 1.3.0) 100 | sprockets-helpers (1.1.0) 101 | sprockets (~> 2.0) 102 | sprockets-sass (1.3.1) 103 | sprockets (~> 2.0) 104 | tilt (~> 1.1) 105 | thor (0.19.1) 106 | thread_safe (0.3.5) 107 | tilt (1.4.1) 108 | timers (4.0.4) 109 | hitimes 110 | titleize (1.4.0) 111 | tzinfo (1.2.2) 112 | thread_safe (~> 0.1) 113 | uber (0.0.15) 114 | uglifier (2.7.2) 115 | execjs (>= 0.3.0) 116 | json (>= 1.8.0) 117 | 118 | PLATFORMS 119 | ruby 120 | 121 | DEPENDENCIES 122 | middleman (= 3.3.7) 123 | middleman-syntax (= 2.0.0) 124 | middleman-toc 125 | redcarpet 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testing For Beginners 2 | 3 | *Ruby Monday Study Group curriculum for advanced intermediates* 4 | 5 | You can [read it online here](http://testing-for-beginners.rubymonstas.org). 6 | 7 | This book is built using [middleman](http://middlemanapp.com). 8 | 9 | The source code is kept on the main branch. The `build` directory is 10 | `.gitignore`d and should be initialized with another git repository so it can 11 | be pushed to the `gh-pages` branch in order to deploy changes to the site. 12 | 13 | After making changes, once you've committed them, you can publish your 14 | changes to the website using: 15 | 16 | ``` 17 | ./bin/publish 18 | ``` 19 | 20 | License: Creative Commons Attribution-ShareAlike 21 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubymonsters/testing-for-beginners/51eebe6d7adb35510aeb18b73e4d5a680529fa67/TODO.md -------------------------------------------------------------------------------- /bin/publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | git="git --git-dir=./build/.git --work-tree=./build" 6 | 7 | function run() { 8 | cmd=$@ 9 | echo $cmd 10 | eval "$cmd" 11 | } 12 | 13 | function setup() { 14 | run "mkdir -p build" 15 | run "$git init" 16 | run "$git remote add origin git@github.com:rubymonsters/testing-for-beginners.git" 17 | run "$git fetch" 18 | # run "$git reset --hard origin/gh-pages" 19 | } 20 | 21 | function commit() { 22 | local msg=$1 23 | run "git add -A ." 24 | run "git commit -am '$msg'" 25 | run "git push" 26 | } 27 | 28 | function compile() { 29 | run "bundle exec middleman build --verbose" 30 | } 31 | 32 | function push() { 33 | run "$git add -A ." 34 | run "$git commit -am 'build'" 35 | run "$git push -f origin HEAD:gh-pages" 36 | } 37 | 38 | if [ ! -d build/.git ]; then 39 | setup 40 | fi 41 | 42 | if [ -n "$1" ]; then 43 | commit "$1" 44 | fi 45 | 46 | compile 47 | push 48 | -------------------------------------------------------------------------------- /code/sinatra/app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra/base" 2 | require "member" 3 | require "member_validator" 4 | 5 | class App < Sinatra::Base 6 | FILENAME = "members.txt" 7 | 8 | use Rack::MethodOverride 9 | 10 | enable :sessions 11 | 12 | before do 13 | @message = session.delete(:message) 14 | end 15 | 16 | get "/members" do 17 | @members = members 18 | erb :index 19 | end 20 | 21 | get "/members/new" do 22 | @member = Member.new 23 | erb :new 24 | end 25 | 26 | post "/members" do 27 | @member = Member.new(params[:name]) 28 | validator = MemberValidator.new(@member, members) 29 | 30 | if validator.valid? 31 | add_member(@member.name) 32 | session[:message] = "Successfully saved the new member: #{@member.name}." 33 | redirect "/members/#{@member.id}" 34 | else 35 | @messages = validator.messages 36 | erb :new 37 | end 38 | end 39 | 40 | get "/members/:id" do 41 | @member = find_member(params[:id]) 42 | erb :show 43 | end 44 | 45 | get "/members/:id/edit" do 46 | @member = find_member(params[:id]) 47 | erb :edit 48 | end 49 | 50 | put "/members/:id" do 51 | @member = find_member(params[:id]) 52 | @member.name = params[:name] 53 | validator = MemberValidator.new(@member, members) 54 | 55 | if validator.valid? 56 | update_member(params[:id], @member.name) 57 | session[:message] = "Successfully updated the member: #{@member.name}." 58 | redirect "/members/#{@member.id}" 59 | else 60 | @messages = validator.messages 61 | erb :new 62 | end 63 | end 64 | 65 | get "/members/:id/delete" do 66 | @member = find_member(params[:id]) 67 | erb :delete 68 | end 69 | 70 | delete "/members/:id" do 71 | member = remove_member(params[:id]) 72 | session[:message] = "Successfully removed the member: #{params[:id]}." 73 | redirect "/members" 74 | end 75 | 76 | def names 77 | return [] unless File.exists?(FILENAME) 78 | File.read(FILENAME).split("\n") 79 | end 80 | 81 | def members 82 | names.map { |name| Member.new(name) } 83 | end 84 | 85 | def find_member(id) 86 | members.detect { |member| member.id == id } 87 | end 88 | 89 | def add_member(name) 90 | File.write(FILENAME, name, mode: File::WRONLY | File::APPEND | File::CREAT) 91 | end 92 | 93 | def update_member(id, name) 94 | lines = names.dup 95 | lines[lines.index(id)] = name 96 | store(lines) 97 | end 98 | 99 | def store(lines) 100 | File.write(FILENAME, lines.join("\n"), mode: File::WRONLY | File::TRUNC | File::CREAT) 101 | end 102 | 103 | def remove_member(name) 104 | lines = names.reject { |other| name == other } 105 | store(lines) 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /code/sinatra/app_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | ENV['RACK_ENV'] = 'test' 4 | 5 | RSpec.configure do |config| 6 | config.mock_with :rspec 7 | end 8 | 9 | describe App do 10 | let(:app) { App.new } 11 | let(:filename) { "members.txt" } 12 | let(:content) { "Anja\nMaren\n" } 13 | 14 | it "displays the member's name" do 15 | expect(File).to receive(:read).with(filename).and_return(content) 16 | get "/members" 17 | end 18 | 19 | context "GET to /members" do 20 | let(:response) { get "/members" } 21 | 22 | it "returns status 200 OK" do 23 | expect(response.status).to eq 200 24 | end 25 | 26 | it "displays a list of member names that link to /members/:name" do 27 | expect(response.body).to have_tag(:a, :href => "/members/Anja", :text => "Anja") 28 | expect(response.body).to have_tag(:a, :href => "/members/Maren", :text => "Maren") 29 | end 30 | end 31 | 32 | context "GET to /members/:name" do 33 | let(:response) { get "/members/Anja" } 34 | 35 | it "returns status 200 OK" do 36 | expect(response.status).to eq 200 37 | end 38 | 39 | it "displays the member's name" do 40 | expect(response.body).to have_tag(:p, :text => "Name: Anja") 41 | end 42 | end 43 | 44 | context "GET to /members/new" do 45 | let(:response) { get "/members/new" } 46 | 47 | it "returns status 200 OK" do 48 | expect(response.status).to eq 200 49 | end 50 | 51 | it "displays a form that POSTs to /members" do 52 | expect(response.body).to have_tag(:form, :action => "/members", :method => "post") 53 | end 54 | 55 | it "displays an input tag for the name" do 56 | expect(response.body).to have_tag(:input, :type => "text", :name => "name") 57 | end 58 | 59 | it "displays a submit tag" do 60 | expect(response.body).to have_tag(:input, :type => "submit") 61 | end 62 | end 63 | 64 | context "POST to /members" do 65 | let(:file) { File.read("members.txt") } 66 | before { File.write("members.txt", "Anja\nMaren\n") } 67 | 68 | context "given a valid name" do 69 | let!(:response) { post "/members", :name => "Monsta" } 70 | 71 | it "adds the name to the members.txt file" do 72 | expect(file).to include("Monsta") 73 | end 74 | 75 | it "returns status 302 Found" do 76 | expect(response.status).to eq 302 77 | end 78 | 79 | it "redirects to /members/:name" do 80 | expect(response).to redirect_to "/members/Monsta" 81 | end 82 | end 83 | 84 | shared_examples_for "invalid member data" do 85 | let!(:response) { post "/members", :name => "Maren" } 86 | 87 | it "does not add the name to the members.txt file" do 88 | expect(file).to eq "Anja\nMaren\n" 89 | end 90 | 91 | it "returns status 200 OK" do 92 | expect(response.status).to eq 200 93 | end 94 | 95 | it "displays a form that POSTs to /members" do 96 | expect(response.body).to have_tag(:form, :action => "/members", :method => "post") 97 | end 98 | 99 | it "displays an input tag for the name, with the value set" do 100 | expect(response.body).to have_tag(:input, :type => "text", :name => "name", :value => "Maren") 101 | end 102 | end 103 | 104 | context "given a duplicate name" do 105 | include_examples "invalid member data" 106 | end 107 | 108 | context "given an empty name" do 109 | include_examples "invalid member data" 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /code/sinatra/config.ru: -------------------------------------------------------------------------------- 1 | require 'app' 2 | 3 | run App 4 | -------------------------------------------------------------------------------- /code/sinatra/feature_spec.rb: -------------------------------------------------------------------------------- 1 | require "app" 2 | require 'capybara/dsl' 3 | require 'capybara/poltergeist' 4 | 5 | Capybara.default_driver = :poltergeist 6 | Capybara.app = proc { |env| App.new.call(env) } 7 | 8 | RSpec.configure do |config| 9 | config.include Capybara::DSL 10 | config.before { File.write("members.txt", "Anja\nMaren\n") } 11 | end 12 | 13 | describe App do 14 | let(:links) { within('#members') { page.all('a').map(&:text) } } 15 | 16 | it "listing members" do 17 | # go to the members list 18 | visit "/members" 19 | 20 | # check the list of links 21 | expect(links).to eq ['Anja', 'Edit', 'Remove', 'Maren', 'Edit', 'Remove'] 22 | end 23 | 24 | it "showing member details" do 25 | # go to the members list 26 | visit "/members" 27 | 28 | # click on the link 29 | click_on "Maren" 30 | 31 | # check the h1 tag 32 | expect(page).to have_css 'h1', text: 'Member: Maren' 33 | 34 | # check the name 35 | expect(page).to have_content 'Name: Maren' 36 | end 37 | 38 | it "creating a new member" do 39 | # go to the members list 40 | visit "/members" 41 | 42 | # click on the link 43 | click_on "New Member" 44 | 45 | # fill in the form 46 | fill_in "Name", :with => "Monsta" 47 | 48 | # submit the form 49 | click_on "Save" 50 | 51 | # check the current path 52 | expect(page).to have_current_path "/members/Monsta" 53 | 54 | # check the message 55 | expect(page).to have_content 'Successfully saved the new member: Monsta.' 56 | 57 | # check the h1 tag 58 | expect(page).to have_css 'h1', text: 'Member: Monsta' 59 | end 60 | 61 | it "updating a member" do 62 | # go to the members list 63 | visit "/members" 64 | 65 | # click on the link for Anja 66 | click_on "Edit", match: :first 67 | 68 | # fill in the form 69 | fill_in "Name", :with => "Tyranja" 70 | 71 | # submit the form 72 | click_on "Save" 73 | 74 | # check the current path 75 | expect(page).to have_current_path "/members/Tyranja" 76 | 77 | # check the message 78 | expect(page).to have_content 'Successfully updated the member: Tyranja.' 79 | 80 | # check the h1 tag 81 | expect(page).to have_css 'h1', text: 'Member: Tyranja' 82 | end 83 | 84 | it "removing a member" do 85 | # go to the members list 86 | visit "/members" 87 | 88 | # click on the link for Anja 89 | click_on "Remove", match: :first 90 | 91 | # check the message 92 | expect(page).to have_content /Are you sure .*?/ 93 | 94 | # click the button 95 | click_on "Yes" 96 | 97 | # check the current path 98 | expect(page).to have_current_path "/members" 99 | 100 | # check the message 101 | expect(page).to have_content 'Successfully removed the member: Anja.' 102 | 103 | # check the list 104 | expect(page).to have_selector('li', count: 1) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /code/sinatra/member.rb: -------------------------------------------------------------------------------- 1 | class Member 2 | attr_accessor :name 3 | 4 | def initialize(name = nil) 5 | @name = name.to_s 6 | end 7 | 8 | def id 9 | name 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /code/sinatra/member_validator.rb: -------------------------------------------------------------------------------- 1 | class MemberValidator 2 | attr_reader :member, :members, :messages 3 | 4 | def initialize(member, members) 5 | @member = member 6 | @members = members 7 | @messages = [] 8 | end 9 | 10 | def valid? 11 | validate 12 | messages.empty? 13 | end 14 | 15 | private 16 | 17 | def names 18 | members.map { |member| member.name } 19 | end 20 | 21 | def validate 22 | if member.name.empty? 23 | messages << "You need to enter a name" 24 | elsif names.include?(member.name) 25 | messages << "#{member.name} is already included in our list." 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /code/sinatra/members.txt: -------------------------------------------------------------------------------- 1 | Anja 2 | Maren -------------------------------------------------------------------------------- /code/sinatra/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "app" 2 | require "rack/test" 3 | require "rspec-html-matchers" 4 | 5 | RSpec.configure do |config| 6 | config.include Rack::Test::Methods 7 | config.include RSpecHtmlMatchers 8 | end 9 | 10 | RSpec::Matchers.define(:redirect_to) do |url| 11 | match do |response| 12 | response.status == 302 && response.headers['Location'] == url 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /code/sinatra/views/delete.erb: -------------------------------------------------------------------------------- 1 |

2 | All Members | 3 | <%= @member.name %> 4 |

5 | 6 |

Removing Member: <%= @member.name %>

7 | 8 |

Are you sure you want to remove the member <%= @member.name %>?

9 | 10 |
11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /code/sinatra/views/edit.erb: -------------------------------------------------------------------------------- 1 |

2 | All Members | 3 | <%= @member.name %> 4 |

5 | 6 |

Update Member: <%= @member.name %>

7 | 8 |
9 | 10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /code/sinatra/views/index.erb: -------------------------------------------------------------------------------- 1 |

Members

2 | 3 |

New Member

4 | 5 | 14 | -------------------------------------------------------------------------------- /code/sinatra/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% if @message %> 4 |

<%= @message %>

5 | <% end %> 6 | <%= yield %> 7 | 8 | 9 | -------------------------------------------------------------------------------- /code/sinatra/views/new.erb: -------------------------------------------------------------------------------- 1 |

All Members

2 | 3 |

New Member

4 | 5 | <% if @messages %> 6 | 11 | <% end %> 12 | 13 |
14 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /code/sinatra/views/show.erb: -------------------------------------------------------------------------------- 1 |

All Members

2 | 3 |

Member: <%= @member.name %>

4 | 5 |

Name: <%= @member.name %>

6 | -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | require 'middleman_toc' 2 | 3 | set :layouts_dir, '/layouts' 4 | set :css_dir, 'assets/stylesheets' 5 | set :js_dir, 'assets/javascripts' 6 | set :images_dir, 'images' 7 | set :source, 'source' 8 | 9 | page '/sitemap.xml', layout: false 10 | page '/solutions/*', :layout => false 11 | 12 | ignore(/themes\/(?!#{data.book.theme.downcase}).*/) 13 | config.ignored_sitemap_matchers[:layout] = proc { |file| 14 | file.start_with?(File.join(config.source, 'layout.')) || file.start_with?(File.join(config.source, 'layouts/')) || !!(file =~ /themes\/.*\/layouts\//) 15 | } 16 | 17 | activate :syntax 18 | set :markdown_engine, :redcarpet 19 | set :markdown, :fenced_code_blocks => true, :smartypants => true, :no_intra_emphasis => true, :autolink => true, :strikethrough => true, :tables => true 20 | 21 | set :relative_links, true 22 | activate :relative_assets 23 | activate :minify_css 24 | activate :minify_javascript 25 | activate :asset_hash 26 | activate :toc 27 | 28 | helpers do 29 | def discover_page_title(page = current_page) 30 | if page.data.title 31 | return page.data.title # Frontmatter title 32 | elsif page.url == '/' 33 | return data.book.title 34 | elsif match = page.render(layout: false, no_images: true).match(/(.*?)<\/h1>/) 35 | return match[1] + ' | ' + data.book.title 36 | else 37 | filename = page.url.split(/\//).last.gsub('%20', ' ').titleize 38 | return filename.chomp(File.extname(filename)) + ' | ' + data.book.title 39 | end 40 | end 41 | 42 | def link_to_if_exists(*args, &block) 43 | url = args[0] 44 | 45 | resource = sitemap.find_resource_by_path(url) 46 | if resource.nil? 47 | block.call 48 | else 49 | link_to(*args, &block) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | run Middleman.server 2 | -------------------------------------------------------------------------------- /data/book.yml: -------------------------------------------------------------------------------- 1 | --- 2 | title: Testing for Beginners 3 | author: Ruby Monstas 4 | github_url: https://github.com/ruby-monsters/testing_for_beginners 5 | domain: http://testing-for-beginners.rubymonstas.org 6 | license_name: Attribution-ShareAlike 7 | license_url: https://creativecommons.org/licenses/by-sa/4.0 8 | theme: monstas 9 | -------------------------------------------------------------------------------- /data/toc.yml: -------------------------------------------------------------------------------- 1 | - index 2 | - preface 3 | - path: testing 4 | children: 5 | - output 6 | - separation 7 | - computed 8 | - assert 9 | - stages 10 | - classes 11 | - libraries 12 | - path: minitest 13 | children: 14 | - path: rspec 15 | children: 16 | - basics 17 | - matchers 18 | - format 19 | - advanced 20 | - custom_matchers 21 | - filtering 22 | - path: rack_test 23 | children: 24 | - rack 25 | - sinatra 26 | - path: headless 27 | children: 28 | - phantomjs 29 | - capybara 30 | - features 31 | - path: test_doubles 32 | - path: analysis 33 | - path: services 34 | 35 | -------------------------------------------------------------------------------- /source/01-preface.md: -------------------------------------------------------------------------------- 1 | # Preface 2 | 3 | Testing, in software development, is a very broad field, especially if you look 4 | at all of its history, too. 5 | 6 | Today, in the field of developing modern web applications, it is a set of 7 | practices that helps writing better and less error prone software, and helps 8 | you be confident that your (or others') changes won't break your application. 9 | 10 | Still, even today, if you ask 10 different programmers how to write good tests 11 | you'll probably get 10 varying answers. However, there are a couple things that 12 | they'll all have in common, too, and we'll try to explore some of the answers 13 | in this book. 14 | -------------------------------------------------------------------------------- /source/02-testing.md: -------------------------------------------------------------------------------- 1 | # Testing Code 2 | 3 | Your code is supposed to function in a certain way. You expect that, whenever 4 | used, when you input certain bits of data to it, you'll get a certain, 5 | expected, output. 6 | 7 | Tests are extra code that you write alongside your code. You use this extra code 8 | to exercise, and test, your actual code, i.e. the code you really care about. 9 | So you could also say that tests are meta code: code whose sole purpose is to 10 | test other code. 11 | 12 | That sounds more complicated than it actually is. 13 | 14 | We'll expore the basic concepts of testing by writing very simple tests first. 15 | Later we'll look at libraries and services that make writing such tests more 16 | easy and effective. 17 | 18 | -------------------------------------------------------------------------------- /source/02-testing/01-output.md: -------------------------------------------------------------------------------- 1 | # Testing via output 2 | 3 | If you think back to the exercise to define a method `leap_year?` in the 4 | [Ruby for Beginners](http://ruby-for-beginners.rubymonstas.org/exercises/methods_1.html) 5 | book, a leap year is [defined](https://en.wikipedia.org/wiki/Leap_year#Algorithm) 6 | in pseudo code like this: 7 | 8 | ``` 9 | if (year is not divisible by 4) then (it is a common year) 10 | else if (year is not divisible by 100) then (it is a leap year) 11 | else if (year is not divisible by 400) then (it is a common year) 12 | else (it is a leap year) 13 | ``` 14 | 15 | Ok. You've implemented this method something like this: 16 | 17 | ```ruby 18 | def leap_year?(year) 19 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 20 | end 21 | ``` 22 | 23 | Now, how do you make sure the method does exactly what it is supposed to do? 24 | 25 | While working through the exercises you've usually added code at the end of the 26 | file that somehow exercised the methods written, and output results to the 27 | terminal. You've then run the file, and inspected the terminal to see what the 28 | result was. 29 | 30 | So maybe you've had something along the lines of: 31 | 32 | ```ruby 33 | def leap_year?(year) 34 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 35 | end 36 | 37 | puts "2001: #{leap_year?(2001)}" 38 | puts "1900: #{leap_year?(1900)}" 39 | puts "2000: #{leap_year?(2000)}" 40 | puts "2004: #{leap_year?(2004)}" 41 | ``` 42 | 43 | And then you've run the code to see that the output actually is the expected 44 | one: 45 | 46 | ``` 47 | 2001: false 48 | 1900: false 49 | 2000: true 50 | 2004: true 51 | ``` 52 | 53 | This works well enough for exercises. However, if you write a bigger program 54 | you don't really want all this output everytime the files are loaded, e.g. via 55 | `require`. 56 | 57 | Essentially, you'd want to separate your test code from the actual code, so 58 | that tests are only run when you actually want them to run. 59 | 60 | So what to do about that? 61 | 62 | 63 | -------------------------------------------------------------------------------- /source/02-testing/02-separation.md: -------------------------------------------------------------------------------- 1 | # Separating test code 2 | 3 | Here's one trick that has been used a lot in the early days of Ruby development: 4 | 5 | ```ruby 6 | def leap_year?(year) 7 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 8 | end 9 | 10 | if $0 == __FILE__ 11 | puts "2001: #{leap_year?(2001)}" 12 | puts "1900: #{leap_year?(1900)}" 13 | puts "2000: #{leap_year?(2000)}" 14 | puts "2004: #{leap_year?(2004)}" 15 | end 16 | ``` 17 | 18 | If you store this code in a file `leap_year.rb` and execute it with `ruby 19 | leap_year.rb` then you'll get the same output as above. 20 | 21 | However, if you write a bigger program which requires this file by `require 22 | "leap_year"` (so it can include your method definition, and use it somewhere 23 | else) then you would not get this output. 24 | 25 | You can try this out quickly on the command line: 26 | 27 | ``` 28 | $ ruby -I . -r leap_year.rb -e "p leap_year?(1996)" 29 | true 30 | ``` 31 | 32 | The flag `-r` tells Ruby to require your file. The flag `-I .` tells it to look 33 | in the current directory to load the file. The flag `-e` tells it to execute 34 | the given code. This way you don't have to create a new file in order to try this out. 35 | 36 | As you can see it now won't execute your "test", and thus won't output `2004: 37 | true` again. 38 | 39 | That is cool. We've just separated our test code from the implementation, i.e. 40 | we can run the tests separately, if we want to. In turn it won't run the test 41 | code when we just `require` the file, so we can use the method for something 42 | else. 43 | 44 | How does this work though? 45 | 46 | The variables `$0` and `__FILE__` are rather arcane, and they were inspired by 47 | other languages that existed when Matz designed Ruby in the 90s, especially 48 | Perl, in this case. 49 | 50 | The variable `$0` is a global variable (hence the dollar sign `$`) that holds 51 | the name of the Ruby file that was given on the command line. So if you run 52 | `ruby leap_year.rb`, then `$0` will contain `"leap_year.rb"` 53 | 54 | The variable `__FILE__` on the other hand is defined in every Ruby file, and 55 | contains the file name of this exact file. If we execute `ruby leap_year.rb` 56 | then these two names will be the same. If we execute any other ruby code that 57 | requires the file `leap_year.rb` though, then they will not be the same. 58 | 59 | We can further improve our test code by making it less repetitive, and abstract 60 | it: 61 | 62 | ```ruby 63 | def leap_year?(year) 64 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 65 | end 66 | 67 | if $0 == __FILE__ 68 | [2001, 1900, 2000, 2004].each do |year| 69 | puts "#{year}: #{leap_year?(year)}" 70 | end 71 | end 72 | ``` 73 | 74 | Exercise: Try going to back to the [Ruby for Beginners](http://ruby-for-beginners.rubymonstas.org/) 75 | book and add some tests to some of the exercises you did. 76 | -------------------------------------------------------------------------------- /source/02-testing/03-computed.md: -------------------------------------------------------------------------------- 1 | # Computed tests 2 | 3 | What if you have lots and lots of methods in lots of classes, and you want to 4 | make changes to them? You'd need to output a lot of things, and inspect them 5 | very carefully, in order not to miss any mistakes. 6 | 7 | In the example above you'd have to remember that your code is valid if it 8 | outputs `false` for the first two years, and `true` for the last two ones. 9 | That's a lot of knowledge to keep in mind for just one method. Imagine you'd 10 | have hundreds of methods. You'd need to very carefully inspect a lot of output. 11 | 12 | Isn't that what computers are there for? Doing all the tedious, mechanical work 13 | for us that requires a lot of precision? 14 | 15 | Let's see. What if we, instead of outputting plain values to the terminal, also 16 | output a hint if this is the value that we expected to see? 17 | 18 | ```ruby 19 | def leap_year?(year) 20 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 21 | end 22 | 23 | if $0 == __FILE__ 24 | data = { 25 | 2001 => false, 26 | 1900 => false, 27 | 2000 => true, 28 | 2004 => true 29 | } 30 | 31 | data.each do |year, expected| 32 | actual = leap_year?(year) 33 | if expected == actual 34 | puts "leap_year?(#{year}) returned #{actual} as expected." 35 | else 36 | puts "KAPUTT! leap_year?(#{year}) did not return #{expected} as expected, but actually returned #{actual}." 37 | end 38 | end 39 | end 40 | ``` 41 | 42 | This will output: 43 | 44 | ``` 45 | leap_year?(2001) returned false as expected. 46 | leap_year?(1900) returned false as expected. 47 | leap_year?(2000) returned true as expected. 48 | leap_year?(2004) returned true as expected. 49 | ``` 50 | 51 | Let's try breaking our method by always returning `true`: 52 | 53 | ```ruby 54 | def leap_year?(year) 55 | true 56 | end 57 | ``` 58 | 59 | We'll then get: 60 | 61 | ``` 62 | KAPUTT! leap_year?(2001) did not return false as expected, but actually returned true. 63 | KAPUTT! leap_year?(1900) did not return false as expected, but actually returned true. 64 | leap_year?(2000) returned true as expected. 65 | leap_year?(2004) returned true as expected. 66 | ``` 67 | 68 | That's much better, isn't it? Even if you'd have hundreds of tests (many 69 | real-world applications do have thousands) it would be pretty easy to spot 70 | any broken behavior, right? 71 | -------------------------------------------------------------------------------- /source/02-testing/04-assert.md: -------------------------------------------------------------------------------- 1 | # Assertions 2 | 3 | Testing libraries provide a lot of tools to make writing tests easier, and we'll look 4 | at two of the most common ones in a bit. 5 | 6 | In order to get even closer to what real testing libraries look like we could 7 | extract a method `assert_equal`, like so: 8 | 9 | ```ruby 10 | def leap_year?(year) 11 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 12 | end 13 | 14 | if $0 == __FILE__ 15 | def assert_equal(expected, actual, method) 16 | if expected == actual 17 | puts "#{method} returned #{actual} as expected." 18 | else 19 | puts "KAPUTT! #{method} did not return #{expected} as expected, but actually returned #{actual}." 20 | end 21 | end 22 | 23 | data = { 24 | 2001 => false, 25 | 1900 => false, 26 | 2000 => true, 27 | 2004 => true 28 | } 29 | 30 | data.each do |year, expected| 31 | actual = leap_year?(year) 32 | assert_equal(expected, actual, "leap_year?(#{year})") 33 | end 34 | end 35 | ``` 36 | 37 | Our method `assert_equal` outputs directly to the terminal, which, in our case, 38 | is good enough. 39 | 40 | This is pretty cool! 41 | 42 | With just plain Ruby we've written some useful tests that only will be executed 43 | if we want them to, and our actual test code now looks much more focussed. The 44 | method `assert_equal` could be defined somewhere else, in an external file, so 45 | we could reuse it in other places. 46 | 47 | You could imagine writing lots of methods, and adding tests to them so that, 48 | whenever you or fellow developers change something about them, your tests would 49 | catch any mistakes. 50 | 51 | This is pretty close to what people did in the very early days of Ruby, and 52 | sometimes you can still find such code if you explore, for example, the 53 | Ruby standard library code from older Ruby versions, such as 1.8.x. 54 | -------------------------------------------------------------------------------- /source/02-testing/05-stages.md: -------------------------------------------------------------------------------- 1 | # Stages of a test 2 | 3 | There are three stages in most tests, and we want to introduce them early so 4 | you recognize them later. 5 | 6 | Let's assume you've stored the `leap_year?` method in a file `leap_year.rb`, 7 | and you have another file `user.rb` that looks like this: 8 | 9 | 10 | ```ruby 11 | require "leap_year" 12 | require "date" 13 | 14 | class User 15 | def initialize(name, birthday) 16 | @name = name 17 | @birthday = birthday 18 | end 19 | 20 | def born_in_leap_year? 21 | leap_year?(Date.parse(@birthday).year) 22 | end 23 | end 24 | 25 | if $0 == __FILE__ 26 | def assert_equal(expected, actual, method) 27 | if expected == actual 28 | puts "#{method} returned #{actual} as expected." 29 | else 30 | puts "KAPUTT! #{method} did not return #{expected} as expected, but actually returned #{actual}." 31 | end 32 | end 33 | 34 | data = { 35 | "2001-01-01" => false, 36 | "1900-01-01" => false, 37 | "2000-01-01" => true, 38 | "2004-01-01" => true 39 | } 40 | 41 | data.each do |date, expected| 42 | user = User.new("Jennifer", date) 43 | actual = user.born_in_leap_year? 44 | assert_equal(expected, actual, "born_in_leap_year? for a User born on #{date}") 45 | end 46 | end 47 | ``` 48 | 49 | As you can see our tests now have three stages: 50 | 51 | 1. We first set up an object that we want to test with a certain birthday: `User.new("Jennifer", date)`. 52 | 2. We then call the method we're interested in: `user.born_in_leap_year?`. 53 | 3. And finally we assert that the result actually is the expected result. 54 | 55 | These three stages often can be found in tests: 56 | 57 | 1. Setup 58 | 2. Execution 59 | 3. Assertion 60 | 61 | In a web application, for example, the setup stage could mean that we store 62 | certain data in the database. In the execution stage we then make a request to 63 | the application. E.g. we'd `GET` a list, or we'd `POST` a new entry. In the 64 | assertion stage we'd then assert (make sure) that we get the expected result. 65 | E.g. if we've used `GET` we'd inspect the returned HTML to see if the expected 66 | entries are listed. Or if we've used `POST` to create a new entry we might look 67 | at the database to see if the record actually has been created. 68 | -------------------------------------------------------------------------------- /source/02-testing/06-classes.md: -------------------------------------------------------------------------------- 1 | # Test Classes 2 | 3 | *Write your own little testing library* 4 | 5 | Ruby is an object-oriented programming language. So testing libraries often allow you 6 | to implement your tests in the form of classes. 7 | 8 | Let's write our own simple testing library. 9 | 10 | Suppose we've stored the `leap_year?` method in a file `leap_year.rb`, and the 11 | `User` class in a file `user.rb`. 12 | 13 | We'd want the following code to work: 14 | 15 | 16 | ```ruby 17 | require "date" 18 | require "leap_year" 19 | require "test" 20 | 21 | class User 22 | def initialize(name, birthday) 23 | @name = name 24 | @birthday = birthday 25 | end 26 | 27 | def born_in_leap_year? 28 | leap_year?(Date.parse(@birthday).year) 29 | end 30 | end 31 | 32 | if $0 == __FILE__ 33 | class UserTest < Test 34 | def test_not_born_in_leap_year_when_born_in_2001 35 | user = User.new("Jennifer", "2001-01-01") 36 | assert_false(user.born_in_leap_year?) 37 | end 38 | 39 | def test_not_born_in_leap_year_when_born_in_1900 40 | user = User.new("Jennifer", "1900-01-01") 41 | assert_false(user.born_in_leap_year?) 42 | end 43 | 44 | def test_born_in_leap_year_when_born_in_2000 45 | user = User.new("Jennifer", "2000-01-01") 46 | assert_true(user.born_in_leap_year?) 47 | end 48 | 49 | def test_born_in_leap_year_when_born_in_2004 50 | user = User.new("Jennifer", "2004-01-01") 51 | assert_true(user.born_in_leap_year?) 52 | end 53 | end 54 | 55 | test = UserTest.new 56 | test.run 57 | end 58 | ``` 59 | 60 | This is pretty much exactly how almost all Ruby tests looked like, maybe, 10 61 | years ago. And it is still the style preferred by quite some Ruby developers. 62 | 63 | The idea here is to represent each test with a method on a class. The method 64 | should be as descriptive and readable as possible, and focus on the semantics, 65 | instead of the implementation (i.e. what we test, not how we test). 66 | 67 | However, this code will break, because the class `Test` does not exist. Also, 68 | if you've followed our curriculum you'll spot a new thing here: 69 | 70 | ``` 71 | class UserTest < Test 72 | ``` 73 | 74 | What's that? 75 | 76 | The `<` operator used here refers to a concept called "inheritance". It says: 77 | *Define a new class `UserTest` and inherit all the methods from the class `Test`.* 78 | In other words `UserTest` *is* a `Test`, but it also adds some extra stuff to it. 79 | 80 | We can define the class `Test` like so, and store it to a file `test.rb`: 81 | 82 | ```ruby 83 | class Test 84 | def run 85 | tests = methods.select { |method| method.to_s.start_with?("test_") } 86 | tests.each { |test| send(test) } 87 | end 88 | 89 | def assert_true(actual) 90 | assert_equal(true, actual) 91 | end 92 | 93 | def assert_false(actual) 94 | assert_equal(false, actual) 95 | end 96 | 97 | def assert_equal(expected, actual) 98 | if expected == actual 99 | puts "#{actual} is #{expected} as expected." 100 | else 101 | puts "KAPUTT! #{actual} is not #{expected} as expected." 102 | end 103 | end 104 | end 105 | ``` 106 | 107 | Whoa. That's a bunch of new stuff. If you don't grasp all of this don't worry, 108 | it's certainly a level of Ruby knowledge you don't actually need this often. 109 | 110 | Let's walk through it though: 111 | 112 | * Our class `UserTest` inherits all the methods from the class `Test`. So we 113 | can call the method `run` on our instance (as in `test.run`, from above). 114 | 115 | * The method `run` looks at all the `methods` defined on this object, and selects 116 | the method names that start with the string `test_`. The `Test` class does not 117 | have any such methods. So these must be the methods that we've defined on the 118 | class `UserTest`. 119 | 120 | * It then, for each of these method names, calls `send` with the given method name. 121 | `send` calls this exact method on the object itself. That's right. `send` is 122 | an abstract way of calling a method: You hand it the method name you want to 123 | call, and it calls that method for you. 124 | 125 | * So we call all the methods `test_not_born_in_leap_year_when_born_in_2001`, 126 | `test_not_born_in_leap_year_when_born_in_1900`, and so on. 127 | 128 | * Now these methods set up a `User` object with the birthday we care about, and 129 | then call `assert_false` or `assert_true` with the actual that value the method 130 | `born_in_leap_year?` returned. 131 | 132 | * The methods `assert_false` and `assert_true` just call `assert_equal`, passing 133 | the expected value (`true` or `false`), and the actual value they received from 134 | the test method. 135 | 136 | Pretty cool. 137 | 138 | ## Adding the test method name 139 | 140 | However, we're now missing some important information. If you try breaking the 141 | first test by changing `2001` to `2000` in the birthday date (not the method 142 | name), and run the output you'll see: 143 | 144 | ``` 145 | $ ruby -I . user.rb 146 | KAPUTT! true is not false as expected. 147 | false is false as expected. 148 | true is true as expected. 149 | true is true as expected. 150 | ``` 151 | 152 | Umm. We've lost the ability to easily identify which one of the test methods 153 | broke. If we have a few hundred tests then counting them to figure out the 154 | right one is not a cool option. 155 | 156 | So how can we fix that? 157 | 158 | We've previously passed in an identifier to `assert_equal` by calling 159 | something like `assert_equal(expected, actual, "born_in_leap_year? for a User born on #{date}")`. 160 | 161 | However, that requires us to type a lot of code every time we want to call any 162 | of our assertion methods. 163 | 164 | Luckily, Ruby allows us to grab the so-called backtrace at any point in our 165 | code. The backtrace is the funny-looking stuff that you see on any error message 166 | in the console. It is an array of strings that tell which methods in which 167 | files and on which lines have been called so far, so we can "trace" the method 168 | call back. 169 | 170 | The method that lets us grab this backtrace is the method `caller`. Let's try 171 | adding this line at the very top of our method `assert_equal`: 172 | 173 | ``` 174 | puts caller 175 | ``` 176 | 177 | When you run this code you'll see the backtrace printed, something like the 178 | following: 179 | 180 | ``` 181 | $ ruby -I . user.rb 182 | test.rb:16:in `assert_false' 183 | user.rb:25:in `test_not_born_in_leap_year_when_born_in_2001' 184 | test.rb:8:in `run_test' 185 | test.rb:4:in `block in run' 186 | test.rb:4:in `each' 187 | test.rb:4:in `run' 188 | user.rb:40:in `
' 189 | ``` 190 | 191 | Ok, great! 192 | 193 | So the backtrace that Ruby returns to us when we call `caller` includes the 194 | method name that we are after: the test method that has, at some point in the 195 | past, called the current method `assert_equal`. 196 | 197 | All we have to do is filter this array for a line that includes `test_`, and 198 | then extract the method name from that line: 199 | 200 | 201 | ```ruby 202 | class Test 203 | # ... 204 | 205 | def assert_equal(expected, actual) 206 | line = caller.detect { |line| line.include?("test_") } 207 | method = line =~ /(test_.*)'/ && $1 208 | if expected == actual 209 | puts "#{method} #{actual} is #{expected} as expected." 210 | else 211 | puts "KAPUTT! #{method} #{actual} is not #{expected} as expected." 212 | end 213 | end 214 | end 215 | ``` 216 | 217 | If the code `line =~ /(test_.*)'/ && $1` on the second line looks confusing to 218 | you, this is a regular expression that grabs the method name from the line in 219 | the backtrace. The expression says: 220 | 221 | "Find a string that starts with `test_`, and then include all characters until 222 | you find a single quote `'`. Grab all these characters including `test_`, but 223 | do not include the single quote." 224 | 225 | Ruby's special variable `$1` will then include the characters grabbed by the 226 | regular expression. (The correct term would be "captured". This is everything 227 | between the round parentheses inside the regular expression). 228 | 229 | And with this change we've got our method names back, even though we didn't 230 | have to pass them to our assertion methods in each of our tests: 231 | 232 | ``` 233 | KAPUTT! test_not_born_in_leap_year_when_born_in_2001 true is not false as expected. 234 | test_not_born_in_leap_year_when_born_in_1900 false is false as expected. 235 | test_born_in_leap_year_when_born_in_2000 true is true as expected. 236 | test_born_in_leap_year_when_born_in_2004 true is true as expected. 237 | ``` 238 | 239 | Testing libraries come, essentially, with code like this. 240 | 241 | They define classes and methods that make it easy for you to, as much as 242 | possible, focus on what you want to test, and not to bother with the question 243 | how to write these tests. 244 | 245 | ## Autorun 246 | 247 | Let's make one more tiny improvement, similar to what such testing libraries do: 248 | 249 | Let's try to remove the two extra lines for instantiating our test class and 250 | calling `run` on it: 251 | 252 | ```ruby 253 | test = UserTest.new 254 | test.run 255 | ``` 256 | 257 | We've just defined a test class and, in this context, we can be fairly certain 258 | that we want to run these tests, right? So not having to type these lines would 259 | be kinda useful. Ruby could just automatically create an instance of the class 260 | and call `run` on it whenever we define a class that inherits from `Test`. 261 | 262 | How can we do that? 263 | 264 | First of all we'd want a way to find out all subclasses that have inherited from 265 | the class `Test`. Rails adds a way to do that with the [method `subclasses`](http://apidock.com/rails/v3.2.13/Class/subclasses). 266 | 267 | Not using Rails though we need to add this ourselves: 268 | 269 | ```ruby 270 | class Test 271 | class << self 272 | def inherited(subclass) 273 | subclasses << subclass 274 | end 275 | 276 | def subclasses 277 | @subclasses ||= [] 278 | end 279 | end 280 | 281 | # ... 282 | end 283 | ``` 284 | 285 | The method `inherited` is called by Ruby every time the class is inherited, passing 286 | the inheriting class (i.e. in our case the class `UserTest`). We keep track of all 287 | these classes in the array that is stored on the instance variable `@subclasses`. 288 | 289 | Now, how can we automatically run these tests? 290 | 291 | There's another little trick that is so rarely used in day-to-day programming that 292 | many Ruby programmers don't even know about it. You can tell Ruby to execute code 293 | before it exits (i.e. terminates the program). And this is exactly what we want 294 | to do, isn't it? 295 | 296 | Here's how: 297 | 298 | ```ruby 299 | at_exit do 300 | Test.subclasses.each do |subclass| 301 | test = subclass.new 302 | test.run 303 | end 304 | end 305 | ``` 306 | 307 | The method `at_exit` takes a block that is called an "exit hook". I.e. we tell 308 | Ruby to execute the block that we've hooked up right before Ruby terminates the 309 | program, and "exits". 310 | 311 | In this block we take each of the subclasses of the class `Test` (in our case 312 | that is going to be just one class, the class `UserTest`), instantiate it, 313 | and call `run` on the instance. 314 | 315 | Pretty neat. 316 | 317 | We've essentially implemented a really small, but actually useful testing 318 | library ourselves, with just 45 lines of Ruby. 319 | 320 | Let's look at some real world testing libraries next. 321 | 322 | -------------------------------------------------------------------------------- /source/03-libraries.md: -------------------------------------------------------------------------------- 1 | # Libraries 2 | 3 | As mentioned, there are several libraries that make testing in Ruby much 4 | easier and more pleasant. Some of them are rather simple, and very fast. 5 | Others are very powerful, and come with lots of useful features. 6 | 7 | We'll look at the two most commonly used general purpose testing libraries: 8 | MiniTest and RSpec. 9 | 10 | Later we will then have a look at some libraries that have more specific 11 | purposes and help with testing in certain contexts. 12 | 13 | -------------------------------------------------------------------------------- /source/04-minitest.md: -------------------------------------------------------------------------------- 1 | # Minitest 2 | 3 | [Minitest](https://github.com/seattlerb/minitest) is a library that has been 4 | developed by the (some might say, infamous) Seattle Ruby community. 5 | 6 | It has replaced the much older, and much more clunky, original `test/unit`, a 7 | library that used to be included in Ruby's standard library. Nowadays, Ruby 8 | ships with the more modern, and more extensible, Minitest, so you can simply 9 | require it, and you're good to go — you can start writing tests. 10 | 11 | Minitest works much like our little `Test` library. Here's an example taken 12 | straight from the project's 13 | [README](https://github.com/seattlerb/minitest#synopsis), I've only shortened it 14 | a bit. 15 | 16 | Given that you'd like to test the following class: 17 | 18 | ```ruby 19 | class Meme 20 | def i_can_has_cheezburger? 21 | "OHAI!" 22 | end 23 | end 24 | ``` 25 | 26 | Define your tests as methods beginning with `test_`: 27 | 28 | ```ruby 29 | require "minitest/autorun" 30 | 31 | class TestMeme < Minitest::Test 32 | def setup 33 | @meme = Meme.new 34 | end 35 | 36 | def test_that_kitty_can_eat 37 | assert_equal "OHAI!", @meme.i_can_has_cheezburger? 38 | end 39 | 40 | def test_that_will_be_skipped 41 | skip "test this later" 42 | end 43 | end 44 | ``` 45 | 46 | As you can see there's a method called `setup`. This method will be called before each 47 | of the test methods. This makes sense if you think about the [stages](/testing/stages.html) 48 | that tests usually include: you want setup to be run first, before each of the 49 | tests. 50 | 51 | Check out their documentation on what [assertions](http://docs.seattlerb.org/minitest/Minitest/Assertions.html) 52 | are defined. There are `assert`, and `assert_equal`, much like the methods 53 | that we've defined before. But there also are a lot more useful methods, and 54 | most of them come with a counterpart method `refute` (fail if truthy, while 55 | `assert` fails if falsy). 56 | 57 | Try to translate some of our manual tests in the chapter [testing](/testing.html) 58 | to Minitest. 59 | 60 | In order to do so create a file that has your code (e.g. the method `leap_year?`), 61 | and then defines a class, e.g. `LeapYearTest`, that inherits from `Minitest::Test`. 62 | You'll also want to `require "minitest/autorun"` at the very top of that file. 63 | 64 | Also consider finding other code in the [Ruby for Beginners](http://ruby-for-beginners.rubymonstas.org/) 65 | book that looks like it should be tested, and try writing some tests for it. 66 | 67 | -------------------------------------------------------------------------------- /source/05-rspec.md: -------------------------------------------------------------------------------- 1 | # RSpec 2 | 3 | [RSpec](http://rspec.info/) is a library that has caused quite a bit of debate 4 | in the Ruby community over the last years. 5 | 6 | Some people really love it and use nothing else. It is very powerful, and includes 7 | some very unique features. Others find it way too big, and too complicated for 8 | something as simple as writing some tests. 9 | 10 | RSpec is a [DSL](http://webapps-for-beginners.rubymonstas.org/sinatra/dsl.html) for 11 | writing tests. In other words, it tries to make tests as readable as possible. Not 12 | only that, it also produces output that reads like a story, or spec ("specification"), 13 | hence the name. 14 | -------------------------------------------------------------------------------- /source/05-rspec/01-basics.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | RSpec tests can be written in several flavors (or "styles"). Let's have a look at 4 | the most basic one first. 5 | 6 | RSpec wants us to define tests in a file that ends with `_spec.rb`, so we store 7 | both our class and our test in the file `user_spec.rb`. Normally, in modern 8 | code bases, you'd store your code in one file, and your tests in another file: 9 | 10 | ```ruby 11 | require "date" 12 | 13 | def leap_year?(year) 14 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 15 | end 16 | 17 | class User 18 | def initialize(name, birthday) 19 | @name = name 20 | @birthday = birthday 21 | end 22 | 23 | def name 24 | @name 25 | end 26 | 27 | def born_in_leap_year? 28 | leap_year?(Date.parse(@birthday).year) 29 | end 30 | end 31 | 32 | describe User do 33 | it "is born in a leap year when born in 2000" do 34 | user = User.new("Francisca", "2000-01-01") 35 | actual = user.born_in_leap_year? 36 | expected = true 37 | expect(actual).to eq expected 38 | end 39 | end 40 | ``` 41 | 42 | Does that read ok? 43 | 44 | We're going to work with this test more later, so let's shorten that a bit and 45 | use less space, by removing the `actual` and `expected` variables: 46 | 47 | 48 | ```ruby 49 | describe User do 50 | it "is born in a leap year when born in 2000" do 51 | user = User.new("Francisca", "2000-01-01") 52 | expect(user.born_in_leap_year?).to eq true 53 | end 54 | end 55 | ``` 56 | 57 | Ok. That's the same, but uses 2 lines instead of 4. 58 | 59 | Remember how Sinatra is a [DSL](http://webapps-for-beginners.rubymonstas.org/sinatra/dsl.html), 60 | a language, for "talking" about (writing code that deals with) the problem 61 | domain of HTTP, i.e. writing web applications? 62 | 63 | RSpec is a DSL for the problem domain of writing tests (or "specifications"). 64 | 65 | While Sinatra defines methods such as `get`, `post`, `status`, `redirect`, and 66 | so on, RSpec defines methods like `describe`, `it`, `expect`, and `eq` (equal). 67 | 68 | Using these methods we can describe our expectations about our code and 69 | execute them. In RSpec's thinking, that's what tests are all about: expressing 70 | our expectations about the behaviour of our code. We *describe* the class 71 | `User`, and specify our expectations. 72 | 73 | Instead of `it` you can also use `example`. That's exactly the same: 74 | 75 | ```ruby 76 | describe User do 77 | example "is born in a leap year when born in 2000" do 78 | # ... 79 | end 80 | end 81 | ``` 82 | 83 | Also, suppose we have many tests that deal with the case that a user was 84 | born in 2000, maybe like this: 85 | 86 | ```ruby 87 | describe User do 88 | it "is born in a leap year when born in 2000" do 89 | # ... 90 | end 91 | 92 | it "is at voting age when born in 2000" do 93 | # ... 94 | end 95 | end 96 | ``` 97 | 98 | RSpec allows us to group such tests (examples) like so: 99 | 100 | ```ruby 101 | describe User do 102 | describe "when born in 2000" do 103 | it "is born in a leap year" do 104 | # ... 105 | end 106 | 107 | example "is at voting age" do 108 | # ... 109 | end 110 | end 111 | end 112 | ``` 113 | 114 | And again, there's an alias for nested `describe` blocks — you can use `context` 115 | there, too: 116 | 117 | ```ruby 118 | describe User do 119 | context "when born in 2000" do 120 | it "is born in a leap year" do 121 | # ... 122 | end 123 | 124 | example "is at voting age" do 125 | # ... 126 | end 127 | end 128 | end 129 | ``` 130 | 131 | Nice, isn't it? Our spec says: "A user, in the context of being born in 2000, 132 | is born in a leap year", and then "[in the same context] is at voting age". 133 | 134 | In short, the methods `describe` and `context` are used to set up a logical 135 | structure for your tests. There needs to be at least one top level `describe` 136 | block. This is the equivalent to defining a class that inherits from 137 | `Minitest::Test`. 138 | 139 | The method `it` (or one of its alias `example` and `specify`) is then used to 140 | add the actual tests, i.e. that's the equivalent to defining methods that start 141 | with `test_` in Minitest. 142 | 143 | Under the hood RSpec uses a lot of [metaprogramming](http://rubylearning.com/blog/2010/11/23/dont-know-metaprogramming-in-ruby/); 144 | i.e. RSpec has methods that, when called, define code, classes and methods, 145 | according to the arguments you pass. For example the code `describe User do ... 146 | end` defines a class, and methods like `context`, and `it` add more code to 147 | this class. RSpec then, eventually, executes this code automatically, and runs 148 | your tests. 149 | 150 | That means, even though you're very familiar with Ruby, you'll still need to 151 | learn RSpec in order to use it effectively. That's one of the reasons why some 152 | Ruby developers dislike RSpec: It's not "just Ruby" any more. On the flip side, 153 | it's extremely powerful, and comes with features that no other testing library 154 | has. 155 | 156 | When you run the code in our `user_spec.rb` file, the output will look 157 | something like this: 158 | 159 | ``` 160 | $ rspec user_spec.rb 161 | . 162 | 163 | Finished in 0.00204 seconds (files took 0.1559 seconds to load) 164 | 1 example, 0 failures 165 | ``` 166 | 167 | The dot indicates that there is exactly one test defined. RSpec calls tests 168 | "examples". That's because they like to stress that tests shouldn't be so much 169 | about technical details, but about the behavior that the user cares about. 170 | They like to say that we "specify" behavior by the way of defining 171 | "examples". 172 | 173 | Let's break our test, and change the method `born_in_leap_year?` to always 174 | return `false`: 175 | 176 | ```ruby 177 | def born_in_leap_year? 178 | false 179 | end 180 | ``` 181 | 182 | When you now run the code again the output will look like this: 183 | 184 | ``` 185 | $ rspec user_spec.rb 186 | F 187 | 188 | Failures: 189 | 190 | 1) User born in 2000 is born in a leap year 191 | Failure/Error: expect(user.born_in_leap_year?).to eq true 192 | 193 | expected: true 194 | got: false 195 | 196 | (compared using ==) 197 | # ./user_spec.rb:25:in `block (3 levels) in ' 198 | 199 | Finished in 0.02033 seconds (files took 0.15813 seconds to load) 200 | 1 example, 1 failure 201 | 202 | Failed examples: 203 | 204 | rspec ./user_spec.rb:23 # User born in 2000 is born in a leap year 205 | ``` 206 | 207 | Wow, that's pretty comprehensive. RSpec tells us exactly what's going wrong, 208 | and where. So nice of them. 209 | -------------------------------------------------------------------------------- /source/05-rspec/02-matchers.md: -------------------------------------------------------------------------------- 1 | # Matchers 2 | 3 | We've discussed how methods such as `describe` and `context` are used to set 4 | up a structure for our tests, and how `it` adds an actual test ("example") to 5 | it. 6 | 7 | What about the implementation of the test, though? 8 | 9 | Let's look at our code again: 10 | 11 | ```ruby 12 | user = User.new("Francisca", "2000-01-01") 13 | expect(user.born_in_leap_year?).to eq true 14 | ``` 15 | 16 | What does `expect` do, exactly? And what's the deal with `to` and `eq` (equal)? 17 | 18 | These also are methods that RSpec defines for us, so we can describe our 19 | expectations in a readable way. That is to say, these methods are RSpec's equivalent to 20 | assertions (`assert` and friends) in Minitest. 21 | 22 | Some people feel they read much better than Minitest's assertion methods 23 | (`assert_equal(one, other)`), because they express more clearly what's the 24 | actual value, how to compare, and what's the expected value. Almost like 25 | an English sentence. 26 | 27 | Technically, `expect` returns an object that responds to the method `to`. This 28 | method `to` expects to be passed an object that is a so-called matcher. It will 29 | then call the matcher to see if it ... well, matches. 30 | 31 | Remember that in Ruby you can omit parentheses when calling a method (as long as this doesn't make your code ambiguous). So we 32 | could just as well add them: 33 | 34 | ```ruby 35 | expect(user.born_in_leap_year?).to(eq(true)) 36 | ``` 37 | 38 | Or we could make more visible what the matcher is (if we wanted): 39 | 40 | ```ruby 41 | match = eq(true) 42 | expect(user.born_in_leap_year?).to(match) 43 | ``` 44 | 45 | The method `eq` returns an RSpec matcher that simply tests if the object passed 46 | to `expect` is equal to the object passed to `eq`. This may sound more 47 | complicated than it is. 48 | 49 | If you have a look at the [documentation](https://relishapp.com/rspec/rspec-expectations/v/3-5/docs/built-in-matchers) 50 | there are lots and lots of matchers pre-defined, and RSpec makes it easy to 51 | define your own matchers, too (we'll get to that later). 52 | 53 | For example: 54 | 55 | ```ruby 56 | expect(10).to be > 5 57 | expect([1, 2, 3]).to include 1 58 | expect("Ruby Monstas").to start_with "Ruby" 59 | ``` 60 | 61 | No matter exactly how the code that implements these methods `expect`, `to`, 62 | and, for example, `eq` or `start_with` works: the purpose is to allow for 63 | code that kinda reads like an English sentence. You'll get used to these pretty soon, once 64 | you've started writing some RSpec tests. 65 | 66 | Just try to remember (or look it up here) that, essentially, you start with 67 | `expect(whatever_thing_to_test).to`, and then you find a matcher that works. 68 | `eq` always is a good start. So you end up with: 69 | 70 | ```ruby 71 | expect(whatever_thing_to_test).to eq whatever_you_expect 72 | ``` 73 | 74 | For example: 75 | 76 | ```ruby 77 | expect(your_object.some_method_to_test).to eq "the concrete value that you expect to be returned" 78 | ``` 79 | 80 | Does that make sense? 81 | 82 | Cool. Let's have a look at another badass feature RSpec comes with. 83 | 84 | ## Magic matchers 85 | 86 | RSpec also allows you to use matchers that depend on the methods defined on the 87 | object passed. 88 | 89 | Wait, what? 90 | 91 | Yeah. 92 | 93 | Here's a simple example: 94 | 95 | ```ruby 96 | expect(nil).to be_nil 97 | ``` 98 | 99 | The matcher `be_nil` expects the method `nil?` to be defined on the object 100 | under test, i.e. the object `nil`. As a matter of fact, the method `nil?` *is* 101 | defined on every object in Ruby. And in our case, `nil.nil?` returns true, of 102 | course, so the test would pass. 103 | 104 | This test, however, would not pass: 105 | 106 | ```ruby 107 | expect(true).to be_nil 108 | ``` 109 | 110 | Because `true.nil?` returns false. 111 | 112 | Now, our `User` instances respond to the method `born_in_leap_year?`. Therefore 113 | RSpec allows us to use a matcher `be_born_in_leap_year`: 114 | 115 | ```ruby 116 | user = User.new("Francisca", "2000-01-01") 117 | expect(user).to be_born_in_leap_year 118 | ``` 119 | 120 | Whoa. 121 | 122 | RSpec sees that we're calling the method `be_born_in_leap_year` and it figures 123 | "Ok, that must mean that the call `user.born_in_leap_year?` must return true." 124 | 125 | Such "magic" methods are another meta-programming technique that RSpec leverages 126 | here. Usually they're pretty debatable, and often not a great choice. However, 127 | in this case, they allow adding this very cool feature to RSpec. 128 | 129 | ## Negating matchers 130 | 131 | What if we want to specify that a user is *not* born in a leap year, though? 132 | In other words, if we want to negate our expectation? 133 | 134 | RSpec allows us to simply invert a matcher by using the method `not_to` as 135 | opposed to `to`: 136 | 137 | ```ruby 138 | expect(user).to be_born_in_leap_year # vs 139 | expect(user).not_to be_born_in_leap_year 140 | ``` 141 | 142 | This works for all other matchers, too, of course — and `to_not` can be used interchangeably with `not_to`: 143 | 144 | ```ruby 145 | expect(1).to eq 1 146 | expect(2).not_to eq 1 147 | 148 | expect(true).to be true 149 | expect(false).not_to be true 150 | 151 | expect([1, 2, 3]).to include 1 152 | expect([1, 2, 3]).to_not include 9 153 | 154 | expect("Ruby Monstas").to start_with "Ruby" 155 | expect("Ruby Monstas").to_not start_with "Java" 156 | ``` 157 | 158 | And so on. 159 | 160 | ## Simple expectations 161 | 162 | If all this matcher business seems too complicated to you, don't worry. You're 163 | not the first programmer feeling that way. 164 | 165 | For now you can always fall back on comparing actual and expected values 166 | like so: 167 | 168 | ```ruby 169 | user = User.new("Francisca", "2000-01-01") 170 | actual = user.name 171 | expected = "Francisca" 172 | expect(actual).to eq expected 173 | ``` 174 | 175 | Or: 176 | 177 | ```ruby 178 | user = User.new("Francisca", "2000-01-01") 179 | actual = user.born_in_leap_year? 180 | expected = true 181 | expect(actual).to eq expected 182 | ``` 183 | 184 | That's some more code to type, but doing that sometimes helps RSpec beginners to 185 | understand what's going on better. 186 | -------------------------------------------------------------------------------- /source/05-rspec/03-format.md: -------------------------------------------------------------------------------- 1 | # Format 2 | 3 | Let's add a few more tests first, and complete the four cases for the 4 | `leap_year?` logic: 5 | 6 | ```ruby 7 | describe User do 8 | context "born in 2001" do 9 | it "is not born in a leap year" do 10 | user = User.new("Francisca", "2001-01-01") 11 | expect(user).not_to be_born_in_leap_year 12 | end 13 | end 14 | 15 | context "born in 1900" do 16 | it "is not born in a leap year" do 17 | user = User.new("Francisca", "1900-01-01") 18 | expect(user).not_to be_born_in_leap_year 19 | end 20 | end 21 | 22 | context "born in 2000" do 23 | it "is born in a leap year" do 24 | user = User.new("Francisca", "2000-01-01") 25 | expect(user).to be_born_in_leap_year 26 | end 27 | end 28 | 29 | context "born in 2004" do 30 | it "is born in a leap year" do 31 | user = User.new("Francisca", "2004-01-01") 32 | expect(user).to be_born_in_leap_year 33 | end 34 | end 35 | end 36 | ``` 37 | 38 | When we run this we'll get the following output: 39 | 40 | ``` 41 | $ rspec user_spec.rb 42 | .... 43 | 44 | Finished in 0.00632 seconds (files took 0.15438 seconds to load) 45 | 4 examples, 0 failures 46 | ``` 47 | 48 | That's nice. Each dot represents an executed test, and we get a pretty summary. 49 | For large test suites this is the most useful output format. 50 | 51 | We only have a few tests, though. Let's try turning on RSpec's documentation 52 | format by passing the command line option `--format doc`. With all 53 | tests passing the output will look like this: 54 | 55 | ``` 56 | $ rspec --format doc user_spec.rb 57 | 58 | User 59 | born in 2001 60 | is not born in a leap year 61 | born in 1900 62 | is not born in a leap year 63 | born in 2000 64 | is born in a leap year 65 | born in 2004 66 | is born in a leap year 67 | 68 | Finished in 0.00398 seconds (files took 0.15358 seconds to load) 69 | 4 examples, 0 failures 70 | ``` 71 | 72 | That's pretty awesome, isn't it? 73 | 74 | Our test output reads like documentation, and tells exactly what behaviour we 75 | expect. 76 | -------------------------------------------------------------------------------- /source/05-rspec/04-advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | RSpec comes with a lot of well-thought-out features that allow us to write very 4 | descriptive, succinct, and concise tests that focus on the few things we really 5 | care about. 6 | 7 | So far, our tests, using the most basic style, look something like this: 8 | 9 | ```ruby 10 | describe User do 11 | context "born in 2001" do 12 | it "is not born in a leap year" do 13 | user = User.new("Francisca", "2001-01-01") 14 | expect(user).not_to be_born_in_leap_year 15 | end 16 | end 17 | 18 | context "born in 1900" do 19 | it "is not born in a leap year" do 20 | user = User.new("Francisca", "1900-01-01") 21 | expect(user).not_to be_born_in_leap_year 22 | end 23 | end 24 | 25 | context "born in 2000" do 26 | it "is born in a leap year" do 27 | user = User.new("Francisca", "2000-01-01") 28 | expect(user).to be_born_in_leap_year 29 | end 30 | end 31 | 32 | context "born in 2004" do 33 | it "is born in a leap year" do 34 | user = User.new("Francisca", "2004-01-01") 35 | expect(user).to be_born_in_leap_year 36 | end 37 | end 38 | end 39 | ``` 40 | 41 | As you see we keep repeating the setup in the first line of every test. 42 | Wouldn't it be nice to move this to a shared place, like Minitest's `setup` 43 | method? 44 | 45 | ## Before 46 | 47 | RSpec has the same feature, but it calls it `before`. 48 | 49 | This is just another method that RSpec defines, and it takes a block, too. 50 | RSpec will call (execute) this block before each one of the tests (examples): 51 | 52 | 53 | ```ruby 54 | describe User do 55 | before { @user = User.new("Francisca", "2001-01-01") } 56 | 57 | context "born in 2001" do 58 | it "is not born in a leap year" do 59 | expect(@user).not_to be_born_in_leap_year 60 | end 61 | end 62 | 63 | context "born in 2000" do 64 | it "is born in a leap year" do 65 | expect(@user).to be_born_in_leap_year 66 | end 67 | end 68 | end 69 | ``` 70 | 71 | Our `before` block sets up an instance variable `@user` so that our test then 72 | can use it. That's cool. 73 | 74 | However, we now have a problem: The hard-coded birthday is specific to the first 75 | context, and should be different for each one of our contexts, because that's 76 | the one single piece of data that changes. The second test would use the wrong 77 | year, and therefore fail, because it would use a user that is actually born in 78 | `2000`, not `2001`, despite what the context desciption tells. 79 | 80 | So how do we fix that? 81 | 82 | ## Let 83 | 84 | RSpec comes with another feature to help with this: the method `let` allows us 85 | to define such bits of data (or more precisely, objects) that need to be 86 | specified per context. Here's how that looks like: 87 | 88 | 89 | ```ruby 90 | describe User do 91 | before { @user = User.new("Francisca", "#{year}-01-01") } 92 | 93 | context "born in 2001" do 94 | let(:year) { 2001 } 95 | 96 | it "is not born in a leap year" do 97 | expect(@user).not_to be_born_in_leap_year 98 | end 99 | end 100 | 101 | context "born in 2000" do 102 | let(:year) { 2000 } 103 | 104 | it "is born in a leap year" do 105 | expect(@user).to be_born_in_leap_year 106 | end 107 | end 108 | end 109 | ``` 110 | 111 | This fixes our problem, and these tests pass. 112 | 113 | Essentially, `let` is a method that defines another method with the given name, 114 | in our case `year`. This method then can be used in other places, such as the 115 | `before` block, or our tests (`it` blocks). 116 | 117 | Instead of using the rather generic `before` block, and instance variables, we 118 | can also use `let` to setup the user: 119 | 120 | ```ruby 121 | describe User do 122 | let(:user) { User.new("Francisca", "#{year}-01-01") } 123 | 124 | context "born in 2001" do 125 | let(:year) { 2001 } 126 | 127 | it "is not born in a leap year" do 128 | expect(user).not_to be_born_in_leap_year 129 | end 130 | end 131 | 132 | context "born in 2000" do 133 | let(:year) { 2000 } 134 | 135 | it "is born in a leap year" do 136 | expect(user).to be_born_in_leap_year 137 | end 138 | end 139 | end 140 | ``` 141 | 142 | This actually is a pretty common way of writing RSpec tests. 143 | 144 | The `let(:user)` statement defines the `user`, and as you can see, this 145 | statement is common to both contexts: they both use `user`. 146 | 147 | The `let(:year)` statements however, are specific to the contexts, and define 148 | the `year` for each one of the contexts. 149 | 150 | ## Subject and Should 151 | 152 | Now `user` is the object under test, and it is an instance of the class `User` 153 | which is already mentioned in the `describe` statement. So, in a way, this is 154 | a little repetitive. 155 | 156 | Because this is such a common pattern, RSpec comes with another feature to make 157 | this a little more concise, and remove this repetition: `subject`. We can use 158 | it like so: 159 | 160 | ```ruby 161 | describe User do 162 | subject { User.new("Francisca", "#{year}-01-01") } 163 | 164 | context "born in 2000" do 165 | let(:year) { 2000 } 166 | 167 | it "is born in a leap year" do 168 | expect(subject).to be_born_in_leap_year 169 | end 170 | end 171 | end 172 | ``` 173 | 174 | And because `subject`, semantically, is the thing we want to test, RSpec also 175 | defines a shorthand for `expect(subject).to` that we can use if we have a 176 | `subject` defined: `should`. That makes our code even more concise: 177 | 178 | ```ruby 179 | describe User do 180 | subject { User.new("Francisca", "#{year}-01-01") } 181 | 182 | context "born in 2000" do 183 | let(:year) { 2000 } 184 | 185 | it "is born in a leap year" do 186 | should be_born_in_leap_year 187 | end 188 | end 189 | end 190 | ``` 191 | 192 | This works great. And we've reduced the amount of code we have to type by 193 | a great deal. 194 | 195 | However, what's with the duplication in the `it` message, and the actual code 196 | that implements our expectation? 197 | 198 | ## Anonymous it 199 | 200 | The lines `it "is born in a leap year"` and `should be_born_in_leap_year` 201 | pretty much describe the same thing, don't they? 202 | 203 | RSpec allows us to omit the message passed to `it` and simply put the 204 | whole test on one line, like so: 205 | 206 | ```ruby 207 | describe User do 208 | subject { User.new("Francisca", "#{year}-01-01") } 209 | 210 | context "born in 2000" do 211 | let(:year) { 2000 } 212 | it { should be_born_in_leap_year } 213 | end 214 | end 215 | ``` 216 | 217 | Whoa. 218 | 219 | Let's apply this to all of our tests. 220 | 221 | We can implement the same test case we've had before (at the beginning of this 222 | chapter) like this, using all the advanced features we've just learned: 223 | 224 | ```ruby 225 | describe User do 226 | subject { User.new("Francisca", "#{year}-01-01") } 227 | 228 | context "born in 2001" do 229 | let(:year) { 2001 } 230 | it { should_not be_born_in_leap_year } 231 | end 232 | 233 | context "born in 1900" do 234 | let(:year) { 1900 } 235 | it { should_not be_born_in_leap_year } 236 | end 237 | 238 | context "born in 2000" do 239 | let(:year) { 2000 } 240 | it { should be_born_in_leap_year } 241 | end 242 | 243 | context "born in 2004" do 244 | let(:year) { 2004 } 245 | it { should be_born_in_leap_year } 246 | end 247 | end 248 | ``` 249 | 250 | You decide which one you like better. 251 | 252 | The output will look a wee bit different, but just as readable, even though we 253 | haven't written out the extra description on the `it` block: 254 | 255 | ``` 256 | $ rspec --format doc user_spec.rb 257 | 258 | User 259 | born in 2001 260 | should not be born in leap year 261 | born in 1900 262 | should not be born in leap year 263 | born in 2000 264 | should be born in leap year 265 | born in 2004 266 | should be born in leap year 267 | 268 | Finished in 0.00462 seconds (files took 0.1464 seconds to load) 269 | 4 examples, 0 failures 270 | ``` 271 | 272 | To summarize, the extra features used are: 273 | 274 | * `let` allows you to dynamically define a method that will return the given 275 | value. We use this to define the only varying bit of data: the `year`. 276 | It is important to note that `let` memoizes the result. I.e. it only calls 277 | the block once. Also, it only executes the block when you actually call it. 278 | * `subject` is a convenience helper that lets us specify the "thing" that 279 | is under test. In our case that's a `User` instance. `subject` also memoizes 280 | the result. And just like `let`, it also only executes the block when you 281 | actually call it. 282 | * `should` assumes that we want to test the `subject`. It is a shorthand for 283 | `expect(subject).to` (while `should_not` is the corresponding shorthand for 284 | `expect(subject).not_to`). 285 | 286 | This style lets us reduce the amount of code that we need to type (and read) 287 | significantly. 288 | 289 | The first version ("basic style") of our tests had 715 characters on 29 lines. 290 | This new version has 474 characters on 23 lines. That's a massive reduction 291 | (~30% less characters to type and read), and allows us to focus much more on 292 | the relevant differences. 293 | 294 | However, it also requires for us to learn these RSpec features, and get 295 | familiar with how to implement and understand such tests properly. 296 | 297 | -------------------------------------------------------------------------------- /source/05-rspec/05-custom_matchers.md: -------------------------------------------------------------------------------- 1 | # Custom Matchers 2 | 3 | We've talked a bit about matchers before, and briefly mentioned that RSpec 4 | even allows us to define our own custom matchers. 5 | 6 | Let's have a quick look at this. 7 | 8 | Here is the code that we have so far: 9 | 10 | ```ruby 11 | require "date" 12 | 13 | def leap_year?(year) 14 | year % 400 == 0 or year % 100 != 0 and year % 4 == 0 15 | end 16 | 17 | class User 18 | def initialize(name, birthday) 19 | @name = name 20 | @birthday = birthday 21 | end 22 | 23 | def name 24 | @name 25 | end 26 | 27 | def born_in_leap_year? 28 | leap_year?(Date.parse(@birthday).year) 29 | end 30 | end 31 | 32 | describe User do 33 | subject { User.new("Francisca", "#{year}-01-01") } 34 | 35 | context "born in 2001" do 36 | let(:year) { 2001 } 37 | it { should_not be_born_in_leap_year } 38 | end 39 | 40 | context "born in 1900" do 41 | let(:year) { 1900 } 42 | it { should_not be_born_in_leap_year } 43 | end 44 | 45 | context "born in 2000" do 46 | let(:year) { 2000 } 47 | it { should be_born_in_leap_year } 48 | end 49 | 50 | context "born in 2004" do 51 | let(:year) { 2004 } 52 | it { should be_born_in_leap_year } 53 | end 54 | end 55 | ``` 56 | 57 | Now, what if we want to specify (test) the `name` method? 58 | 59 | We could simply add the following test, using the basic style: 60 | 61 | ```ruby 62 | describe User do 63 | subject { User.new("Francisca", "#{year}-01-01") } 64 | 65 | context "born in 2001" do 66 | it "returns the name" do 67 | expect(subject.name).to eq "Francisca" 68 | end 69 | 70 | it { should_not be_born_in_leap_year } 71 | end 72 | end 73 | ``` 74 | 75 | However, we'd mix styles here. That's not a bad thing, really! But wouldn't it 76 | be cool if we could say this instead? 77 | 78 | ```ruby 79 | describe User do 80 | subject { User.new("Francisca", "#{year}-01-01") } 81 | 82 | context "born in 2001" do 83 | let(:year) { 2001 } 84 | it { should be_named("Francisca") } 85 | it { should_not be_born_in_leap_year } 86 | end 87 | end 88 | ``` 89 | 90 | If we were to execute this, RSpec would try to call the method `named?` on our 91 | `User` instance (just like `be_born_in_leap_year` calls `born_in_leap_year?` on 92 | the user), and that method does not exist. We could add that method `named?` to 93 | our `User` class, but we don't really want to add any such methods to our real 94 | code, just so we can make the tests prettier. 95 | 96 | Instead, we can define a [custom matcher](https://www.relishapp.com/rspec/rspec-expectations/v/2-4/docs/custom-matchers/define-matcher) 97 | `be_named` that inspects the user's `name`: 98 | 99 | ```ruby 100 | RSpec::Matchers.define(:be_named) do |expected| 101 | match do |object| 102 | object.name == expected 103 | end 104 | end 105 | ``` 106 | 107 | Hmmmm, ... apparently a matcher is a block that calls a method `match` that 108 | takes another block. The actual and expected values are passed as arguments to 109 | the two blocks, somehow. Inside the inner block we are supposed to return 110 | `true` or `false` depending if the matcher is supposed to "match". 111 | 112 | Ok, well, we don't really have to understand how exactly this works in 113 | detail—we can just slap it at the end of our file, and run it: 114 | 115 | ``` 116 | $ rspec --format doc user_spec.rb 117 | 118 | User 119 | born in 2001 120 | should be named "Francisca" 121 | should not be born in leap year 122 | 123 | Finished in 0.00267 seconds (files took 0.15704 seconds to load) 124 | 2 examples, 0 failures 125 | ``` 126 | 127 | Yay! 128 | 129 | Now, how cool is that. 130 | 131 | With those five lines of Ruby code we've extended RSpec to include a matcher 132 | that is pretty specific to our code. And now we can use the advanced style 133 | in order to test our `name` method. 134 | -------------------------------------------------------------------------------- /source/05-rspec/06-filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | One other great feature of RSpec is that it allows us to specify which tests we 4 | want to execute. 5 | 6 | Remember the test output when we made our specs fail before? 7 | 8 | It ended with something like this: 9 | 10 | ``` 11 | Failed examples: 12 | 13 | rspec ./user_spec.rb:28 # User born in 2001 should not be born in leap year 14 | ``` 15 | 16 | The bit `:28` at the end of the filename means "line 28". So this is how we can tell RSpec to 17 | execute one single test only, and it even outputs the command we need to run to 18 | the test output for our convenience. In order to re-run the test that has 19 | failed, we can copy and paste this command from the output. 20 | 21 | That is really convenient if you have a big test suite and your tests are 22 | rather slow. So, while working on fixing a certain bug you'd only want to 23 | run this one failing test. 24 | 25 | You can also run groups of tests: E.g. you can run all tests in the first 26 | `context` by adding the line that `context` statement sits on. In my case 27 | that's line `25`, so this command runs all tests in the first context: 28 | 29 | ``` 30 | $ rspec --format doc ./user_spec.rb:25 31 | Run options: include {:locations=>{"./user_spec.rb"=>[25]}} 32 | 33 | User 34 | born in 2001 35 | should be named "Francisca" 36 | should not be born in leap year 37 | 38 | Finished in 0.00237 seconds (files took 0.15871 seconds to load) 39 | 2 examples, 0 failures 40 | ``` 41 | 42 | That's pretty handy. 43 | 44 | RSpec has more such features that allow you to run your tests selectively. For 45 | example you can tag contexts and tests, and then specify certain tags when 46 | running your tests. 47 | -------------------------------------------------------------------------------- /source/06-rack_test.md: -------------------------------------------------------------------------------- 1 | # Rack::Test 2 | 3 | [Rack::Test](https://github.com/brynary/rack-test) is a library that makes 4 | testing Rack-based web applications easier. 5 | 6 | In this chapter we'll write some tests for our Rack and Sinatra apps from the 7 | book [Webapps for Beginners](http://webapps-for-beginners.rubymonstas.org/), 8 | and, in doing so, explore the helpful features that Rack::Test provides. 9 | 10 | Let's get started. 11 | -------------------------------------------------------------------------------- /source/06-rack_test/01-rack.md: -------------------------------------------------------------------------------- 1 | # Testing a Rack app 2 | 3 | Let's grab our very Rack application from the book 4 | [Webapps for Beginners](http://webapps-for-beginners.rubymonstas.org/rack/hello_world.html). 5 | 6 | It looked like this: 7 | 8 | ```ruby 9 | class Application 10 | def call(env) 11 | handle_request(env["REQUEST_METHOD"], env["PATH_INFO"]) 12 | end 13 | 14 | private 15 | 16 | def handle_request(method, path) 17 | if method == "GET" 18 | get(path) 19 | else 20 | method_not_allowed(method) 21 | end 22 | end 23 | 24 | def get(path) 25 | [200, { "Content-Type" => "text/html" }, ["You have requested the path #{path}, using GET"]] 26 | end 27 | 28 | def method_not_allowed(method) 29 | [405, {}, ["Method not allowed: #{method}"]] 30 | end 31 | end 32 | ``` 33 | 34 | How do we test such an app? 35 | 36 | We could do this manually in RSpec like so: 37 | 38 | ```ruby 39 | describe Application do 40 | context "get to /ruby/monstas" do 41 | it "returns the body" do 42 | env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/ruby/monstas" } 43 | response = app.call(env) 44 | body = response[2][0] 45 | expect(body).to eq "You have requested the path /ruby/monstas, using GET" 46 | end 47 | end 48 | end 49 | ``` 50 | 51 | Or we could make some of this a little more reusable, like so: 52 | 53 | ```ruby 54 | describe Application do 55 | context "get to /ruby/monstas" do 56 | let(:app) { Application.new } 57 | let(:env) { { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/ruby/monstas" } } 58 | let(:response) { app.call(env) } 59 | let(:body) { response[2][0] } 60 | 61 | it "returns the body" do 62 | expect(body).to eq "You have requested the path /ruby/monstas, using GET" 63 | end 64 | end 65 | end 66 | ``` 67 | 68 | And add a test for the status code: 69 | 70 | ```ruby 71 | describe Application do 72 | context "get to /ruby/monstas" do 73 | let(:app) { Application.new } 74 | let(:env) { { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/ruby/monstas" } } 75 | let(:response) { app.call(env) } 76 | let(:status) { response[0] } 77 | let(:body) { response[2][0] } 78 | 79 | it "returns the status 200" do 80 | expect(status).to eq 200 81 | end 82 | 83 | it "returns the body" do 84 | expect(body).to eq "You have requested the path /ruby/monstas, using GET" 85 | end 86 | end 87 | end 88 | ``` 89 | 90 | This passes: 91 | 92 | 93 | ``` 94 | $ rspec rack_spec.rb 95 | .. 96 | 97 | Finished in 0.00086 seconds (files took 0.1486 seconds to load) 98 | 2 examples, 0 failures 99 | ``` 100 | 101 | Our Rack application is just a Ruby class which, when started (e.g. with 102 | `rackup`), will be hooked up to the web server, and called whenever an HTTP 103 | request comes in (e.g. from the browser). 104 | 105 | But we can also just instantiate it ourselves, and call the method `call` with 106 | a hash that complies to the Rack `env` conventions. E.g. in our case we'd want 107 | to set the `REQUEST_METHOD` and `PATH_INFO` keys. 108 | 109 | While this works well, it's also a little bit of a hassle. And that's where 110 | Rack::Test can help. 111 | 112 | Here's how we can use Rack::Test to make our tests a little less verbose: 113 | 114 | 115 | ```ruby 116 | require "rack/test" 117 | 118 | describe Application do 119 | include Rack::Test::Methods 120 | 121 | context "get to /ruby/monstas" do 122 | let(:app) { Application.new } 123 | 124 | it "returns the status 200" do 125 | get "/ruby/monstas" 126 | expect(last_response.status).to eq 200 127 | end 128 | 129 | it "returns the body" do 130 | get "/ruby/monstas" 131 | expect(last_response.body).to eq "You have requested the path /ruby/monstas, using GET" 132 | end 133 | end 134 | end 135 | ``` 136 | 137 | Rack::Test's helper methods expect that there's a method `app` defined, so they 138 | can call it. We can implement that using RSpec's handy `let` feature. 139 | 140 | Now we first call the Rack::Test method `get` with our path. This method 141 | creates the `env` hash for us, and calls `call` on the application. So we don't 142 | have to compose the nasty hash ourselves. 143 | 144 | After this, the method `last_response` will return the response that our 145 | request has returned, and we can test it. 146 | 147 | But the method `get` also returns the same response. So we could make this 148 | a little more concise, and remove the duplicate call `get "/ruby/monstas"` like 149 | this: 150 | 151 | 152 | ```ruby 153 | require "rack/test" 154 | 155 | describe Application do 156 | include Rack::Test::Methods 157 | 158 | context "get to /ruby/monstas" do 159 | let(:app) { Application.new } 160 | let(:response) { get "/ruby/monstas" } 161 | 162 | it { expect(response.status).to eq 200 } 163 | it { expect(response.body).to include "/ruby/monstas, using GET" } 164 | end 165 | end 166 | ``` 167 | 168 | We can also remove the `include` line from our actual tests, and move it to 169 | the RSpec configuration. This configuration normally would sit in a separate 170 | file called `spec_helper.rb`, but for now we'll just move it above our test 171 | code: 172 | 173 | ```ruby 174 | require "rack/test" 175 | 176 | RSpec.configure do |config| 177 | config.include Rack::Test::Methods 178 | end 179 | 180 | describe Application do 181 | context "get to /ruby/monstas" do 182 | let(:app) { Application.new } 183 | let(:response) { get "/ruby/monstas" } 184 | 185 | it { expect(response.status).to eq 200 } 186 | it { expect(response.body).to include "/ruby/monstas, using GET" } 187 | end 188 | end 189 | ``` 190 | 191 | Nice. 192 | 193 | Let's add another test for the case when an unsupported HTTP method is used: 194 | 195 | ```ruby 196 | describe Application do 197 | let(:app) { Application.new } 198 | 199 | context "get to /ruby/monstas" do 200 | let(:response) { get "/ruby/monstas" } 201 | it { expect(response.status).to eq 200 } 202 | it { expect(response.body).to include "/ruby/monstas, using GET" } 203 | end 204 | 205 | context "post to /" do 206 | let(:response) { post "/" } 207 | it { expect(response.status).to eq 405 } 208 | it { expect(response.body).to eq "Method not allowed: POST" } 209 | end 210 | end 211 | ``` 212 | 213 | As you see we've moved the `let(:app)` statement one level up so it can be 214 | shared among both contexts. the `let(:response)` statement on the other 215 | hand is different for both contexts, so we kept them there. 216 | 217 | Again, this passes: 218 | 219 | ``` 220 | $ rspec rack_spec.rb 221 | .... 222 | 223 | Finished in 0.01175 seconds (files took 0.21704 seconds to load) 224 | 4 examples, 0 failures 225 | ``` 226 | 227 | Very cool. We've used RSpec and Rack::Test to write a few tests for the 228 | functionality in our first Rack application. 229 | 230 | Let's head over to the next chapter and do the same for our Sinatra resource 231 | from the Webapps for Beginners book. We'll see a few more Rack::Test helper 232 | methods there. 233 | -------------------------------------------------------------------------------- /source/06-rack_test/02-sinatra.md: -------------------------------------------------------------------------------- 1 | # Testing a Sinatra app 2 | 3 | Let's go back to our Sinatra application that defined the `members` resource in our 4 | [Webapps for Beginners](http://webapps-for-beginners.rubymonstas.org/exercises/sinatra_resource.html) 5 | book. 6 | 7 | We've written that app in what Sinatra calls the "classic style". That means 8 | that we've simply defined the routes in the global namespace, not using any 9 | class for them. 10 | 11 | In order to make it easy for us to test the application using Rack::Test we need 12 | to convert it to what Sinatra calls the "modular style". This simply means that 13 | we require `sinatra/base` instead of `sinatra`, and then define a class that 14 | inherits from `Sinatra::Base`: 15 | 16 | ```ruby 17 | require "sinatra/base" 18 | 19 | class Application < Sinatra::Base 20 | get "/members" do 21 | # ... 22 | end 23 | 24 | get "/members/new" do 25 | # ... 26 | end 27 | end 28 | ``` 29 | 30 | We've converted the application, and included the code in our repository 31 | [here](https://github.com/rubymonsters/testing-for-beginners/tree/main/code/sinatra). 32 | In order to work with it, you can clone this repository from GitHub using 33 | `git`, and `cd` into the directory `code/sinatra`. 34 | 35 | You will notice that we've also extracted the class `Member` to a file 36 | `member.rb`, and the `MemberValidator` to a file `member_validator.rb`. These 37 | files are required at the top of the file `app.rb`, which is the main file and 38 | defines the class `App`. Also, there's a `config.ru` file that allows us to 39 | start the application separately. 40 | 41 | This is nice, because it lets us focus better on our application code. However 42 | it also means that we need to tell Ruby where to look for these files. I.e. we 43 | have to setup the Ruby 44 | [load path](http://webapps-for-beginners.rubymonstas.org/libraries/load_path.html) 45 | properly. 46 | 47 | When we use `ruby`, `rackup`, or `rspec` to load the file we can add the 48 | current working directory to the Ruby load path by adding the option `-I .`. 49 | The dot means "this current directory". The option `-I` tells Ruby to look for 50 | files here when we use `require`. 51 | 52 | In order to start the application you can run `rackup -I .`. 53 | 54 | Ok, let's add some tests next. 55 | 56 | For that we'll create a separate file `app_spec.rb`. RSpec wants us to name 57 | files that contain tests with the suffix `_spec.rb`, and we want to keep 58 | our code and tests separate this time. 59 | 60 | We've also included a file `spec_helper.rb`. In RSpec this is a common place 61 | to keep setup and configuration. Our spec helper doesn't do a lot, but it 62 | requires our `app`, and includes the `Rack::Test::Methods` module to our 63 | RSpec tests. 64 | 65 | So in `app_spec.rb` we'll want to require the `spec_helper` first, and then 66 | we're good to go, and can start writing tests: 67 | 68 | ```ruby 69 | require "spec_helper" 70 | 71 | describe App do 72 | it "works" do 73 | # ... 74 | end 75 | end 76 | ``` 77 | 78 | We can run our tests like this: 79 | 80 | ``` 81 | $ rspec -I . app_spec.rb 82 | . 83 | 84 | Finished in 0.00052 seconds (files took 0.6769 seconds to load) 85 | 1 example, 0 failures 86 | ``` 87 | 88 | Of course we haven't implemented any actual test, yet. 89 | 90 | So let's do that next. 91 | 92 | ## Taking notes first 93 | 94 | Let's start adding tests in the order that the [exercise](http://webapps-for-beginners.rubymonstas.org/exercises/sinatra_resource.html) 95 | specified. 96 | 97 | In RSpec, the method `it` can be used to write test stubs first and mark them 98 | as "to be done later", by simply not adding a block just yet. This is nice 99 | because it allows us to focus on what we want to test first, and then add the 100 | test implementation later. 101 | 102 | We'll just copy the requirements from the exercise, more or less: 103 | 104 | 105 | ```ruby 106 | require "spec_helper" 107 | 108 | describe App do 109 | let(:app) { App.new } 110 | 111 | context "GET to /members" do 112 | it "returns status 200 OK" 113 | it "displays a list of member names that link to /members/:name" 114 | end 115 | 116 | context "GET to /members/:name" do 117 | it "returns status 200 OK" 118 | it "displays the member's name" 119 | end 120 | 121 | context "GET to /members/new" do 122 | it "returns status 200 OK" 123 | it "displays a form that POSTs to /members" 124 | it "displays an input tag for the name" 125 | it "displays a submit tag" 126 | end 127 | 128 | context "POST to /members" do 129 | context "given a valid name" do 130 | it "adds the name to the members.txt file" 131 | it "returns status 302 Found" 132 | it "redirects to /members/:name" 133 | end 134 | 135 | context "given a duplicate name" do 136 | it "does not add the duplicate to the members.txt file" 137 | it "returns status 200 OK" 138 | it "displays a form that POSTs to /members" 139 | it "displays an input tag for the name, with the value set" 140 | end 141 | 142 | context "given an empty name" do 143 | it "does not add the name to the members.txt file" 144 | it "returns status 200 OK" 145 | it "displays a form that POSTs to /members" 146 | it "displays an input tag for the name, with the value set" 147 | end 148 | end 149 | end 150 | ``` 151 | 152 | Does this make sense? We've basically formulated the specification from 153 | [this exercise](http://webapps-for-beginners.rubymonstas.org/exercises/sinatra_resource.html) 154 | as RSpec test stubs. 155 | 156 | You can see how we're using nested `context` blocks here for the first time. 157 | This allows us to group our tests for the three cases of submitting a valid, 158 | duplicate, or empty name. 159 | 160 | If we run this, RSpec will tell us we have 19 "pending" tests to fill in: 161 | 162 | ``` 163 | rspec -I . app_spec.rb 164 | ************ 165 | 166 | Pending: (Failures listed here are expected and do not affect your suite's status) 167 | 168 | 1) App GET to /members returns status 200 OK 169 | # Not yet implemented 170 | # ./app_spec.rb:8 171 | 172 | [...[ 173 | 174 | 19) App POST to /members given an empty name displays an input tag for the name, with the value set 175 | # Not yet implemented 176 | # ./app_spec.rb:41 177 | 178 | 179 | Finished in 0.0018 seconds (files took 0.59211 seconds to load) 180 | 19 examples, 0 failures, 19 pending 181 | ``` 182 | 183 | Cool. 184 | 185 | Let's start filling them in. 186 | 187 | ## Adding test implementation 188 | 189 | Since Sinatra uses Rack under the hood we can apply all the techniques we've 190 | learned while writing tests for our Rack app. 191 | 192 | We can create a new application instance with `App.new`, and make requests 193 | using the `Rack::Test` helper methods `get`, `post`, and so on. These methods 194 | will return a response object that we can inspect in our tests: 195 | 196 | ```ruby 197 | require "spec_helper" 198 | 199 | describe App do 200 | let(:app) { App.new } 201 | 202 | context "GET to /members" do 203 | let(:response) { get "/members" } 204 | 205 | it "returns status 200 OK" do 206 | expect(response.status).to eq 200 207 | end 208 | 209 | it "displays a list of member names that link to /members/:name" do 210 | expect(response.body).to include( 211 | 'Anja', 212 | 'Maren' 213 | ) 214 | end 215 | end 216 | 217 | context "GET to /members/:name" do 218 | it "returns status 200 OK" 219 | it "displays the member's name" 220 | end 221 | end 222 | ``` 223 | 224 | Does this work? Yes it does. These tests indeed pass: 225 | 226 | ``` 227 | $ rspec -I . --format doc app_spec.rb:6 228 | Run options: include {:locations=>{"./app_spec.rb"=>[6]}} 229 | 230 | App 231 | GET to /members 232 | returns status 200 OK 233 | displays a list of member names that link to /members/:name 234 | 235 | Finished in 0.04964 seconds (files took 0.60312 seconds to load) 236 | 2 examples, 0 failures 237 | ``` 238 | 239 | Nice. 240 | 241 | However, our test for the HTML tags is a little brittle. A test is brittle when 242 | it breaks too easily. It's not robust enough. 243 | 244 | In our case our specification says that there needs to be a list of links that 245 | show the name and link to the right path. However, our test would fail if 246 | we would, for example, add a CSS class to the links, so we can style them 247 | more easily. Or if we'd add any other HTML attributes to it. Because we simply 248 | compare the full HTML tag as a string. 249 | 250 | Our app would then still function the same, and comply with the specification. 251 | But our test would break. That's called a brittle test. 252 | 253 | So what do we do? 254 | 255 | ## HaveTag matcher 256 | 257 | One option would be to use a regular expression, like so: 258 | 259 | ```ruby 260 | it "displays a list of member names that link to /members/:name" do 261 | expect(response.body).to match %r(Anja) 262 | expect(response.body).to match %r(Maren) 263 | end 264 | ``` 265 | 266 | This is cool because we can use plain Ruby, but on the other hand regular 267 | expressions are a little hard to read. 268 | 269 | We could also implement a custom matcher for this. How about `have_tag`: 270 | 271 | ```ruby 272 | RSpec::Matchers.define(:have_tag) do |name, content, attributes = {}| 273 | match do |html| 274 | # somehow figure out if `html` has the right tag. 275 | end 276 | end 277 | ``` 278 | 279 | With that we could formulate our test like so: 280 | 281 | ```ruby 282 | it "displays a list of member names that link to /members/:name" do 283 | expect(response.body).to have_tag(:a, :href => "/members/Anja", :text => "Anja") 284 | expect(response.body).to have_tag(:a, :href => "/members/Maren", :text => "Maren") 285 | end 286 | ``` 287 | 288 | And leave the nitty gritty work of matching to our custom matcher. 289 | 290 | Luckily there's a gem for that: [rspec-html-matchers](https://github.com/kucaahbe/rspec-html-matchers). 291 | Let's try that. We need to install the gem and add it to RSpec in our 292 | `spec_helper.rb` file: 293 | 294 | ```ruby 295 | require "rspec-html-matchers" 296 | 297 | RSpec.configure do |config| 298 | # ... 299 | config.include RSpecHtmlMatchers 300 | end 301 | ``` 302 | 303 | Ok, this works. Our test is now much less brittle, very cool. 304 | 305 | Now let's have a look at the next route: 306 | 307 | ```ruby 308 | context "GET to /members/:name" do 309 | let(:response) { get "/members/Anja" } 310 | 311 | it "returns status 200 OK" do 312 | expect(response.status).to eq 200 313 | end 314 | 315 | it "displays the member's name" do 316 | expect(response.body).to have_tag(:p, :text => "Name: Anja") 317 | end 318 | end 319 | ``` 320 | 321 | We can simply use all the same techniques for the `GET /members/:name` route. 322 | Nothing new here. 323 | 324 | These specs pass, too: 325 | 326 | ``` 327 | $ rspec -I . --format doc app_spec.rb:19 328 | Run options: include {:locations=>{"./app_spec.rb"=>[19]}} 329 | 330 | App 331 | GET to /members/:name 332 | returns status 200 OK 333 | displays the member's name 334 | 335 | Finished in 0.08506 seconds (files took 0.80809 seconds to load) 336 | 2 examples, 0 failures 337 | ``` 338 | 339 | Cool. Ok, what about the form on `/members/new`? 340 | 341 | ```ruby 342 | context "GET to /members/new" do 343 | let(:response) { get "/members/new" } 344 | 345 | it "returns status 200 OK" do 346 | expect(response.status).to eq 200 347 | end 348 | 349 | it "displays a form that POSTs to /members" do 350 | expect(response.body).to have_tag(:form, :action => "/members", :method => "post") 351 | end 352 | 353 | it "displays an input tag for the name" do 354 | expect(response.body).to have_tag(:input, :type => "text", :name => "name") 355 | end 356 | 357 | it "displays a submit tag" do 358 | expect(response.body).to have_tag(:input, :type => "submit") 359 | end 360 | end 361 | ``` 362 | 363 | We seem to be getting the hang of this web application testing business. 364 | 365 | These specs pass, too: 366 | 367 | ``` 368 | $ rspec -I . --format doc app_spec.rb:31 369 | Run options: include {:locations=>{"./app_spec.rb"=>[31]}} 370 | 371 | App 372 | GET to /members/new 373 | returns status 200 OK 374 | displays a form that POSTs to /members 375 | displays an input tag for the name 376 | displays a submit tag 377 | 378 | Finished in 0.06903 seconds (files took 0.63294 seconds to load) 379 | 4 examples, 0 failures 380 | ``` 381 | 382 | Now the next route, `POST to /members`, is going to be a little less trivial, 383 | and we'll need to introduce a few new concepts here. 384 | 385 | Let's see. 386 | 387 | ```ruby 388 | context "POST to /members" do 389 | let(:file) { File.read("members.txt") } 390 | 391 | context "given a valid name" do 392 | let(:response) { post "/members", :name => "Monsta" } 393 | 394 | it "adds the name to the members.txt file" do 395 | expect(file).to include("Monsta") 396 | end 397 | 398 | it "returns status 302 Found" do 399 | expect(response.status).to eq 302 400 | end 401 | end 402 | ``` 403 | 404 | These tests read as if they should pass, don't they? We think they do. 405 | 406 | Except, they don't. 407 | 408 | ## Leaking state 409 | 410 | When we run these tests something curious happens. At first the second test 411 | (testing the status `302`) passes, and the first one does not. From then on, 412 | when we re-run the tests, the first one passes, and the second one doesn't. 413 | 414 | Why's that? This is a common problem in testing. Programmers say that "tests 415 | leak state". By that they mean that there is something that persists state 416 | (data), this state is modified when we run our tests, and our tests rely on it. 417 | Now whenever we run our tests the state persisted in one test can influence the 418 | next test. Thus, it leaks. 419 | 420 | In our case this is the file `members.txt` of course. More precisely, our tests 421 | rely on the assumption that the name `Monsta` is not in the persistent file 422 | `members.txt`. 423 | 424 | But when we run our tests the first test that executes will add it, and save 425 | the file. All other tests from then on run against a *different* persistent 426 | state than the first one. That is bad. 427 | 428 | We can fix that by resetting the contents of the file `members.txt` to the 429 | same state before or after each test run. Let's do that: 430 | 431 | ```ruby 432 | context "POST to /members" do 433 | let(:response) { post "/members", :name => "Monsta" } 434 | let(:file) { File.read("members.txt") } 435 | 436 | before { File.write("members.txt", "Anja\nMaren\n") } 437 | 438 | # ... 439 | end 440 | ``` 441 | 442 | I.e. for each of our tests, before RSpec runs it, it will execute the `before` 443 | block first, and write the same content to the file. 444 | 445 | This is an important concept in testing: You want your tests to always run 446 | against the same state. If anything is persisted, e.g. in our file, in a 447 | database, or anywhere else, we need to apply extra measures to make sure 448 | this state is reset everytime we run a single test. 449 | 450 | Cool. When we now run the tests we still get a failure. Our first test still 451 | does not pass. However, we now get the same failure no matter how often we run 452 | it. 453 | 454 | So what's wrong with the first test? 455 | 456 | ## Side effects 457 | 458 | If you think about it, we run the actual `POST` request in the `let(:response)` 459 | statement. And so far, all of our tests have somehow used the `response`. 460 | Therefore RSpec has executed the `POST` request, and we've seen the right 461 | results. 462 | 463 | However, this one test now does not use `response` at all. It looks at the file 464 | contents instead. In programming, this is called a [side effect](http://programmers.stackexchange.com/questions/40297/what-is-a-side-effect). 465 | We test something that is not returned from the method call that we need to 466 | execute, and therefore our test happens to not make that method call at all. 467 | You could also say that our test happens to reveal that we're testing a side 468 | effect here. In this way tests can be diagnostic, and tell us things about 469 | our code that we haven't noticed before. 470 | 471 | In web applications side effects are expected: we do want to store (persist) 472 | some data in our text file, or in the database. However, it is also good to 473 | be aware of this. 474 | 475 | We could fix our test like so: 476 | 477 | ```ruby 478 | it "adds the name to the members.txt file" do 479 | response 480 | expect(file).to include("Monsta") 481 | end 482 | ``` 483 | 484 | This will first make the `POST` request, and then inspect the file. In fact, 485 | our test now passes: 486 | 487 | ``` 488 | $ rspec -I . --format doc app_spec.rb:57 489 | Run options: include {:locations=>{"./app_spec.rb"=>[57]}} 490 | 491 | App 492 | POST to /members 493 | given a valid name 494 | adds the name to the members.txt file 495 | returns status 302 Found 496 | 497 | Finished in 0.04476 seconds (files took 0.64694 seconds to load) 498 | 2 examples, 0 failures 499 | ``` 500 | 501 | However, calling `response` in this place seems kind of weird, does it not? We 502 | don't actually use the response object here. And the line does not really 503 | convey that all we want to do is make the `POST` request here. 504 | 505 | So what's an alternative? 506 | 507 | ## Let! 508 | 509 | RSpec has another variation of the `let` method that makes this more visible: 510 | `let!`. 511 | 512 | `let!` is useful in exactly such situations: We need to evaluate the 513 | `response`, because we need to test a side effect. And we want to mark this as 514 | an exceptional thing. The same line then also hints that we're making a `POST` 515 | request. 516 | 517 | That seems like a good compromise, let's use it: 518 | 519 | ```ruby 520 | context "POST to /members" do 521 | let(:file) { File.read("members.txt") } 522 | before { File.write("members.txt", "Anja\nMaren\n") } 523 | 524 | context "given a valid name" do 525 | let!(:response) { post "/members", :name => "Monsta" } 526 | 527 | it "adds the name to the members.txt file" do 528 | expect(file).to include("Monsta") 529 | end 530 | end 531 | end 532 | ``` 533 | 534 | Ok, this looks great. Our tests pass, and we're using another nice RSpec 535 | feature. 536 | 537 | ## Custom matchers 538 | 539 | What's next? What about our redirect test? It would be nice if we could use a 540 | matcher for that: 541 | 542 | ```ruby 543 | it "redirects to /members/:name" do 544 | expect(response).to redirect_to "/members/Monsta" 545 | end 546 | ``` 547 | 548 | In fact `rspec-rails`, a gem for testing Rails applications with RSpec, has such 549 | a matcher. However, Rack::Test doesn't. So let's use that opportunity to write 550 | our own custom matcher for this: 551 | 552 | ```ruby 553 | RSpec::Matchers.define(:redirect_to) do |path| 554 | match do |response| 555 | response.status == 302 && response.headers['Location'] == "http://example.org#{path}" 556 | end 557 | end 558 | ``` 559 | 560 | Looks alright? We compare the actual response status to 302, and we compare the 561 | response header `Location` to a URL that has our path. 562 | 563 | What's with the `example.org` business though? As mentioned at some point in 564 | the Webapps for Beginners book, a redirect header needs to be a full URL as per 565 | the HTTP specification. So our Sinatra app turns the path into a full URL. 566 | Since we haven't specified any other hostname in our app it just adds this 567 | fantasy domain name. 568 | 569 | This works, the given test would pass. 570 | 571 | Can you spot a problem with it though? 572 | 573 | Our matcher, again, is a brittle. What if we configure a proper hostname for 574 | our app at some point? Our tests then would fail, even though the application 575 | code would function as expected. Our tests would be too brittle, and fail when 576 | they should pass. 577 | 578 | Let's fix that, and parse the URL, so we can compare the path only: 579 | 580 | ```ruby 581 | require 'uri' 582 | 583 | RSpec::Matchers.define(:redirect_to) do |path| 584 | match do |response| 585 | uri = URI.parse(response.headers['Location']) 586 | response.status == 302 && uri.path == path 587 | end 588 | end 589 | ``` 590 | 591 | Now, that's much better. 592 | 593 | There's one more aspect that is a little brittle, too: we test for a very 594 | specific status code. According to the HTTP specification all status codes that 595 | start with a `3` are considered [redirects](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection). 596 | 597 | So let's fix that, too. 598 | 599 | ```ruby 600 | RSpec::Matchers.define(:redirect_to) do |path| 601 | match do |response| 602 | uri = URI.parse(response.headers['Location']) 603 | response.status / 100 == 3 && uri.path == path 604 | end 605 | end 606 | ``` 607 | 608 | This is a trick, of course. The response status code is an `Integer`. So if we 609 | divide it by `100` we'll get another `Integer`, in our case `3`, with any 610 | decimals cut off. 611 | 612 | We could also turn the number into a string and inspect the first character: 613 | 614 | ```ruby 615 | RSpec::Matchers.define(:redirect_to) do |path| 616 | match do |response| 617 | uri = URI.parse(response.headers['Location']) 618 | response.status.to_s[0] == "3" && uri.path == path 619 | end 620 | end 621 | ``` 622 | 623 | You decide which one you like better. 624 | 625 | Ok, let's slap this matcher into our `spec_helper.rb` and see if it works: 626 | 627 | ``` 628 | $ rspec -I . --format doc app_spec.rb:65 629 | Run options: include {:locations=>{"./app_spec.rb"=>[65]}} 630 | 631 | App 632 | POST to /members 633 | given a valid name 634 | redirects to /members/:name 635 | 636 | Finished in 0.039 seconds (files took 0.69768 seconds to load) 637 | 1 example, 0 failures 638 | ``` 639 | 640 | It does! Very nice. 641 | 642 | ## Shared examples 643 | 644 | Let's fill in the tests for the next case, posting a duplicate name. We can 645 | mostly steal from the tests we've already written for the `GET /members/new` 646 | route. 647 | 648 | Also, we can simply test that the file still has the expected contents: 649 | 650 | ```ruby 651 | context "given a duplicate name" do 652 | let!(:response) { post "/members", :name => "Maren" } 653 | 654 | it "does not add the name to the members.txt file" do 655 | expect(file).to eq "Anja\nMaren" 656 | end 657 | 658 | it "returns status 200 OK" do 659 | expect(response.status).to eq 200 660 | end 661 | 662 | it "displays a form that POSTs to /members" do 663 | expect(response.body).to have_tag(:form, :action => "/members", :method => "post") 664 | end 665 | 666 | it "displays an input tag for the name, with the value set" do 667 | expect(response.body).to have_tag(:input, :type => "text", :name => "name", :value => "Maren") 668 | end 669 | end 670 | ``` 671 | 672 | Now there's only one context missing: posting an empty string as a name. 673 | 674 | This is interesting. 675 | 676 | We could simply copy and paste the tests that we already have, and just change 677 | the name in the before block (and the context description, of course). 678 | 679 | However, this is also a great opportunity to look at one more, rather advanced 680 | RSpec features: shared example groups. 681 | 682 | RSpec allows us to define groups of tests (examples), and include them in 683 | different contexts. And this is exactly what's really useful for us here. 684 | 685 | Let's move our tests from the last context to a shared example group like so: 686 | 687 | ```ruby 688 | shared_examples_for "invalid member data" do 689 | it "does not add the name to the members.txt file" do 690 | expect(file).to eq "Anja\nMaren" 691 | end 692 | 693 | it "returns status 200 OK" do 694 | expect(response.status).to eq 200 695 | end 696 | 697 | it "displays a form that POSTs to /members" do 698 | expect(response.body).to have_tag(:form, :action => "/members", :method => "post") 699 | end 700 | 701 | it "displays an input tag for the name, with the value set" do 702 | expect(response.body).to have_tag(:input, :type => "text", :name => "name", :value => "Maren") 703 | end 704 | end 705 | ``` 706 | 707 | Now we can include these tests to our two contexts that deal with invalid 708 | member data: 709 | 710 | ```ruby 711 | shared_examples_for "invalid member data" do 712 | # ... 713 | end 714 | 715 | context "given a duplicate name" do 716 | let!(:response) { post "/members", :name => "Maren" } 717 | include_examples "invalid member data" 718 | end 719 | 720 | context "given an empty name" do 721 | let!(:response) { post "/members", :name => "" } 722 | include_examples "invalid member data" 723 | end 724 | ``` 725 | 726 | That's really cool. 727 | 728 | Our final tests now all pass: 729 | 730 | ``` 731 | $ rspec -I . --format doc app_spec.rb 732 | 733 | App 734 | GET to /members 735 | returns status 200 OK 736 | displays a list of member names that link to /members/:name 737 | GET to /members/:name 738 | returns status 200 OK 739 | displays the member's name 740 | GET to /members/new 741 | returns status 200 OK 742 | displays a form that POSTs to /members 743 | displays an input tag for the name 744 | displays a submit tag 745 | POST to /members 746 | given a valid name 747 | adds the name to the members.txt file 748 | returns status 302 Found 749 | redirects to /members/:name 750 | given a duplicate name 751 | does not add the name to the members.txt file 752 | returns status 200 OK 753 | displays a form that POSTs to /members 754 | displays an input tag for the name, with the value set 755 | given an empty name 756 | does not add the name to the members.txt file 757 | returns status 200 OK 758 | displays a form that POSTs to /members 759 | displays an input tag for the name, with the value set 760 | 761 | Finished in 0.14409 seconds (files took 0.62813 seconds to load) 762 | 19 examples, 0 failures 763 | ``` 764 | 765 | Why don't you go ahead and add some more specs for the remaining routes. 766 | 767 | The groups 768 | 769 | * `GET to /members/:name/edit` and `PUT to /members/:name` and 770 | * `GET to /members/:name/delete` and `DELETE to /members/:name` 771 | 772 | still need to be tested, and adding these tests makes for an excellent exercise. 773 | 774 | -------------------------------------------------------------------------------- /source/07-headless.md: -------------------------------------------------------------------------------- 1 | # Headless browsers 2 | 3 | A so-called headless browser is a full featured browser that can be started 4 | on the command line, and behaves just like any other browser but simply does 5 | not come with a browser window. 6 | 7 | Most importantly, it can be used programmatically: You can write code that asks 8 | this browser to navigate to a certain page, click on a link, submit a form, and 9 | so on. 10 | 11 | This is cool because so far we've completely ignored that our web application 12 | might include things like Javascript code or CSS that hides certain elements 13 | from the page, and only reveals them when the user does something. 14 | 15 | Headless browsers can be used to test the user's experience in a more complete 16 | way. 17 | 18 | Rack::Test based tests (and friends, there are other libraries that do similar 19 | things) simply instantiate the application, run a fake request against it, and 20 | then inspect the response. This works great for simple applications like our 21 | Sinatra app. 22 | 23 | However, this approach also does not really test the "full stack". E.g. it 24 | ignores that the user's browser might alter the page in some way, like, through 25 | Javascript or CSS. 26 | 27 | Headless browsers allow us to write full stack tests. These tests tend to be a 28 | little slower, and potentially more brittle. But they're a great tool to have 29 | on your belt as a developer. 30 | 31 | There are several [headless browsers](https://github.com/dhamaniasad/HeadlessBrowsers) 32 | out there, and their quality isn't always the greatest. 33 | 34 | The most popular ones probably are [Selenium](http://docs.seleniumhq.org/) 35 | (which has become a little dusty these days), and [Phantom.js](http://phantomjs.org/) 36 | (which is pretty modern, and stable). 37 | 38 | So, let's have a look at [Phantom.js](http://phantomjs.org/) next. 39 | 40 | -------------------------------------------------------------------------------- /source/07-headless/01-phantomjs.md: -------------------------------------------------------------------------------- 1 | # Phantom.js 2 | 3 | *Headless browers* 4 | 5 | In order to install Phantom.js download it from their website [here](http://phantomjs.org/download.html). 6 | 7 | On Windows the download page says that `phantomjs.exe` should be ready use 8 | once you've run the installer. 9 | 10 | On Mac OSX and Linux you'll need to download and extract the zip file, move the 11 | contents to a proper place, and make the binary (executable, command line app) 12 | available in your PATH (that's a variable that tells the system where to look 13 | for binaries (command line apps). 14 | 15 | Here's one way to do that: 16 | 17 | Download and the zip file for your operating system. On Mac OSX it would end 18 | up in a the `~/Downloads` directory, so you can go there and expand the zip 19 | file by double clicking it. 20 | 21 | At the time of this writing the current version is `2.1.1`, and it is included 22 | in the directory name. So we're going to use `~/Downloads/phantomjs-2.1.1-macosx` 23 | in this example. You'll need to use the version number and operating system 24 | name that you have. 25 | 26 | Now in your terminal copy it to a proper place. One good location is `/usr/local`: 27 | 28 | ``` 29 | $ sudo cp ~/Downloads/phantomjs-2.1.1-macosx/bin/phantomjs /usr/local/bin 30 | ``` 31 | 32 | `sudo` might not be necessary, e.g. if you're using Homebrew. But on many 33 | systems it will be. `sudo` will ask you for your computer's password, enter it, 34 | and hit return. 35 | 36 | Once you've successfully installed `phantomjs` your system should find it 37 | when you run: 38 | 39 | ``` 40 | $ which phantomjs 41 | /usr/local/bin/phantomjs 42 | ``` 43 | 44 | If that outputs `phantomjs not found` you've made a mistake. 45 | 46 | ## Trying out Phantom.js 47 | 48 | Now let's try to do something with it. 49 | 50 | Phantom.js wants us to use Javascript. So let's create a file `monstas.js` 51 | and add the following code: 52 | 53 | ```javascript 54 | console.log('Hello Ruby Monstas!'); 55 | phantom.exit(); 56 | ``` 57 | 58 | Save that file and run it with phantom.js in the terminal like so: 59 | 60 | ``` 61 | $ phantomjs monstas.js 62 | Hello Ruby Monstas! 63 | ``` 64 | 65 | On my computer that hangs for a brief moment, but then executes the Javascript 66 | code and prints out the message. 67 | 68 | That's kinda cool, isn't it? We've just uses a browser to run some Javascript 69 | and print something to the Javascript console (which gets printed to our 70 | terminal because there's no browser window). 71 | 72 | Let's try browsing to an actual website. 73 | 74 | Change the code in the file `monstas.js` like so: 75 | 76 | ```javascript 77 | var page = require('webpage').create(); 78 | page.open('http://rubymonstas.org/', function(status) { 79 | console.log(page.plainText); 80 | phantom.exit(); 81 | }); 82 | ``` 83 | 84 | When you run this it will output the plain text from our website, with all 85 | HTML tags removed: 86 | 87 | ``` 88 | $ phantomjs monstas.js 89 | Ruby Monstas 90 | 91 | Ruby Monstas stands for (Berlin) Ruby Monday Study Group’stas, and this is our homepage. 92 | 93 | We are one of many project groups in the Berlin Rails Girls community, some of which can be found here. 94 | 95 | We meet every Monday at 7pm at the Ganz oben office (the office on the very top floor). 96 | ``` 97 | 98 | Hah! How cool is that. We can write some Javascript code that, when run on the terminal 99 | will actually browse to a website, fetch the response, and display the text. 100 | 101 | Let's try outputting the actual HTML: 102 | 103 | ```javascript 104 | var page = require('webpage').create(); 105 | page.open('http://rubymonstas.org/', function(status) { 106 | console.log(page.content); 107 | phantom.exit(); 108 | }); 109 | ``` 110 | 111 | And run it: 112 | 113 | ``` 114 | $ phantomjs monstas.js 115 | 116 | 117 | 118 | Ruby Monstas 119 | 120 | 121 | 122 | 123 |
124 |

Ruby Monstas

125 |

Ruby Monstas stands for (Berlin) Ruby Monday Study Group’stas, and this is 126 | our homepage.

127 | ... 128 | ``` 129 | 130 | Wohoo! Very cool. 131 | 132 | We can see the full HTML just like we saw it in our Rack::Test based tests. 133 | So we could now run some assertions against it, and test the website. 134 | 135 | This is roughly how testing a web application using a headless browser works. 136 | 137 | * Instead of navigating to an external website (like our homepage) you'd 138 | navigate to the app that is started locally, just like you'd start it when 139 | you navigate around during development. 140 | * Instead of manually typing Javascript we use another Ruby library in order 141 | to talk to Phantom.js and instruct it to do the things that we want to test. 142 | * We can then inspect the resulting website, and see if the HTML elements that 143 | we expect are there. Except this time (unlike Rack::Test based tests) we'd 144 | see the output that the browser actually displays, including Javascript and 145 | CSS applied. 146 | 147 | Does that make sense? 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /source/07-headless/02-capybara.md: -------------------------------------------------------------------------------- 1 | # Capybara 2 | 3 | *A DSL for testing web applications* 4 | 5 | Now, we want to test a Ruby application, and we don't really want to write 6 | a lot of Javascript code in order to do so. 7 | 8 | And the Javascript code we would have to write actually is quite a bit of a 9 | hassle, unless we use a bunch of libraries. For example, it's not even very 10 | easy to click on a link in a browser through plain Javascript. That's because 11 | the document object model (DOM), as defined by the W3C is quite an odd 12 | construct. 13 | 14 | The code for clicking on a link might look something like this: 15 | 16 | ```javascript 17 | var element = document.querySelector("a[href='/location.html']"); 18 | var event = document.createEvent("MouseEvents"); 19 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 20 | element.dispatchEvent(event); 21 | ``` 22 | 23 | That's right. One does not just "click". One creates a mouse event, sets it up 24 | with tons of options, and then "dispatches" it on the element. 25 | 26 | Luckily there are Ruby libraries to help with this kind of stuff. The probably 27 | most widely used one is [Capybara](http://jnicklas.github.io/capybara/). 28 | 29 | That's right. Capybara maybe has the most badass mascot animal of all open 30 | source libraries. 31 | 32 | It also defines a [DSL](https://github.com/jnicklas/capybara#the-dsl) for 33 | describing interactions with web pages. The DSL includes handy methods such 34 | as: 35 | 36 | ```ruby 37 | visit "/members" # go to a URL 38 | click_on "Add member" # click on a link 39 | fill_in "Name", with: "Monsta!" # fill in a form field 40 | click_on "Submit" # submit the form 41 | ``` 42 | 43 | Now that looks a little more handy. 44 | 45 | With Capybara you can easily inspect the HTML of a page, find elements 46 | (remember how we added another gem `rspec-html-matchers`? Capybara brings 47 | similar functionality), interact with the page by clicking around, filling in 48 | form fields, submitting forms etc. You can also evaluate Javascript on the page 49 | (in order to simulate certain things), work with native browser alert windows, 50 | and a lot more. You can even take screenshots, even though there's no browser 51 | window. 52 | 53 | How does Capybara know how to talk to Phantom.js though? 54 | 55 | As mentioned there are a lot of different headless browsers, and Capybara can 56 | talk to some of them. In order to do so Capybara uses "drivers". And the driver 57 | for talking to Phantom.js is called [Poltergeist](https://github.com/teampoltergeist/poltergeist), 58 | because a name like `CapybaraPhantomjsDriver` would have been too boring. 59 | 60 | Anyway, we want to install both gems: 61 | 62 | ``` 63 | $ gem install capybara poltergeist 64 | ``` 65 | 66 | Let's try it out! 67 | 68 | Create a file `capybara.rb` and add the following: 69 | 70 | ```ruby 71 | require 'capybara/poltergeist' 72 | 73 | Capybara.default_driver = :poltergeist 74 | 75 | browser = Capybara.current_session 76 | browser.visit 'http://rubymonstas.org' 77 | puts browser.html 78 | ``` 79 | 80 | Awesome. Again, that prints out the HTML of our homepage. 81 | 82 | Now, let's click on a link: 83 | 84 | ```ruby 85 | browser = Capybara.current_session 86 | browser.visit 'http://rubymonstas.org' 87 | browser.click_on 'Ganz oben office' 88 | puts browser.text 89 | ``` 90 | 91 | That prints out: 92 | 93 | ``` 94 | Where we meet How to get to the office space where we meet (the former Travis CI office). 95 | ... 96 | ``` 97 | 98 | As you can see we've successfully navigated to another page, and now look at 99 | the plain text of that other. 100 | 101 | If we want to make our test not depend on the link text (because that might 102 | change at any time) then we can also use XPath or CSS [selectors](http://ejohn.org/blog/xpath-css-selectors/). 103 | 104 | CSS selectors are a little more common because many developers already know 105 | them from CSS and Javascript (e.g. JQuery). XPath selectors on the other hand 106 | are even more powerful. 107 | 108 | For example, this CSS selector says "select the `a` tag that has the attribute 109 | `href="/location.html"`. 110 | 111 | ```ruby 112 | browser.find('a[href="/location.html"]').click 113 | ``` 114 | -------------------------------------------------------------------------------- /source/07-headless/03-features.md: -------------------------------------------------------------------------------- 1 | # Feature tests 2 | 3 | *Telling user stories* 4 | 5 | Tests that use Capybara or similar libraries, and tests that use a headless 6 | browser, often have a slightly different format from the tests that we've 7 | written so far. 8 | 9 | These tests tell stories per test, they explain the features that our app has, 10 | and that our users care about. 11 | 12 | For example, for our `members` app, we explain: 13 | 14 | When I go to the members list, click the link "New member", fill in the name, 15 | and click submit, then I want the member details page for this new member to be 16 | shown, with a confirmation message. 17 | 18 | This is also called a user story. Tests that implement such stories are called 19 | feature tests. [1] 20 | 21 | We'll write some feature tests for our `members` app, and then later discuss 22 | the differences to the Rack::Test based tests that we've written for this app 23 | before. 24 | 25 | ## Specification 26 | 27 | Let's write down our user stories first. 28 | 29 | This way we can focus on them, and figure out the implementation later. Also, 30 | we'll know exactly how much work we still have in front of us. 31 | 32 | * Listing all members: When I go to the members list then I want to see a list 33 | of all members, linking to their details page, and links to edit and remove 34 | the member. 35 | 36 | * Showing member details: When I go to the members list, and I click on a 37 | member name, then I want the member's details page to be shown, displaying 38 | their name. 39 | 40 | * Creating a new member: When I go to the members list, click the link "New 41 | member", fill in the name, and click submit, then I want the member details 42 | page for this new member to be shown, with a confirmation message. 43 | 44 | * Editing a member: When I go to the members list, click the "Edit" link for a 45 | member, change their name, and click submit, then I want the member details 46 | page for this member to be shown, displaying their new name, with a 47 | confirmation message. 48 | 49 | * Removing a member: When I go to the members list, click the "Remove" link 50 | for a member, and confirm, then I want the members list to be shown, with 51 | the member removed, and a confirmation message. 52 | 53 | Does this make sense? 54 | 55 | We think this describes the functionality of our little application fairly 56 | well, from the perspective of a user. 57 | 58 | And unsurprisingly we have 5 stories. These correspond to the 5 groups of 59 | routes on a typical [resource](http://webapps-for-beginners.rubymonstas.org/resources/groups_routes.html). 60 | 61 | ## Test setup 62 | 63 | Ok, let's get started. 64 | 65 | We'll want our `spec_helper.rb` file to look like this. For now you can also 66 | just stick it to the top of your test file. Let's call it `feature_spec.rb`. 67 | 68 | ```ruby 69 | require "app" 70 | require 'capybara/dsl' 71 | require 'capybara/poltergeist' 72 | 73 | Capybara.default_driver = :poltergeist 74 | Capybara.app = proc { |env| App.new.call(env) } 75 | 76 | RSpec.configure do |config| 77 | config.include Capybara::DSL 78 | end 79 | ``` 80 | 81 | As you can see we need to require both Capybara's DSL and the Poltergeist 82 | driver. We then tell Capybara to use Poltergeist as a driver, and tell RSpec to 83 | include the DSL into our tests, so we can use these methods. 84 | 85 | The line `Capybara.app = proc { |env| App.new.call(env) }` tells Capybara 86 | how to call our app. This is the equivalent of the `let(:app)` statement in 87 | our Rack::Test based tests: The test libararies we use do not know about 88 | our app, and how to create or call it, so we have to tell them. 89 | 90 | Also, while we're at it, let's make sure our `members.txt` file does not 91 | leak state again, and add this to our configuration: 92 | 93 | ```ruby 94 | config.before { File.write("members.txt", "Anja\nMaren\n") } 95 | ``` 96 | 97 | We've had this in a `before` block in our tests before. Moving it to our 98 | general RSpec configuration is a sensible choice, too. This way we make 99 | sure that we don't forget about it. 100 | 101 | ## Test implementation 102 | 103 | Ok, now we're ready to write our first feature test. 104 | 105 | Remember how we've used `browser.visit` when we played with Capybara? 106 | The Capybara DSL that we've included to our RSpec tests allows us to 107 | just directly call these methods without using `browser`: 108 | 109 | ```ruby 110 | describe App do 111 | let(:links) { page.all('li').map { |li| li.all('a').map(&:text) } } 112 | 113 | it "listing members" do 114 | visit "/members" 115 | puts page.html 116 | end 117 | ``` 118 | 119 | Awesome, this outputs the HTML from our members `index` page, just as 120 | expected. 121 | 122 | Let's make sure that the links are all there. We'll just test for the 123 | link texts for now: 124 | 125 | ```ruby 126 | describe App do 127 | let(:links) { within('ul') { page.all('a').map(&:text) } } 128 | 129 | it "listing members" do 130 | visit "/members" 131 | expect(links).to eq ['Anja', 'Edit', 'Remove', 'Maren', 'Edit', 'Remove'] 132 | end 133 | ``` 134 | 135 | Ok, how do we look up those links there? 136 | 137 | `within` expects a CSS selector. In our case we select the `ul` tag. Inside 138 | that tag we then look for all `a` tags, and return the text (content) of each 139 | tag. I.e. we end up with an array that has the texts of all links in our `ul` 140 | tag. 141 | 142 | That seems like a good way to make sure all the links are there. We'll want 143 | to check their `href` attribute, too, but we can leave that for the following 144 | tests that will click these links. 145 | 146 | Hmmm, isn't that a little brittle though? What if we decide to change our 147 | HTML at some point, and not use a `ul` tag any more. Maybe we'd use a `table` 148 | or some other tag. Our app would still function the same, but our tests would 149 | now fail. 150 | 151 | Unfortunately our HTML does not give a lot of clues what's what. There's no 152 | way to identify the list of members, other than looking for the `ul` tag. 153 | 154 | One good way of dealing with this is to use a HTML `id`. In HTML an `id` is a 155 | unique identifier on a page, and it can be used to ... well, identify that 156 | element. 157 | 158 | So, let's change our `index.erb` view to add that `id`: 159 | 160 | ```erb 161 |
    162 | ... 163 |
164 | ``` 165 | 166 | That seems good. An `ul` is a list, and we call it `members`. Pretty 167 | straightforward. 168 | 169 | Now we can change our tests to look for the element with the `members` id. 170 | Since we're using a CSS selector here, `#members` selects our list. 171 | 172 | ```ruby 173 | let(:links) { within('#members') { page.all('a').map(&:text) } } 174 | ``` 175 | 176 | That's better. 177 | 178 | Let's implement the next story. 179 | 180 | ```ruby 181 | it "showing member details" do 182 | # go to the members list 183 | visit "/members" 184 | 185 | # click on the link 186 | click_on "Maren" 187 | 188 | # check the h1 tag 189 | expect(page).to have_css 'h1', text: 'Member: Maren' 190 | 191 | # check the name 192 | expect(page).to have_content 'Name: Maren' 193 | end 194 | ``` 195 | 196 | See the pattern? We go to the members list, click the respective link, and then 197 | we can assert that the page shows the contents we care about. 198 | 199 | We're using the `have_css` and `have_content` matchers here. Again, `have_css` 200 | expects a CSS selector, and `h1` simply selects the element with this tag name. 201 | We then also specify the content that we expect on this `h1` tag. There's no 202 | matcher for expecting various elements on the page at once, which is why we 203 | had to find and check the links on the members list manually in our first test. 204 | 205 | Ok, let's try the next story: 206 | 207 | ```ruby 208 | it "creating a new member" do 209 | # go to the members list 210 | visit "/members" 211 | 212 | # click on the link 213 | click_on "New Member" 214 | 215 | # fill in the form 216 | fill_in "name", :with => "Monsta" 217 | 218 | # submit the form 219 | find('input[type=submit]').click 220 | 221 | # check the current path 222 | expect(page).to have_current_path "/members/Monsta" 223 | 224 | # check the message 225 | expect(page).to have_content 'Successfully saved the new member: Monsta.' 226 | 227 | # check the h1 tag 228 | expect(page).to have_css 'h1', text: 'Member: Monsta' 229 | end 230 | ``` 231 | 232 | Woha. This actually works, our test passes. 233 | 234 | You see that, after navigating to the "new member" page, filling in the form, 235 | and submitting it, we can assert that we're now looking at the right path, and 236 | there's a confirmation message, and the right `h1` tag on the page. 237 | 238 | However, if you look closely, when we select the input tag to fill in, we need 239 | to use the actual name attribute of that input tag (which happens to be `name` 240 | in our case). 241 | 242 | We said we wanted to forumate tests in a way that they reflect what our users 243 | see, and care about, right? They don't see the name attribute of an input tag 244 | at all. 245 | 246 | Our test tells us something about our HTML here: There's no way for the user 247 | to know what the input field is for. 248 | 249 | Also, in the next line, we select the submit button with `find('input[type=submit]')`. 250 | This, again, is a CSS selector that selects an input tag that has the attribute 251 | `type` set to `submit`. 252 | 253 | That's the same problem, isn't it? The user does not see the `type` attribute, 254 | and they don't care about it. 255 | 256 | Let's fix that. 257 | 258 | In HTML the right way to name an input field is adding a `label` tag. A label 259 | tag has a `for` attribute that identifies the input field it is, well, for. 260 | This way software, i.e. browsers, screenreaders, but also our tests, can 261 | identify the field. 262 | 263 | Adding labels to form elements generally is a good idea in web development. 264 | So let's add a label to our `new.erb`, and `edit.erb` views: 265 | 266 | ```erb 267 | 268 | 269 | ``` 270 | 271 | You can see how the `for` attribute of the `label` tag relates to the `id` 272 | attribute of the `input` tag. Here is the specification for `for` on 273 | [MDN](https://developer.mozilla.org/en/docs/Web/HTML/Element/label#attr-for) 274 | (a great resource about all things HTML). 275 | 276 | While we're at it, let's also add a `value` attribute to the `submit` input tag 277 | in both forms. This tells the browser to display a certain value to the user. 278 | 279 | ```erb 280 | 281 | ``` 282 | 283 | Great. 284 | 285 | Now we can change our tests to use the actual texts that the user sees on the 286 | page: 287 | 288 | ```ruby 289 | # fill in the form 290 | fill_in "Name", :with => "Monsta" 291 | 292 | # submit the form 293 | click_on "Save" 294 | ``` 295 | 296 | Awesome. 297 | 298 | Our tests now really read like a story about our user's experience, except, 299 | maybe when we assert the current path. This one is debateable: 300 | 301 | On one hand users can see that path in the URL in their browser. On the other 302 | hand most users usually don't really care about it, and often don't look at it. 303 | 304 | We'll just keep this assertion because it helps us express in our tests which 305 | page we expect to be on. 306 | 307 | How about you go ahead and try to fill in the two remaining user stories. That 308 | seems like a great exercise at this point. 309 | 310 | Doing so you'll want to select a specific "Edit" link, and then later a "Remove" 311 | link. 312 | 313 | The Capybara [documentation](http://www.rubydoc.info/github/jnicklas/capybara/Capybara/Node/Actions#click_link_or_button-instance_method) 314 | does not mention this for some reason, but there's an option `match: :first` 315 | that you can pass to the `click_on` method (e.g. `click_on "Edit", match: 316 | :first`). This will click the first matching link. 317 | 318 | Have fun! 319 | 320 | ## Footnotes 321 | 322 | [1] In the short history of software development the semantics of testing, and 323 | terms for various kinds of tests, have changed a lot. If you ask 10 different 324 | developers to define the most important kinds of tests you'll probably get 10 325 | different lists of definitions. 326 | -------------------------------------------------------------------------------- /source/07-headless/04-test_types.md: -------------------------------------------------------------------------------- 1 | # Types of tests 2 | 3 | TBD 4 | 5 | ## One assertion per test 6 | 7 | You may have noticed that our tests so far, ever since we've written our own 8 | little `Test` class, had followed a certain rule: Every test tested one single 9 | thing. For example in the RSpec tests that test the `User` class, and it's 10 | `born_in_leap_year?` method every test tests the return value of that method in 11 | a given context. That's one assertion per test. 12 | 13 | Now with Capybara we have tested things by the way of using them, and then, 14 | eventually, made some assertions about the resulting page. 15 | 16 | For example, we make sure that a certain link is there by the way of clicking 17 | it, not using an actual assertion method. We then made sure that a certain 18 | input field with a label tag was on the page, by the way of filling it in. 19 | So while we were using the page we have made assumptions about the elements 20 | on the page. 21 | 22 | This way we've made several assertions in one single test. 23 | 24 | ## Using stubs vs real resources 25 | 26 | 27 | -------------------------------------------------------------------------------- /source/08-test_doubles.md: -------------------------------------------------------------------------------- 1 | # Test doubles 2 | 3 | *Faking all the things* 4 | 5 | Again, terminology about testing can be super confusing, and people have used 6 | the terms used in this chapter in various, different ways. Even more confusing, 7 | some test libraries use these terms in different ways, implementing different 8 | kinds of behaviour. 9 | 10 | For now we'll roll with the terms referenced by Martin Fowler in his famous 11 | article [Mocks Aren't Stubs](http://martinfowler.com/articles/mocksArentStubs.html), 12 | also referenced [here](http://stackoverflow.com/a/17810004/4130316). 13 | 14 | This defines "test double" as an umbrella term for the following four terms: 15 | 16 | * Mocks: Expectations about method calls, verifying them, and faking returning a value 17 | * Stubs: Fake responses to method calls 18 | * Fake: Objects with a working implementation that is useful for tests 19 | * Dummy: Usually not very relevant in Ruby testing 20 | 21 | Also, in Ruby, we might add this one to the list, instead of Dummy (which you 22 | don't really see that often): 23 | 24 | * Spies: Verifying that a stubbed method has been called before 25 | 26 | Ok, that's a lot of stuff. 27 | 28 | The two most commonly used techniqes are mocks and stubs. So let's focus on 29 | these first: 30 | 31 |

32 | Mocks and stubs are techniques that are used at the boundaries of the code 33 | under test. 34 |

35 | 36 | What? 37 | 38 | In our `members` application the boundaries of our app are the Rack interface 39 | on one side (which hooks into the server, and is called whenever a an actual 40 | HTTP request comes in). 41 | 42 | Most of the time, when we called our app in the `Rack::Test` based tests our 43 | Ruby class (or: Sinatra app) just checked some conditions, rendered some 44 | HTML, etc, and then returned a response object that we could test. 45 | 46 | However, doing so it then also talks to something external: It reads and writes 47 | to a text file that is stored on our hard drive. In other words, it uses an 48 | external resource. Something that is not specific to our test or application 49 | code (written in Ruby), but specific to the computer ("system") we're running 50 | these tests on. 51 | 52 | In most web applications this would be a database, not a text file. Sometimes 53 | it also would mean that we'd make HTTP requests to talk to another application. 54 | Or we might send emails, resize images or store PDF files, ... whatever our 55 | application needs to produce in order to do its job, besides returning a 56 | status code and some kind of HTML. 57 | 58 | Mocks and stubs are useful techniques in tests in exactly these places: At the 59 | boundary of our code and external "things". 60 | 61 | ## Stubs 62 | 63 | Let's see how that looks like in praxis. 64 | 65 | Imagine we'd want our code, when we test our `members` app to not actually 66 | touch the `members.txt` file for whatever reason. Instead we'd like to fake 67 | reading and writing to the file. 68 | 69 | How can we do that? 70 | 71 | RSpec has a built-in library called [rspec-mocks](https://www.relishapp.com/rspec/rspec-mocks/docs). 72 | There are a bunch of [other libraries](https://www.ruby-toolbox.com/categories/mocking) 73 | that provide similar functionality, most notably [Mocha](https://github.com/freerange/mocha) 74 | which is really popular, too. We'll just use RSpec for now. 75 | 76 | Consider our test code from before: 77 | 78 | ```ruby 79 | describe App do 80 | let(:app) { App.new } 81 | before { File.write('members.txt', "Anja\nMaren\n") } 82 | 83 | context "GET to /members/:name" do 84 | let(:response) { get "/members/Anja" } 85 | 86 | it "displays the member's name" do 87 | expect(response.body).to have_tag(:p, :text => "Name: Anja") 88 | end 89 | end 90 | end 91 | ``` 92 | 93 | This code, under the hood, will read the file `members.txt`. Doing so it talks 94 | to an "external system", i.e. our computer's operating system, in order to 95 | access the file on the harddrive. 96 | 97 | This method making this call might look like this: 98 | 99 | ```ruby 100 | def names 101 | File.read(FILENAME).split("\n") 102 | end 103 | ``` 104 | 105 | Now our objective is to make it so that the method call `File.read` is not 106 | actually executed, but faked, and returns a value that we expect: some fake 107 | content. 108 | 109 | The RSpec documentation gives the following example about setting up stubs with 110 | RSpec: 111 | 112 | ```ruby 113 | allow(dbl).to receive(:foo).with(1).and_return(14) 114 | ``` 115 | 116 | Ok, so we "allow" an object (`dbl`) to "receive" a certain method call 117 | (`:foo`), with a certain argument (`1`), and return a certain value (`14`). 118 | 119 | That sounds like what we want. Let's try this: 120 | 121 | ```ruby 122 | RSpec.configure do |config| 123 | config.mock_with :rspec # use rspec-mocks 124 | end 125 | 126 | describe App do 127 | let(:app) { App.new } 128 | 129 | before do 130 | allow(File).to receive(:read).with("members.txt").and_return("Anja\nMaren\n") 131 | end 132 | 133 | context "GET to /members/:name" do 134 | let(:response) { get "/members/Anja" } 135 | 136 | it "displays the member's name" do 137 | expect(response.body).to have_tag(:p, :text => "Name: Anja") 138 | end 139 | end 140 | end 141 | ``` 142 | 143 | If you run this this test should pass. If you delete the file `members.txt` it 144 | still passes. 145 | 146 | What's going on here? 147 | 148 | Under the hood, RSpec leverages Ruby's powerful capabilities in modifying, 149 | replacing, and re-defining code at runtime. 150 | 151 | Let's walk through it. 152 | 153 | * In the before block, when RSpec has started executing our code, it replaces 154 | the method `read` with *another* method that, if it is passed the argument 155 | specified (in our case: the filename), will return the return value specified 156 | (the string that is our fake file content). 157 | 158 | * When it then executes the test, calls our application, and our application 159 | calls the method `names`, it will call this fake ("stubbed") method 160 | `File.read?` instead of the original, real method that actually reads a file 161 | on the harddrive. The fake method will do nothing but return the value 162 | we specified: `"Anja\nMaren\n"` 163 | 164 | * So to our application everything looks as if it was talking to the operating 165 | system, and looking at actual files on the harddrive, while actually it 166 | just calls fake methods that return the fake values we specified. Therefore 167 | the application functions just the same, and our tests passs, except it's not 168 | talking to the external system that is our computer at all at this point. 169 | 170 | * After the test has run RSpec will then remove these fake methods, so that 171 | other tests (in other contexts that do not have this `before` block) could 172 | talk to the original, "real" method `File.read` again. 173 | 174 | Wow. This is quite a bit of stuff to digest. 175 | 176 | The core idea is that, when we `allow` an object to `receive` a method, RSpec 177 | will create this fake method for the time the test runs, and it will remove 178 | it again at the end. 179 | 180 | Now, this is called "stubbing" a method. We replace it with a fake method, 181 | so that we don't have to talk to an external system. 182 | 183 | Btw if we would make a mistake, and stub the method call with the wrong arguments, 184 | then RSpec would fail, and display an error like this: 185 | 186 | ``` 187 | RSpec::Mocks::MockExpectationError at members 188 | File (class) received :read with unexpected arguments 189 | expected: ("members.text") 190 | got: ("members.txt") 191 | Please stub a default value first if message might be received with other args as well. 192 | ``` 193 | 194 | Because we've "allowed" the method to be called with certain arguments, but 195 | not otherwise. 196 | 197 | Some would argue that this is an implicit expectation or assertion, and that 198 | stubs shouldn't actuall assert anything, but this is how RSpec stubs work. 199 | 200 | ## Mocks 201 | 202 | Mocks on the other hand are pretty similar, but also very different. 203 | 204 | Mocks are there to make assertions about methods being called during your 205 | tests. 206 | 207 | They work pretty much the same in that RSpec replaces the original method with 208 | a fake method, and you can specify arguments as well as a return value. 209 | 210 | However, RSpec will also record how often your method has been called during 211 | your test. And at the end of the test it will not only remove this fake method 212 | again, but also verify that the method has been called the expected number of 213 | times (usually once) with the expected arguments. 214 | 215 | Here's how that looks like in RSpec: 216 | 217 | ```ruby 218 | describe App do 219 | let(:app) { App.new } 220 | 221 | context "GET to /members/:name" do 222 | let(:response) { get "/members/Anja" } 223 | 224 | it "displays the member's name" do 225 | expect(File).to receive(:read).with("members.txt").and_return("Anja\nMaren\n") 226 | get "/members" 227 | end 228 | end 229 | end 230 | ``` 231 | 232 | This is called "mocking" a method: expecting and asserting that the method will 233 | be called later. 234 | 235 | Again, if we run this spec, but we make a mistake with the argument that we 236 | expect to be passed (e.g. we have a typo `members.text`), then our tests will 237 | fail: 238 | 239 | ``` 240 | $ rspec -I . app_spec.rb:15 241 | Run options: include {:locations=>{"./app_spec.rb"=>[15]}} 242 | F 243 | 244 | Failures: 245 | 246 | 1) App GET to /members/:name displays the member's name 247 | Failure/Error: expect(File).to receive(:read).with("members.text").and_return("Anja\nMaren\n") 248 | 249 | (File (class)).read("members.text") 250 | expected: 1 time with arguments: ("members.text") 251 | received: 0 times 252 | # ./app_spec.rb:16:in `block (3 levels) in ' 253 | 254 | Finished in 0.08187 seconds (files took 0.72415 seconds to load) 255 | 1 example, 1 failure 256 | 257 | Failed examples: 258 | 259 | rspec ./app_spec.rb:15 # App GET to /members/:name displays the member's name 260 | ``` 261 | 262 | That makes sense, doesn't it? The method hasn't been called with these 263 | arguments after all. 264 | 265 | As you can see the workflow with mocked methods is a little different from 266 | the workflow we've seen in our tests so far: 267 | 268 | With mocked methods you have to set up your expectation *first*, then run 269 | the actual code, and then RSpec will verify your expectation at the end. 270 | 271 | Therefore we need to call `expect(File).to receive(:read)` first, and then 272 | make the get request in our test above. 273 | 274 | ## Spies 275 | 276 | RSpec therefore has another way to achieve the same, which is called a 277 | "spying". [1] 278 | 279 | Here's how method "spying" works in RSpec: 280 | 281 | ```ruby 282 | describe App do 283 | let(:app) { App.new } 284 | 285 | context "GET to /members/:name" do 286 | let(:response) { get "/members/Anja" } 287 | let(:filename) { "members.txt" } 288 | let(:content) { "Anja\nMaren\n" } 289 | 290 | before { allow(File).to receive(:read).with(filename).and_return(content) } 291 | 292 | it "displays the member's name" do 293 | get "/members" 294 | expect(File).to have_received(:read).with(filename) 295 | end 296 | end 297 | end 298 | ``` 299 | 300 | As you can see that "fixes" the order: Our test first runs the get request, 301 | and then verifies that the method `File.read` has been called with the expected 302 | argument. 303 | 304 | However, in order for that to work, we also have to stub the method in the 305 | `before` block first. Otherwise RSpec would not have had the opportunity to 306 | record calls to this method, and therefore raised an error like this: 307 | 308 | ``` 309 | Failure/Error: expect(File).to have_received(:read).with(filename) 310 | # expected to have received read, but that object is not a spy or method has not been stubbed. 311 | ``` 312 | 313 | To summarize: 314 | 315 | * Stubbing and mocking methods replaces the original methods temporarily. 316 | * Stubbing a method replaces it in order to fake it, and allow it to be called 317 | without executing the original, "real" method. This can be useful if we want 318 | our tests to not talk to external systems or code we do not want to test at 319 | the moment. 320 | * Mocking a method asserts that the method actually is being called during our 321 | tests. 322 | * Spying on a method (in RSpec) means verifying that a stubbed method has been 323 | called after the fact. 324 | 325 | ## Double objects 326 | 327 | So far we've talked about replacing methods with fake methods that we'd either 328 | allow or expect to be called during our tests. 329 | 330 | Sometimes it is useful to have entire fake objects that can be passed around. 331 | 332 | Consider this code from our [Ruby for Beginners]() book: 333 | 334 | ```ruby 335 | class Person 336 | def initialize(name) 337 | @name = name 338 | end 339 | 340 | def name 341 | @name 342 | end 343 | 344 | def greet(other) 345 | "Hi " + other.name + "! My name is " + name + "." 346 | end 347 | end 348 | 349 | person = Person.new("Anja") 350 | friend = Person.new("Carla") 351 | 352 | puts person.greet(friend) 353 | puts friend.greet(person) 354 | ``` 355 | 356 | Let's say we want to test that in RSpec, instead of just trying it out at the 357 | end of the file. 358 | 359 | We could turn this into tests like so: 360 | 361 | ```ruby 362 | describe Person do 363 | let(:person) { Person.new("Anja") } 364 | let(:friend) { Person.new("Carla") } 365 | 366 | describe "greet" do 367 | it "returns a greeting" do 368 | expect(person.greet(friend)).to eq "Hi Carla! My name is Anja." 369 | end 370 | end 371 | end 372 | ``` 373 | 374 | Now, imagine that, for whatever reason, it is really expensive or cumbersome to 375 | create the `friend` instance. 376 | 377 | In that case it would be useful to be able to quickly create a fake object (a 378 | "double"), and allow the method `name` to be called on it: That's the only 379 | method the method `person.greet` needs to call on the `other` object, right? 380 | 381 | RSpec has a convient way of creating such fake objects (doubles): 382 | 383 | ```ruby 384 | describe Person do 385 | let(:person) { Person.new("Anja") } 386 | let(:friend) { double(name: "Carla") } 387 | 388 | describe "greet" do 389 | it "returns a greeting" do 390 | expect(person.greet(friend)).to eq "Hi Carla! My name is Anja." 391 | end 392 | end 393 | end 394 | ``` 395 | 396 | So, we do create a `Person` instance for the `person`, so we can actually call 397 | the method `greet` on it. However, we do not create a second instance of the 398 | same class. In the end, the method `greet` does not care what kind of object 399 | is passed as `other`, as long as it responds to the method `name`, right? [2] 400 | 401 | And yes, this test passes, too. Pretty cool. 402 | 403 | ## When to use doubles 404 | 405 | Ok, this is all pretty interesting stuff. But how do you know when to use 406 | stubs, mocks, spies, or fake objects? 407 | 408 | As always, the answer clearly is, it depends. There are rarely any very clear 409 | answers. 410 | 411 | If your application talks to an external API (such as Twitter for signing in 412 | users via OAuth, or GitHub for fetching some code that you'd like to inspect) 413 | then that would be a very clear case. You definitely don't want your tests 414 | to make any HTTP calls to an external API: not only is that super slow, but 415 | it also would mean that you cannot work on your tests when your offline. 416 | 417 | Generally, when talking to any external system is problematic, then that's 418 | a good indication that you'd want to use a stub to fake that call. 419 | 420 | Whether you need to also assert the call being made, i.e. when using a mock is 421 | a good idea, is an entirely different question, and it is one that has been 422 | debated for years. 423 | 424 | Consider our tests from above: 425 | 426 | ```ruby 427 | describe Person do 428 | let(:person) { Person.new("Anja") } 429 | let(:friend) { double(name: "Carla") } 430 | 431 | describe "greet" do 432 | it "returns a greeting" do 433 | expect(person.greet(friend)).to eq "Hi Carla! My name is Anja." 434 | end 435 | end 436 | end 437 | ``` 438 | 439 | Our test does in no way verify that the method `name` actually has been 440 | called on the fake `friend` object. What if we've accidentally left a hardcoded 441 | value in our implementation, like so: 442 | 443 | ```ruby 444 | class Person 445 | def greet(other) 446 | "Hi Carla! My name is " + name + "." 447 | end 448 | end 449 | ``` 450 | 451 | Hmm, ok, that could happen. If we are concerned about this case then we 452 | could verify the method call with a mock like so: 453 | 454 | ```ruby 455 | it "calls name on the other person" do 456 | expect(friend).to receive(:name).and_return("Carla") 457 | person.greet(friend) 458 | end 459 | ``` 460 | 461 | Or we could use a spy (this works fine because `friend` is a double, and 462 | already has the method `name` stubbed): 463 | 464 | ```ruby 465 | it "calls name on the other person" do 466 | person.greet(friend) 467 | expect(friend).to have_receive(:name).and_return("Carla") 468 | end 469 | ``` 470 | 471 | Whether or not you want to add such tests to your test suite depends on many 472 | factors. 473 | 474 | In the end tests are there to make yourself (and your co-workers) feel 475 | comfortable making changes to your code. When you run your tests you want to 476 | feel safe enough to publish your code and not break anything (to your 477 | production system, to your friends, to the open source community). 478 | 479 | "Comfortable" is a very personal thing though. It depends on your personality, 480 | experience, on your team, and everyone's views and gutfeelings. 481 | 482 | This is true for testing in general, but it's also particularly true when 483 | it comes to the question how much to test. 484 | 485 | Remember tests are software, too. They also can have bugs, and cause making 486 | changes hard, when you write too many, too detailed tests. On the other hand, 487 | if you have too few tests, or test the wrong things, you might introduce a bug, 488 | and cause yourself even more work fixing it. So it's a tradeoff, as always. 489 | 490 | Over time, the more tests your write, you'll develop a good feeling, and your 491 | own views. Maybe you'll work on different teams that have different conventions 492 | for what to test, and how. It also helps to ask more experienced developers 493 | about their views. Again, be prepared to get 10 different answers when you 494 | ask 10 different people :) 495 | 496 | Katrina Owen has given an amazing talk titled "467 tests, 0 failures, 0 497 | confidence" on the differences between mocks and stubs, and when to use what, 498 | at Railsberry 2013. You can watch it on Vimeo 499 | [here](https://vimeo.com/68730418), and have a look at her slides 500 | [here](https://speakerdeck.com/railsberry/zero-confidence-by-katrina-owen). 501 | Check it out: 502 | 503 | 504 | 505 | ## Footnotes 506 | 507 | [1] Spying on methods is used in various different ways, depending on the 508 | library used. In the most popular Ruby libraries (such as 509 | [RSpec](http://www.relishapp.com/rspec/rspec-mocks/v/3-5/docs/basics/spies), 510 | [RR](http://technicalpickles.com/posts/ruby-stubbing-and-mocking-with-rr/), 511 | [FlexMock](https://github.com/jimweirich/flexmock#spies)) "spying" refers to 512 | the technique of verifying that a previously *stubbed* method actually has been 513 | called, after the fact. In other contexts it means a technique that leaves the 514 | original method in place, allows it to be called, but records the method call 515 | so it can be verified later. Both techniques are similar, but also very 516 | different in that the original method either needs to be stubbed (replaced) 517 | first or can still be used. 518 | 519 | [2] You've probably heard about the term "duck typing" at some point. This term 520 | refers to the fact that in Ruby methods don't care what kind of objects are 521 | being passed, as long as they behave in a certain way (respond to certain other 522 | methods): As long as it walks like a duck, and quacks like a duck, ... 523 | -------------------------------------------------------------------------------- /source/09-analysis.md: -------------------------------------------------------------------------------- 1 | # Code Analysis 2 | 3 | TBD ... talk about code coverage, complexity analysis etc 4 | -------------------------------------------------------------------------------- /source/10-services.md: -------------------------------------------------------------------------------- 1 | # Services 2 | 3 | TBD talk about services, such as Travis CI, Circle CI, Codeship, Codeclimate, etc. 4 | -------------------------------------------------------------------------------- /source/CNAME: -------------------------------------------------------------------------------- 1 | testing-for-beginners.rubymonstas.org 2 | -------------------------------------------------------------------------------- /source/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubymonsters/testing-for-beginners/51eebe6d7adb35510aeb18b73e4d5a680529fa67/source/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /source/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubymonsters/testing-for-beginners/51eebe6d7adb35510aeb18b73e4d5a680529fa67/source/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /source/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubymonsters/testing-for-beginners/51eebe6d7adb35510aeb18b73e4d5a680529fa67/source/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /source/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubymonsters/testing-for-beginners/51eebe6d7adb35510aeb18b73e4d5a680529fa67/source/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /source/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubymonsters/testing-for-beginners/51eebe6d7adb35510aeb18b73e4d5a680529fa67/source/assets/images/favicon.png -------------------------------------------------------------------------------- /source/assets/javascripts/modernizr.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.6.2 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-borderradius-boxshadow-multiplebgs-opacity-textshadow-cssgradients-shiv-cssclasses-teststyles-testprop-testallprops-prefixes-domprefixes 3 | */ 4 | ;window.Modernizr=function(a,b,c){function z(a){j.cssText=a}function A(a,b){return z(m.join(a+";")+(b||""))}function B(a,b){return typeof a===b}function C(a,b){return!!~(""+a).indexOf(b)}function D(a,b){for(var d in a){var e=a[d];if(!C(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function E(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:B(f,"function")?f.bind(d||b):f}return!1}function F(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+o.join(d+" ")+d).split(" ");return B(b,"string")||B(b,"undefined")?D(e,b):(e=(a+" "+p.join(d+" ")+d).split(" "),E(e,b,c))}var d="2.6.2",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n="Webkit Moz O ms",o=n.split(" "),p=n.toLowerCase().split(" "),q={},r={},s={},t=[],u=t.slice,v,w=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},x={}.hasOwnProperty,y;!B(x,"undefined")&&!B(x.call,"undefined")?y=function(a,b){return x.call(a,b)}:y=function(a,b){return b in a&&B(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=u.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(u.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(u.call(arguments)))};return e}),q.multiplebgs=function(){return z("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},q.borderradius=function(){return F("borderRadius")},q.boxshadow=function(){return F("boxShadow")},q.textshadow=function(){return b.createElement("div").style.textShadow===""},q.opacity=function(){return A("opacity:.55"),/^0.55$/.test(j.opacity)},q.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return z((a+"-webkit- ".split(" ").join(b+a)+m.join(c+a)).slice(0,-a.length)),C(j.backgroundImage,"gradient")};for(var G in q)y(q,G)&&(v=G.toLowerCase(),e[v]=q[G](),t.push((e[v]?"":"no-")+v));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)y(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},z(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=m,e._domPrefixes=p,e._cssomPrefixes=o,e.testProp=function(a){return D([a])},e.testAllProps=F,e.testStyles=w,g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+t.join(" "):""),e}(this,this.document); -------------------------------------------------------------------------------- /source/assets/javascripts/monstas.js: -------------------------------------------------------------------------------- 1 | if(location.hostname == 'rubymonstas.org') { 2 | var host = 'http://ruby-for-beginners.rubymonstas.org'; 3 | var path = location.pathname.replace('ruby-for-beginners/', ''); 4 | location.href = host + path; 5 | } 6 | 7 | window.onload = function() { 8 | var ajax = function(method, path) { 9 | var xhr = new XMLHttpRequest(); 10 | xhr.open(method.toUpperCase(), path, false); 11 | xhr.send(null); 12 | return xhr.responseText; 13 | }; 14 | 15 | var addEventListener = function(el, eventType, handler) { 16 | if (el.addEventListener) { // DOM Level 2 browsers 17 | el.addEventListener(eventType, handler, false); 18 | } else if (el.attachEvent) { // IE <= 8 19 | el.attachEvent('on' + eventType, handler); 20 | } else { // ancient browsers 21 | el['on' + eventType] = handler; 22 | } 23 | }; 24 | 25 | var cancelDefaultAction = function(e) { 26 | var event = event ? event : window.event; 27 | if (event.preventDefault) { event.preventDefault(); } 28 | event.returnValue = false; 29 | return false; 30 | }; 31 | 32 | var hasClassName = function(element, name) { 33 | return new RegExp("(?:^|\\s+)" + name + "(?:\\s+|$)").test(element.className); 34 | } 35 | 36 | var addClassName = function(element, name) { 37 | if (hasClassName(element, name)) return; 38 | element.className = element.className ? [element.className, name].join(' ') : name; 39 | }; 40 | 41 | var removeClassName = function(element, name) { 42 | if (!hasClassName(element, name)) return; 43 | var c = element.className; 44 | element.className = c.replace(new RegExp("(?:^|\\s+)" + name + "(?:\\s+|$)", "g"), ""); 45 | }; 46 | 47 | var toggleClassName = function(element, name) { 48 | if (hasClassName(element, name)) { 49 | removeClassName(element, name); 50 | } else { 51 | addClassName(element, name); 52 | } 53 | }; 54 | 55 | var clickMenu = function() { 56 | var toc = document.getElementsByClassName('toc')[0]; 57 | var page_nav = document.getElementsByClassName('page-nav'); 58 | 59 | toggleClassName(toc, 'overlay'); 60 | 61 | for(var i = 0; i < page_nav.length; i++) { 62 | toggleClassName(page_nav[i], 'hidden'); 63 | } 64 | }; 65 | 66 | var nav = document.getElementsByClassName('menu')[0]; 67 | var link = nav.firstChild; 68 | addEventListener(link, 'click', clickMenu); 69 | 70 | var showSolution = function(link) { 71 | addClassName(link, 'hidden'); 72 | 73 | var solution = ajax('get', link.href); 74 | var id = link.href.split('/').pop().replace('.rb', ''); 75 | var hide = 'Hide solution'; 76 | /* var pre = '
' + solution + '
'; */ 77 | solution = solution.replace(' div { 2 | display: -webkit-flex; 3 | display: flex; 4 | flex-direction: row; 5 | flex-wrap: nowrap; 6 | margin: 0 auto; 7 | max-width: 80rem; 8 | padding: 3rem 6.250rem; 9 | } 10 | 11 | article { 12 | position: relative; 13 | display: -webkit-flex-item; 14 | display: flex-item; 15 | max-width: 55rem; 16 | vertical-align: top; 17 | 18 | h1:first-of-type { 19 | margin-top: 0; 20 | } 21 | } 22 | 23 | .toc { 24 | display: -webkit-flex-item; 25 | display: flex-item; 26 | position: relative; 27 | padding: 1rem 4rem 0 0; 28 | vertical-align: top; 29 | font-size: 0.875rem; 30 | 31 | ul { 32 | margin: 0; 33 | padding: 0; 34 | } 35 | 36 | ul ul { 37 | margin: 0.5rem 0 0 1rem; 38 | } 39 | 40 | li { 41 | line-height: 1.2rem; 42 | padding: 0 0 0.5rem 0; 43 | } 44 | li:last-of-type { 45 | padding: 0; 46 | } 47 | 48 | a, span { 49 | display: inline-block; 50 | width: 100%; 51 | overflow: hidden; 52 | white-space: nowrap; 53 | text-overflow: ellipsis; 54 | } 55 | 56 | h1 { 57 | display: none; 58 | } 59 | 60 | &.overlay { 61 | display: block; 62 | position: absolute; 63 | z-index: 1; 64 | top: 7em; 65 | width: 100%; 66 | background-color: white; 67 | 68 | h1 { 69 | display: block; 70 | } 71 | } 72 | } 73 | 74 | body > .page-nav { 75 | position: fixed; 76 | display: table; 77 | height: 100%; 78 | width: 6.250em; 79 | 80 | &:first-of-type { left: 0; } 81 | &:last-of-type { right: 0; } 82 | 83 | a { 84 | display: table-cell; 85 | height: 100%; 86 | width: 100%; 87 | 88 | vertical-align: middle; 89 | font-family: 'FontAwesome'; 90 | font-size: 0; 91 | color: #ccc; 92 | text-decoration: none; 93 | text-align: center; 94 | overflow: hidden; 95 | 96 | &:hover:before { 97 | color: #444; 98 | } 99 | 100 | &.prev:before { 101 | content: "\f104"; 102 | left: 0; 103 | font-size: 5rem; 104 | } 105 | &.next:before { 106 | content: "\f105"; 107 | right: 0; 108 | font-size: 5rem; 109 | } 110 | } 111 | 112 | &.hidden { 113 | display: none; 114 | } 115 | } 116 | 117 | article .page-nav { 118 | margin-top: 2rem; 119 | 120 | a { 121 | position: absolute; 122 | display: inline-block; 123 | height: 40px; 124 | line-height: 40px; 125 | background-color: $red; 126 | color: white; 127 | text-decoration: none; 128 | text-align: center; 129 | 130 | &:before, &:after { 131 | content: ""; 132 | position: absolute; 133 | top: 0; 134 | width: 0; 135 | height: 0; 136 | border: 0 solid $red; 137 | } 138 | 139 | &.prev { 140 | margin: 0 20px 0 15px; 141 | padding: 0 5px 0 17px; 142 | 143 | &:before { 144 | left: -30px; 145 | border-color: rgba(0, 0, 0, 0); 146 | border-right-color: $red; 147 | border-width: 20px 15px; 148 | } 149 | &:after { 150 | right: -20px; 151 | border-width: 20px 10px; 152 | border-radius: 0 4px 4px 0; 153 | } 154 | } 155 | 156 | &.next { 157 | right: 0; 158 | margin: 0 30px 0 20px; 159 | padding: 0 17px 0 5px; 160 | 161 | &:before { 162 | left: -20px; 163 | border-width: 20px 10px; 164 | border-radius: 4px 0 0 4px; 165 | } 166 | 167 | &:after { 168 | right: -30px; 169 | border-color: rgba(0, 0, 0, 0); 170 | border-left-color: $red; 171 | border-width: 20px 15px; 172 | } 173 | } 174 | } 175 | } 176 | 177 | .menu { 178 | position: relative; 179 | margin-bottom: 3em; 180 | } 181 | 182 | .menu a { 183 | padding-left: 1.25rem; 184 | } 185 | 186 | .menu a:before { 187 | content: ""; 188 | position: absolute; 189 | top: 30%; 190 | left: 0px; 191 | width: 36px; 192 | height: 28px; 193 | border-top: 17px double #000; 194 | border-bottom: 6px solid #000; 195 | border-color: #ccc; 196 | } 197 | 198 | footer { 199 | padding: 2rem 0 3rem 0; 200 | text-align: center; 201 | } 202 | 203 | footer ul { 204 | display: inline-block; 205 | margin: 0; 206 | } 207 | 208 | footer li { 209 | display: inline-block; 210 | padding: 0 1rem; 211 | font-size: 0.9rem; 212 | } 213 | 214 | footer a { 215 | color: #999; 216 | text-decoration: none; 217 | } 218 | 219 | .video { 220 | margin-top: 1em; 221 | width: 55rem; 222 | height: calc(55rem * 0.5625); 223 | } 224 | 225 | -------------------------------------------------------------------------------- /source/assets/stylesheets/_response.scss: -------------------------------------------------------------------------------- 1 | $tablet-1: 700px; 2 | $tablet-2: 900px; 3 | $tablet-3: 1024px; 4 | $desktop: 1200px; 5 | 6 | $font-s: 0.875rem; // 14px 7 | $font-m: 1rem; // 16px 8 | $font-l: 1.125rem; // 18px 9 | 10 | html { 11 | font-size: $font-s; 12 | @media screen and (min-width: $tablet-2) { font-size: $font-s; } 13 | @media screen and (min-width: $tablet-3) { font-size: $font-m; } 14 | @media screen and (min-width: $desktop) { font-size: $font-l; } 15 | } 16 | 17 | @media screen and (max-width: $tablet-2) { 18 | body > .page-nav { display: none; } 19 | article > .page-nav { display: block; } 20 | body > div { padding: 3rem 3rem 6.250rem 3rem; } 21 | .menu { display: block; } 22 | .toc { display: none; } 23 | .video { 24 | width: 50rem; 25 | height: calc(50rem * 0.5625); 26 | } 27 | } 28 | 29 | @media screen and (min-width: $tablet-2) { 30 | article > .page-nav { display: none; } 31 | .menu { display: none; } 32 | .video { 33 | width: 50rem; 34 | height: calc(50rem * 0.5625); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /source/assets/stylesheets/_style.scss: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:700,600,400); 2 | 3 | ::-moz-selection { color: white; background: $red; text-shadow: none; } 4 | ::selection { color: white; background: $red; text-shadow: none; } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | -moz-box-sizing: border-box; 10 | -webkit-box-sizing: border-box; 11 | box-sizing: border-box; 12 | } 13 | 14 | html, body { 15 | height: 100%; 16 | width: 100%; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | body { 22 | position: relative; 23 | background: #fff; 24 | font-family: 'Open Sans', 'Helvetica Neue', helvetica, sans-serif; 25 | line-height: 1.5; 26 | color: #333; 27 | text-shadow: 1px 1px 1px rgba(0,0,0,0.004); 28 | -webkit-font-smoothing: antialiased; 29 | } 30 | 31 | h1, h2, h3, h4, h5, h6 { 32 | font-weight: normal; 33 | margin: 1rem 0; 34 | } 35 | h1 { font-size: 2.750rem; margin-bottom: 2rem; } // 36px 36 | h2 { font-size: 1.50rem; } // 24px 37 | h3 { font-size: 1.313rem; } // 21px 38 | h4 { font-size: 1.125rem; font-weight: bold; } // 18px 39 | 40 | p, ul, ol, pre { 41 | margin-top: 1rem; 42 | } 43 | 44 | ul, ol { 45 | list-style-type: disc; 46 | padding-left: 2rem; 47 | } 48 | 49 | a { 50 | color: $red; 51 | text-decoration: underline; 52 | &:hover { text-decoration: none; } 53 | } 54 | 55 | code { 56 | text-align: left; 57 | padding: 0.2rem 0 0.2rem 0; 58 | margin: 0; 59 | background-color: #f7f7f7; 60 | border-radius: 3px; 61 | font-size: 80%; 62 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 63 | } 64 | 65 | pre { 66 | overflow: auto; 67 | margin: 1.2rem 0; 68 | padding: 5px 0 5px 1.5em; 69 | line-height: 160%; 70 | background-color: #f7f7f7; 71 | border-radius: 3px; 72 | white-space: pre; 73 | word-wrap: normal; 74 | 75 | & > code { 76 | padding: 0; 77 | background: none; 78 | 79 | &:before, &:after { 80 | content: ""; 81 | } 82 | } 83 | } 84 | 85 | code:before, code:after { 86 | letter-spacing: -0.2rem; 87 | content: "\00a0"; 88 | } 89 | 90 | table { 91 | margin-top: 1em; 92 | width: 100%; 93 | border-collapse: collapse; 94 | 95 | th { 96 | border: 1px solid #ddd; 97 | text-align: left; 98 | } 99 | 100 | td, th { 101 | border: 1px solid #ddd; 102 | padding: 0.5em 1em 0.5em 1em; 103 | } 104 | } 105 | 106 | .toc { 107 | a, span { 108 | text-decoration: none; 109 | color: #999; 110 | } 111 | 112 | span { 113 | font-weight: bold; 114 | } 115 | 116 | li ul { 117 | display: none; 118 | } 119 | 120 | li.expanded ul { 121 | display: block; 122 | } 123 | 124 | li.active > a { 125 | color: $red; 126 | } 127 | 128 | li.directory:before { 129 | content: '+'; 130 | position: absolute; 131 | left: -1rem; 132 | color: #ccc; 133 | } 134 | 135 | li.directory.expanded:before { 136 | content: '-'; 137 | } 138 | 139 | a:hover { 140 | text-decoration: underline; 141 | color: $red; 142 | } 143 | } 144 | 145 | .solution { 146 | font-size: 0.8rem; 147 | color: #999; 148 | text-decoration: none; 149 | } 150 | 151 | .credits { 152 | margin-top: 3rem; 153 | font-size: 0.8rem; 154 | color: #999; 155 | } 156 | 157 | .hidden { 158 | display: none; 159 | } 160 | 161 | .hint { 162 | position: relative; 163 | padding: 1rem 1rem 1rem 3.6rem; 164 | background-color: #FFFFE0; 165 | &:before { 166 | content: "\f0eb"; 167 | position: absolute; 168 | display: block; 169 | top: 0; 170 | left: 0; 171 | height: 100%; 172 | padding: 1rem; 173 | font-size: 1.25rem; 174 | font-family: "FontAwesome"; 175 | color: #666; 176 | background-color: #FFF9D8; 177 | } 178 | } 179 | 180 | // overwrite the default from syntax because we want to use bash prompts `$`, 181 | // but still also highlight as Ruby code 182 | .highlight .err { 183 | color: #000; 184 | font-weight: normal; 185 | background-color: transparent; 186 | } 187 | -------------------------------------------------------------------------------- /source/assets/stylesheets/monstas.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | $light-gray: #F2F2F2; 4 | $red: #ee4444; 5 | 6 | @import "fonts"; 7 | @import "html5-reset"; 8 | @import "syntax"; 9 | @import "layout"; 10 | @import "style"; 11 | @import "response"; 12 | -------------------------------------------------------------------------------- /source/assets/stylesheets/syntax.css.erb: -------------------------------------------------------------------------------- 1 | <%# A good day theme, though not that colorful %> 2 | <%= Rouge::Themes::Github.render(:scope => '.highlight') %> 3 | 4 | <%# A good day theme, and is very colorful %> 5 | <%#= Rouge::Themes::Base16.render(:scope => '.highlight') %> 6 | 7 | <%# That good sublime text night theme%> 8 | <%#= Rouge::Themes::Base16::Monokai.render(:scope => '.highlight') %> 9 | -------------------------------------------------------------------------------- /source/index.md: -------------------------------------------------------------------------------- 1 | # Testing For Beginners 2 | 3 | *[Ruby Monday Study Group](http://rubymonstas.org) curriculum for advanced intermediates* 4 | 5 | This book teaches the basics of testing software. 6 | 7 | It has been written after we have run several groups at our [Ruby 8 | Monstas](http://rubymonstas.org) groups in Berlin over the course of 4 years. 9 | These groups start as beginners groups, learning [Ruby](http://ruby-for-beginners.rubymonstas.org/), 10 | then turn into intermediate groups learning the basics of building 11 | [web applications](http://webapps-for-beginners.rubymonstas.org/). 12 | They normally then start building their first Rails application as 13 | a group, and get in touch with the concept of testing. 14 | 15 | After completing this book you'll have a basic understanding of: 16 | 17 | * Why writing tests is a good thing. 18 | * What concepts are relevant for writing tests. 19 | * What libraries exist for making writing tests easy. 20 | * What services can be used to help with your tests and code quality. 21 | 22 | You can find the source code of this book [here](https://github.com/rubymonsters/testing-for-beginners). 23 | 24 | -------------------------------------------------------------------------------- /source/layouts/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= discover_page_title %> 14 | 15 | <%= stylesheet_link_tag "/assets/stylesheets/monstas" %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 28 |
29 | 30 | <%= yield %> 31 | 32 | 33 |
34 |
35 | 36 | 49 | 50 | <%= javascript_include_tag "/assets/javascripts/modernizr" %> 51 | <%= javascript_include_tag "/assets/javascripts/monstas" %> 52 | 53 | 54 | -------------------------------------------------------------------------------- /source/sitemap.xml.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% sitemap.resources.each do |page| %> 5 | <% if page.path.include? ".html" %> 6 | 7 | <%= data.book.domain + page.url %> 8 | 0.5 9 | 10 | <% end %> 11 | <% end %> 12 | 13 | --------------------------------------------------------------------------------