Let’s Test It Well: Simply and Smartly

mock_model
and stub_model
is, how to use shared_context
and shared_examples
, etc.If you are fond of testing, just like our Ruby Developer Nastia Shaternik, you’ll probably be interested to read her post about using RSpec. There, she dwells on how some RSpec features that are not commonly used can help you to simplify testing and make tests clearer.
This article explains how to make tests readable as short documentation and how using mock_model can make your tests run faster. In addition, we exemplify the usage of RSpec’s built-in expectations, two strategies of sharing the same data among different examples, etc.
Tests as specification
I’m really fond of tests, which can be read as short documentation and expose the application’s API. To help you to cope with it, run your specs with the --format d[ocumentation]
option. The output will be printed in a nested way. If you don’t understand what your code can do, you should rewrite your tests. In order not to write RSpec options every time when running specs, create a .rspec
configuration file in your home or application directory. (Options that are stored in ./.rspec
take precedence over options stored in ~/.rspec
, and any options declared directly on the command line will take precedence over those in either file.) The .rspec
file will look as shown below.
1 2 | <span class="o">--</span><span class="n">color</span> <span class="o">--</span><span class="nb">format</span> <span class="n">d</span><span class="p">[</span><span class="n">ocumentation</span><span class="p">]</span> |
RSpec’s built-in expectations
Avoid using !=
and remember about should_not
. To test the actual.predicate?
methods, use actual.should be_[predicate]
.
1 2 3 4 | <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">be_true</span> <span class="c1"># passes if actual is truthy (not nil or false)</span> <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">be_false</span> <span class="c1"># passes if actual is falsy (nil or false)</span> <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">be_nil</span> <span class="c1"># passes if actual is nil</span> <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">be</span> <span class="c1"># passes if actual is truthy (not nil or false)</span> |
Please also use collection’s matchers.
1 2 3 4 5 | <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="kp">include</span><span class="p">(</span><span class="n">expected</span><span class="p">)</span> <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">have</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">items</span> <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">have_exactly</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">items</span> <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">have_at_least</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">items</span> <span class="n">actual</span><span class="p">.</span><span class="nf">should</span> <span class="n">have_at_most</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">items</span> |
mock_model
vs. stub_model
By default, mock_model
produces a mock that acts like an existing record (persisted()
returns true). The stub_model
method is similar to mock_model
except that it creates an actual instance of the model. This requires that the model has a corresponding table in the database. So, the main advantage is obvious, tests written with mock_model
, will run faster. Another advantage of mock_model
over stub_model
is that it’s a true double, so the examples are not dependent on the behavior/misbehavior or even the existence of any other code.
Usage of the mock_model
method is quite simple and is illustrated below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="n">describe</span> <span class="no">SpineData</span> <span class="k">do</span> <span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="n">create</span> <span class="ss">:user</span> <span class="p">}</span> <span class="n">before</span><span class="p">(</span><span class="ss">:each</span><span class="p">)</span> <span class="k">do</span> <span class="no">SpineData</span><span class="p">.</span><span class="nf">stub_chain</span><span class="p">(</span><span class="ss">:controller</span><span class="p">,</span> <span class="ss">:current_user</span><span class="p">).</span><span class="nf">and_return</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">end</span> <span class="c1"># ...</span> <span class="n">context</span> <span class="s2">"dealing with content set"</span> <span class="k">do</span> <span class="n">let</span><span class="p">(</span><span class="ss">:set</span><span class="p">)</span> <span class="p">{</span> <span class="n">mock_model</span> <span class="no">ContentSet</span> <span class="p">}</span> <span class="n">describe</span> <span class="s2">".set_updater"</span> <span class="k">do</span> <span class="n">subject</span> <span class="p">{</span> <span class="no">SpineData</span><span class="p">.</span><span class="nf">set_updater</span> <span class="n">set</span> <span class="p">}</span> <span class="n">it</span> <span class="p">{</span> <span class="n">should_not</span> <span class="n">be_blank</span> <span class="p">}</span> <span class="n">its</span><span class="p">([</span><span class="ss">:id</span><span class="p">])</span> <span class="p">{</span> <span class="n">should</span> <span class="o">==</span> <span class="n">set</span><span class="p">.</span><span class="nf">id</span><span class="p">}</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> |
subject
and it {}
In an example group, you can use the subject
method to define an explicit subject for testing by passing it a block. Now, you can use the it {}
constructions to specify matchers. It’s just concise!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <span class="n">describe</span> <span class="no">AccountProcessing</span> <span class="k">do</span> <span class="n">include_context</span> <span class="ss">:oauth_hash</span> <span class="c1"># ...</span> <span class="n">context</span> <span class="s1">'user is anonymous'</span> <span class="k">do</span> <span class="n">let</span><span class="p">(</span><span class="ss">:acc_processing</span><span class="p">)</span> <span class="p">{</span> <span class="no">AccountProcessing</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">auth_hash</span><span class="p">)</span> <span class="p">}</span> <span class="c1"># ...</span> <span class="n">describe</span> <span class="s1">'#create_or_update_account'</span> <span class="k">do</span> <span class="n">subject</span> <span class="p">{</span> <span class="n">acc_processing</span><span class="p">.</span><span class="nf">create_or_update_account</span> <span class="p">}</span> <span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">be_present</span> <span class="p">}</span> <span class="k">end</span> <span class="n">describe</span> <span class="s1">'#account_info'</span> <span class="k">do</span> <span class="n">subject</span> <span class="p">{</span> <span class="n">acc_processing</span><span class="p">.</span><span class="nf">account_info</span> <span class="p">}</span> <span class="n">it</span> <span class="s1">'returns valid account info hash'</span> <span class="k">do</span> <span class="n">should</span> <span class="n">be</span> <span class="o">==</span> <span class="p">{</span> <span class="ss">network: </span><span class="n">auth_hash</span><span class="p">[</span><span class="s1">'provider'</span><span class="p">],</span> <span class="ss">email: </span><span class="n">auth_hash</span><span class="p">[</span><span class="s1">'info'</span><span class="p">][</span><span class="s1">'email'</span><span class="p">],</span> <span class="ss">first_name: </span><span class="n">auth_hash</span><span class="p">[</span><span class="s1">'info'</span><span class="p">][</span><span class="s1">'first_name'</span><span class="p">],</span> <span class="ss">last_name: </span><span class="n">auth_hash</span><span class="p">[</span><span class="s1">'info'</span><span class="p">][</span><span class="s1">'last_name'</span><span class="p">],</span> <span class="ss">birthday: </span><span class="kp">nil</span> <span class="p">}</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> |
DRY!ness
There are two strategies—shared context and shared examples—to share the same data among different examples.
Shared context
Use shared_context
to define a block that will be evaluated in the context of example groups by employing include_context
. You can put settings (something in the before
/after
block), variables, data, and methods. All the things you put into shared_contex
will be accessible in the example group by name.
Below, you can see how to define shared_context
.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="n">shared_context</span> <span class="s2">"shared_data"</span> <span class="k">do</span> <span class="n">before</span> <span class="p">{</span> <span class="vi">@some_var</span> <span class="o">=</span> <span class="ss">:some_value</span> <span class="p">}</span> <span class="k">def</span> <span class="nf">shared_method</span> <span class="s2">"it works"</span> <span class="k">end</span> <span class="n">let</span><span class="p">(</span><span class="ss">:shared_let</span><span class="p">)</span> <span class="p">{</span> <span class="p">{</span><span class="s1">'arbitrary'</span> <span class="o">=></span> <span class="s1">'object'</span><span class="p">}</span> <span class="p">}</span> <span class="n">subject</span> <span class="k">do</span> <span class="s1">'this is the shared subject'</span> <span class="k">end</span> <span class="k">end</span> |
Here is how you use shared_context
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <span class="nb">require</span> <span class="s2">"./shared_data.rb"</span> <span class="n">describe</span> <span class="s2">"group that includes a shared context using 'include_context'"</span> <span class="k">do</span> <span class="n">include_context</span> <span class="ss">:shared_data</span> <span class="n">it</span> <span class="s2">"has access to methods defined in shared context"</span> <span class="k">do</span> <span class="n">shared_method</span><span class="p">.</span><span class="nf">should</span> <span class="n">eq</span><span class="p">(</span><span class="s2">"it works"</span><span class="p">)</span> <span class="k">end</span> <span class="n">it</span> <span class="s2">"has access to methods defined with let in shared context"</span> <span class="k">do</span> <span class="n">shared_let</span><span class="p">[</span><span class="s1">'arbitrary'</span><span class="p">].</span><span class="nf">should</span> <span class="n">eq</span><span class="p">(</span><span class="s1">'object'</span><span class="p">)</span> <span class="k">end</span> <span class="n">it</span> <span class="s2">"runs the before hooks defined in the shared context"</span> <span class="k">do</span> <span class="vi">@some_var</span><span class="p">.</span><span class="nf">should</span> <span class="n">be</span><span class="p">(</span><span class="ss">:some_value</span><span class="p">)</span> <span class="k">end</span> <span class="n">it</span> <span class="s2">"accesses the subject defined in the shared context"</span> <span class="k">do</span> <span class="n">subject</span><span class="p">.</span><span class="nf">should</span> <span class="n">eq</span><span class="p">(</span><span class="s1">'this is the shared subject'</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> |
Shared examples
Shared examples are used to describe a common behavior and encapsulate it into a single example group. Then, examples can be applied to another example group.
This is how you define shared_examples
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <span class="nb">require</span> <span class="s2">"set"</span> <span class="n">shared_examples</span> <span class="s2">"a collection"</span> <span class="k">do</span> <span class="n">let</span><span class="p">(</span><span class="ss">:collection</span><span class="p">)</span> <span class="p">{</span> <span class="n">described_class</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="mi">7</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">])</span> <span class="p">}</span> <span class="n">context</span> <span class="s2">"initialized with 3 items"</span> <span class="k">do</span> <span class="n">it</span> <span class="s2">"says it has three items"</span> <span class="k">do</span> <span class="n">collection</span><span class="p">.</span><span class="nf">size</span><span class="p">.</span><span class="nf">should</span> <span class="n">eq</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="n">describe</span> <span class="s2">"#include?"</span> <span class="k">do</span> <span class="n">context</span> <span class="s2">"with an an item that is in the collection"</span> <span class="k">do</span> <span class="n">it</span> <span class="s2">"returns true"</span> <span class="k">do</span> <span class="n">collection</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="mi">7</span><span class="p">).</span><span class="nf">should</span> <span class="n">be_true</span> <span class="k">end</span> <span class="k">end</span> <span class="n">context</span> <span class="s2">"with an an item that is not in the collection"</span> <span class="k">do</span> <span class="n">it</span> <span class="s2">"returns false"</span> <span class="k">do</span> <span class="n">collection</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="mi">9</span><span class="p">).</span><span class="nf">should</span> <span class="n">be_false</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> |
Below, you can see how to use shared_examples
.
1 2 3 4 5 6 7 | <span class="n">describe</span> <span class="no">Array</span> <span class="k">do</span> <span class="n">it_behaves_like</span> <span class="s2">"a collection"</span> <span class="k">end</span> <span class="n">describe</span> <span class="no">Set</span> <span class="k">do</span> <span class="n">it_behaves_like</span> <span class="s2">"a collection"</span> <span class="k">end</span> |
For more information about RSpec, you can consult with the official documentation. The book by RSpec’s creator David Chelimsky will also be helpful. Or, you can opt for these guides on Better Specs.