Welcome back! If you missed the first part of our journey so far, then you might want to go back and catch-up first.
So far, we've applied a Test-Driven Development process to building our application, alongside utilizing the popular RSpec testing framework. From here, we're going to investigate some other RSpec features as well as look into using the Pry gem to help you debug and write your code.
Other RSpec Features
Let's take a moment to review some other RSpec features that we've not needed in this simple example application, but you may find useful working on your own project.
Pending Statement
Imagine you write a test but you're interrupted, or you need to leave for a meeting and haven't yet completed the code required to get the test to pass.
You could delete the test and re-write it later when you're able to come back to your work. Or alternatively you could just comment the code out, but that's pretty ugly and definitely no good when using a version control system.
The best thing to do in this situation is to define our test as 'pending' so whenever the tests are run, the test framework will ignore the test. To do this you need to use the pending
keyword:
describe "some method" do it "should do something" pending end end
Set-Up and Tear-Down
All good testing frameworks allow you to execute code before and after each test is run. RSpec is no different.
It provides us before
and after
methods which allows us to set-up a specific state for our test to run, and to then clean up that state after the test has run (this is so the state doesn't leak and effect the outcome of subsequent tests).
describe "some method" do before(:each) do # some set-up code end after(:each) do # some tear-down code end it "should do something" pending end end
Context Blocks
We've already seen the describe
block; but there is another block which is functionally equivalent called context
. You can use it every where you would use describe
.
The difference between them is subtle but important: context
allows us to define a state for our test. Not explicitly though (we don't actually set the state by defining a context
block - it instead is for readability purposes so the intent of the following code is clearer).
Here is an example:
describe "Some method" do context "block provided" do it "yields to block" do pending end end context "no block provided" do it "calls a fallback method" do pending end end end
Stubs
We can use the stub
method to create a fake version of an existing object and to have it return a pre-determined value.
This is useful in preventing our tests from touching live service APIs, and guiding our tests by giving predictable results from certain calls.
Imagine we have a class called Person
and that this class has a speak
method. We want to test that method works how we expect it to. To do this we'll stub the speak
method using the following code:
describe Person do it "speak()" do bob = stub() bob.stub(:speak).and_return('hello') Person.any_instance.stub(:initialize).and_return(bob) instance = Person.new expect(instance.speak).to eq('hello') end end
In this example, we say that 'any instance' of the Person
class should have
its initialize
method stubbed so it returns the object bob
.
You'll notice that bob
is itself a stub which is set-up so that any time code
tries to execute the speak
method it will return "hello".
We then proceed to create a new Person
instance and pass the call of
instance.speak
into RSpec's expect
syntax.
We tell RSpec that we're expecting that call to result in the String "hello".
Consecutive Return Values
In the previous examples we've use the RSpec feature and_return
to indicate what our stub should return when it's called.
We can indicate a different return value each time the stub is called by specifying multiple arguments to the and_return
method:
obj = stub() obj.stub(:foo).and_return(1, 2, 3) expect(obj.foo()).to eq(1) expect(obj.foo()).to eq(2) expect(obj.foo()).to eq(3)
Mocks
Mocks are similar to Stubs in that we are creating fake versions of our objects but instead of returning a pre-defined value we're more specifically guiding the routes our objects must take for the test to be valid.
To do that we use the mock
method:
describe Obj do it "testing()" do bob = mock() bob.should_receive(:testing).with('content') Obj.any_instance.stub(:initialize).and_return(bob) instance = Obj.new instance.testing('some value') end end
In the above example we create a new Object
instance and then call its testing
method.
Behind the scenes of that code we expect the testing
method to be called with the value 'content'
. If it isn't called with that value (which in the above example it's not) then we know that some piece of our code hasn't functioned properly.
Subject Block
The subject
keyword can be used in a couple of different ways. All of which are designed to reduce code duplication.
You can use it implicitly (notice our it
block doesn't reference subject
at all):
describe Array do describe "with 3 items" do subject { [1,2,3] } it { should_not be_empty } end end
You can use it explicitly (notice our it
block refers to subject
directly):
describe MyClass do describe "initialization" do subject { MyClass } it "creates a new instance" do instance = subject.new expect(instance).to be_a(MyClass) end end end
Rather than constantly referencing a subject within your code and passing in different values for instantiation, for example:
describe "Foo" do context "A" do it "Bar" do baz = Baz.new('a') expect(baz.type).to eq('a') end end context "B" do it "Bar" do baz = Baz.new('b') expect(baz.type).to eq('b') end end context "C" do it "Bar" do baz = Baz.new('c') expect(baz.type).to eq('c') end end end
You can instead use subject
along with let
to reduce the duplication:
describe "Person" do subject { Person.new(name) } # Person has a get_name method context "Bob" do let(:name) { 'Bob' } its(:get_name) { should == 'Bob' } end context "Joe" do let(:name) { 'Joe' } its(:get_name) { should == 'Joe' } end context "Smith" do let(:name) { 'Smith' } its(:get_name) { should == 'Smith' } end end
There are many more features available to RSpec but we've looked at the most important ones that you'll find yourself using a lot when writing tests using RSpec.
Randomized Tests
You can configure RSpec to run your tests in a random order. This allows you to ensure that none of your tests have any reliance or dependency on the other tests around it.
RSpec.configure do |config| config.order = 'random' end
You can set this via the command using the --order
flag/option. For example: rspec --order random
.
When you use the --order random
option RSpec will display the random number it used to seed the algorithm. You can use this 'seed' value again when you think you've discovered a dependency issue within your tests. Once you've fixed what you think is the issue you can pass the seed value into RSpec (e.g. if the seed was 1234
then execute --order random:1234
) and it will use that same randomized seed to see if it can replicate the original dependency bug.
Global Configuration
You've seen we've added a project specific set of configuration objects within our Rakefile
. But you can set configuration options globally by added them to a .rspec
file within your home directory.
For example, inside .rspec
:
--color --format nested
Debugging With Pry
Now we're ready to start looking into how we can debug our application and our test code using the Pry gem.
It's important to understand that although Pry is really good for debugging your code, it is actually meant as an improved Ruby REPL tool (to replace irb
) and not strictly debugging purposes; so for example there are no built-in functions such as: step into, step over or step out etc that you would typically find in a tool designed for debugging.
But as a debugging tool, Pry is very focused and lean.
We'll come back to debugging in a moment, but let's first review how we'll be using Pry initially.
Updated Code Example
For the purpose of demonstrating Pry I'm going to add more code to my example application (this extra code doesn't effect our test in any way)
class RSpecGreeter attr_accessor :test @@class_property = "I'm a class property" def greet binding.pry @instance_property = "I'm an instance property" pubs privs "Hello RSpec!" end def pubs test_var = "I'm a test variable" test_var end private def privs puts "I'm private" end end
You'll notice we've added some extra methods, instance and class properties. We also make calls to two of the new methods we've added from within our greet
method.
Lastly, you'll notice the use of binding.pry
.
Setting Break-Points With binding.pry
A break-point is a place within your code where execution will stop.
You can have multiple break-points set within your code and you create them
using binding.pry
.
When you run your code you'll notice that the terminal will stop and place you inside your application's code at the exact spot your binding.pry was placed.
Below is an example of how it might look...
8: def greet => 9: binding.pry 10: pubs 11: privs 12: "Hello RSpec!" 13: end
From this point Pry has access to the local scope so you can use Pry much like
you would in irb
and start typing in for example variables to see what values they
hold.
You can run the exit
command to exit Pry and for your code to continue
executing.
Finding Where You Are: whereami
When using lots of binding.pry
break points it can be difficult to understand where in the application you are.
To get a better context of where you are at any point, you can use the whereami
command.
When run on its own you'll see something similar to when you used binding.pry
(you'll see the line on which the break-point was set and a couple of lines above and below that). The difference being is if you pass an extra numerical argument like whereami 5
you'll see five additional lines above where the binding.pry
was placed. You could request to see 100 lines around the current break-point for example.
This command can help orientate you within the current file.
Stack Trace: wtf
The wtf
command stands for "what the f***" and it provides a full stack trace for the most recent exception that has been thrown. It can help you understand the steps leading up to the error that occurred.
Inspecting: ls
The ls
command displays what methods and properties are available to Pry.
When run it will show you something like...
RSpecGreeter#methods: greet pubs test test= class variables: @@class_property locals: _ __ _dir_ _ex_ _file_ _in_ _out_ _pry_
In the above example we can see that we have four public methods (remember we updated our code to include some additional methods and then test
and test=
were created when using Ruby's attr_accessor
short hand).
It also displays other class and local variables Pry can access.
Another useful thing you can do is to grep (search) the results for only what you're interested in. You'll need to have an understanding of Regular Expressions but it can be a handy technique. Here is an example...
ls -p -G ^p => RSpecGreeter#methods: privs
In the above example we're using the -p
and -G
options/flags which tell Pry we only want to see public and private methods and we use the regex ^p
(which means match anything starting with p
) as our search pattern to filter the results.
Running ls --help
will also show you all available options.
Changing Scope: cd
You can change the current scope by using the cd
command.
In our example if we run cd ../pubs
it'll take us to the result of that method call.
If we now run whereami
you'll see it will display Inside "I'm a test variable"
.
If we run self
then you'll see we get "I'm a test variable"
returned.
If we run self.class
we'll see String
returned.
You can move up the scope chain using cd ..
or you can go back to the top level of the scope using cd /
.
Note: we could add another binding.pry
inside the pubs
method and then our scope would be inside that method rather than the result of the method.
Seeing How Deep You Are: nesting
Consider the previous example of running cd pubs
. If we run the nesting
command we'll get a top level look on the number of contexts/levels Pry currently has:
Nesting status: -- 0. # (Pry top level) 1. "I'm a test variable"
From there we can run exit
to move back to the earlier context (for example, inside the greet
method)
Running exit
again will mean we're closing the last context Pry has and so Pry finishes and our code continues to run.
Locate Any Method: find-method
If you're not sure where to find a particular method then you can use the find-method
command to show you all files within your code base that has a method that matches what you're searching for:
find-method priv => Kernel Kernel#private_methods Module Module#private_instance_methods Module#private_constant Module#private_method_defined? Module#private_class_method Module#private RSpecGreeter RSpecGreeter#privs
You can also use the -c
option/flag to search the content of files instead:
find-method -c greet => RSpecGreeter RSpecGreeter: def greet RSpecGreeter#privs: greet
Classic Debugging: next
, step
, continue
Although the above techniques are useful, it's not really 'debugging' in the same sense as what you're probably used to.
For most developers their editor or browser will provide them with a built-in debugging tool that lets them actually step through their code line by line and follow the route the code takes until completion.
As Pry is developed to be used as a REPL that's not to say it's not useful for debugging.
A naive solution would be to set multiple binding.pry
statements through out a method and use ctrl-d to move through each break-point set. But that' still not quite good enough.
For step by step debugging you can load the gem pry-nav...
source "https://rubygems.org" gem 'rspec' group :development do gem 'guard' gem 'guard-rspec' gem 'pry' # Adds debugging steps to Pry # continue, step, next gem 'pry-remote' gem 'pry-nav' end
This gem extends Pry so it understands the following commands:
-
Next
(move to the next line) -
Step
(move to the next line and if it's a method, then move into that method) -
Continue
(Ignore any further break-points in this file)
Continuous Integration With Travis-CI
As an added bonus let's integrate our tests with the online CI (continuous integration) service Travis-CI.
The principle of CI is to commit/push early and often to avoid conflicts between your code and the master branch. When you do (in this case we're committing to GitHub) then that should kick off a 'build' on your CI server which runs the relevant tests to ensure all is working as it should be.
Now with TDD as your primary development methodology you're less likely to incur bugs every time you push because your tests are an integral part of your development workflow and so before you push you'll already be aware of bugs or regressions. But this doesn't necessarily protect you from bugs that occur from integration tests (where all code across multiple systems are run together to ensure the system 'as a whole' is functioning correctly).
Regardless, code should never be pushed directly to your live production server any way; it should always be pushed first to a CI server to help catch any potential bugs that arise from differences between your development environment and the production environment.
A lot of companies have more environments still for their code to pass through before it reaches the live production server.
For example, at BBC News we have:
- CI
- Test
- Stage
- Live
Although each environment should be identical in set-up, the purpose is to implement different types of testing to ensure as many bugs are caught and resolved before the code reaches 'live'.
Travis-CI
Travis CI is a hosted continuous integration service for the open source community. It is integrated with GitHub and offers first class support for multiple languages
What this means is that Travis-CI offers free CI services for open-source projects and also has a paid model for businesses and organizations who want to keep their CI integration private.
We'll be using the free open-source model on our example GitHub repository.
The process is this:
- Register an account with GitHub
- Sign into Travis-CI using your GitHub account
- Go to your "Accounts" page
- Turn "on" any repositories you want to run CI on
- Create a
.travis.yml
file within the root directory of your project and commit it to your GitHub repository
The final step is the most important (creating a .travis.yml
file) as this determines the configuration settings for Travis-CI so it knows how to handle running the tests for your project.
Let's take a look at the .travis.yml
file we're using for our example GitHub repository:
language: ruby cache: bundler rvm: - 2.0.0 - 1.9.3 script: 'bundle exec rake spec' bundler_args: --without development branches: only: - master notifications: email: - [email protected]
Let's break this down piece by piece...
First we specify what language we're using in our project. In this case we're using Ruby: language: ruby
.
Because running Bundler can be a bit slow and we know that our dependencies aren't going to change that often we can choose to cache the dependencies, so we set cache: bundler
.
Travis-CI uses RVM (Ruby Version Manager) for installing Rubies on their servers. So we need to specify what Ruby versions we want to run our tests against. In this instance we've chosen 2.0
and 1.9.3
which are two popular Ruby versions (technically our application uses Ruby 2 but it's good to know our code passes in other versions of Ruby as well):
rvm: - 2.0.0 - 1.9.3
To run our tests we know that we can use the command rake
or rake spec
. Travis-CI by default runs the command rake
but because of how Gems are installed on Travis-CI using Bundler we need to change the default command: script: 'bundle exec rake spec'
. If we didn't do this then Travis-CI would have an issue locating the rspec/core/rake_task
file which is specified within our Rakefile
.
Note: if you have any issues regarding Travis-CI then you can join the #travis channel on IRC freenode to get help answering any questions you may have. That's where I discovered the solution to my issue with Travis-CI not being able to run my tests using its default rake
command and the suggestion of overwriting the default with bundle exec rake
resolved that issue.
Next, because we're only interested in running our tests we can pass additional arguments to Travis-CI to filter gems we don't want to bother installing. So for us we want to exclude installing the gems grouped as development: bundler_args: --without development
(this means we are excluding gems that are only really used for development and debugging such as Pry and Guard).
It's important to note that originally I was loading Pry within our spec_helper.rb
file. This caused a problem when running the code on Travis-CI, now that I was excluding 'development' gems. So I had to tweak the code like so:
require 'pry' if ENV['APP_ENV'] == 'debug'
You can see that now the Pry gem is only require
'ed if an environment variable of APP_ENV
is set to debug. This way we can avoid Travis-CI from throwing any errors. This does mean that when running your code locally you'd need to set the environment variable if you wanted to debug your code using Pry. The following shows how this could be done in one line:
APP_ENV=debug && ruby lib/example.rb
There were two other changes I made and that was to our Gemfile
. One was to make it clearer what gems were required for testing and which were required for development, and the other was explicitly required by Travis-CI:
source "https://rubygems.org" group :test do gem 'rake' gem 'rspec' end group :development do gem 'guard' gem 'guard-rspec' gem 'pry' # Adds debugging steps to Pry # continue, step, next gem 'pry-remote' gem 'pry-nav' end
Looking at the above updated Gemfile
we can see we've moved the RSpec gem into a new test
group, so now it should be clearer what purpose each gem has. We've also added a new gem 'rake'
. The Travis-CI documentation states this needed to be specified explicitly.
The next section is optional and it allows you to white list (or black list) certain branches on your repository. So by default Travis-CI will run tests against all your branches unless you tell it otherwise. In this example we're telling it we only want it to run against our master
branch:
branches: only: - master
We could tell it to run every branch 'except' a particular branch, like so:
branches: except: - some_branch_I_dont_want_run
The final section tells Travis-CI where to send notifications to when a build fails or succeeds:
notifications: email: - [email protected]
You can specify multiple email addresses if you wish:
notifications: email: - [email protected] - [email protected] - [email protected]
You can be more specific and specify what you want to happen on either a failure or a success (for example, more people will only be interested if the tests fail rather than getting an email every time they pass):
notifications: email: recipients: - [email protected] on_failure: change on_success: never
The above example shows that the recipient will never receive an email if the tests pass but will get notified if the failure status changes (the default value for both is always
which means you'll always be notified regardless of what the status result).
Note: when you explicitly specify a on_failure
or on_success
you need to move the email address(es) inside of recipients
key.
Conclusion
This is the end of our two part look into RSpec, TDD and Pry.
In part one we managed to write our application using the TDD process and the RSpec testing framework. In this second half we've also utilized Pry to show how we can more easily debug a running Ruby application. Finally we were able to get our tests set-up to run as part of a continuous integration server using the popular Travis-CI service.
Hopefully this has given you enough of a taste so you are keen to investigate each of these techniques further.
Comments