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 = '