Introduction
The purpose of this document is to describe
the working principle of Ruby on Rails test frameworks, its application,
advantages and disadvantages. This document contains an introduction
to Test driven development (TDD).
Test Driven Development (TDD)
Test Driven
Development is a development practice which involves writing test cases before
writing the code. Start by writing a very small test for code that does not yet
exist. Run the test and, naturally, it fails. Now we have to write the code to
pass the test.ie, writing code only for the requirement. Large numbers of tests
help to limit the number of defects in the code. The early and frequent nature
of the testing helps to catch defects early in the development cycle, preventing
them from becoming endemic and expensive problems.
TDD
in Ruby on Rails
Ruby on rails supports test
frameworks for Test Driven Development; A Test Framework is a tool or library that provides a
backdrop for writing tests.
This
is an excellent way to build up a test suite that exercises every part of the application.
There are several testing frameworks in use for Ruby today:
- Test::Unit is included with Ruby 1.8, and follows the "xUnit" conventions
- Minitest is included with Ruby 1.9, and allows both xUnit and RSpec style tests
- RSpec, which has more concise syntax and can be used in the same project, but creates a separate suite of tests, called "specs"
- In Cucumber, tests are written not in Ruby but in a language designed for tests
- Rspec-2, which is used with Rails -3. Since We are using Rails -3 Rspec -2 is the best option we can use for test driven development
For using this, we have
to twist thinking a little bit. Our goal is not to write eventual production
code right away , but our goal is to make our test cases pass.
Work Flow Structure
We can structure the test
driven workflow as follows:
- First write a test: This test describes the behavior of a small element of your system. Tests in TDD are called programmer tests. When writing the tests it should be kept in mind that the tests should concentrate on testing the true behaviors, i.e. if a system has to handle multiple inputs, the tests should reflect multiple inputs.
- Run the test: The test definitely fails because you have not yet built the code for that part of your system. This important step tests your test case, verifying that your test case fails. The automatic tests should be run after each change of the application code in order to assure that the changes have not introduced errors to the previous version of the code
- Write Code: Then only real coding comes, write enough code to make the test pass. In TDD, the code writing is actually a process for making the test work, i.e. writing the code that passes the test.
- Run the test: Again run the test and verify that they pass.
·
Refactor
the code - Refactoring is a process of improving the internal structure by
editing the existing working code, without changing its external behavior. The
idea of refactoring is to carry out the modifications as a series of small
steps without introducing new defects into to the system
- Run all tests: To verify that the refactoring did not change the external behavior.
See the Structure below:
The first step involves simply writing a piece of code that tests the
desired functionality. The second one is required to validate that the test is
correct, i.e. the test must not pass at this point, because the behavior under
implementation must not exist as yet. Nonetheless, if the test passes, the test
is either not testing the correct behavior or the TDD principles have not been
followed. The third step is the writing of the code. However, it should be kept
in mind to only write as little code as possible to pass the. Next, all tests
must be run in order to see that the change has not introduced any problems somewhere
else in the system. Once all tests pass, the internal structure of the code
should be improved by refactoring.
Rspec2
RSpec is a great tool in the behavior
driven design process of writing human readable specifications that direct and
validate the development of your application. What follows are some guidelines
taken from the literature, online resources, and from our experience. Rspec is not a tool for
Integration Testing but for Unit Testing, if you want to set up integration
tests, then you should use Cucumber. Cucumber is designed to
easy Behavior Driven Development but even if you don't BDD it is perfect for
integration testing.
Examples For
Rspec-2 Test case
We can integrate Rspec to Ruby On Rails by installing the Rspec2 gem.
gem install
rspec-rails
For this
example, we’ll describe and develop the beginnings of a User class, which can
be assigned any number of roles. Start by creating a directory for the files
for this tutorial.
*
mkdir rspec_example
*
cd rspec_example
The first methods we’ll encounter
are ` d
escribe`
and `it`
.
Create a file in this
directory named user_spec.rb and type
the following:
describe User do
end
The
describe
method creates an instance of Behavior
. So “describe User” is really saying “describe the behaviour of the User class”.
Run the following command:
*
rspec spec/
user_spec.rb
The r
spec
command gets installed when you install the rspec-rails gem. It supports a large number of command line options.
Running
user_spec.rb
should have resulted in output that includes the following error:
./user_spec.rb:1: uninitialized constant User (NameError)
We haven’t even written a single line of production code and already Rspec2 is telling us what code we need to write. We need to create a User class to resolve this error,
so create user.rb with the following:
We haven’t even written a single line of production code and already Rspec2 is telling us what code we need to write. We need to create a User class to resolve this error,
so create user.rb with the following:
class User
end
And require it in
user_spec.rb:
require 'user'
describe User do
end
Now run the r
spec
command again, will get a result like thisFinished in 6.0e-06 seconds
0 examples, 0 failures
The output shows that we have no examples yet, so let’s add
one. We’ll start by describing the intent of example without any code.
describe User do
it "should be in any roles assigned to it" do
end
end
Run the spec, but this time adds the --format option:
$ rspec spec/user_spec.rb --format doc
User - should be in any roles assigned to it
Finished in 0.022865 seconds
1 example, 0 failures
Now add a Ruby statement that begins to express the described intent.
describe User do
it "should be in any roles assigned to it" do
user.should be_in_role ("assigned role")
end
end
… and run the spec command.
$ rspec spec/user_spec.rb --format specdoc
User - should be in any roles assigned to it (ERROR - 1)
1) NameError in 'User should be in any roles assigned to it' undefined local variable or method `user' for #<#<Class:0x14ed15c>:0x14ecdd8> ./user_spec.rb:6:
Finished in 0.017956 seconds
1 example, 1 failure
The output tells us that there is an error that no
user has been defined
, so the next step is to make one:describe User do
it "should be in any roles assigned to it" do
user = User.new
user.should be_in_role ("assigned role")
end
end
Run the spec command,
$ rspec spec/user_spec.rb --format specdoc
User - should be in any roles assigned to it (ERROR - 1)
1) NoMethodError in 'User should be in any roles assigned to it' undefined method `in_role?' for #<User:0x14ec8ec> ./user_spec.rb:7:
Finished in 0.020779 seconds
1 example, 1 failure
Now we learn that User does not respond to in_role?, so we add that to User:
class User
def in_role?(role)
end
$ rspec spec/user_spec.rb --format specdoc
User - should be in any roles assigned to it (FAILED - 1)
1) 'User should be in any roles assigned to it' FAILED expected in_role?("assigned role") to return true,
got nil ./user_spec.rb:7:
Finished in 0.0172110000000001 seconds
1 example, 1 failure
We now have a failing example, which is the first goal. We always want to see a meaningful failure before
success because that’s the only way we can be sure the success is the result of writing code in the right place in the system.
To get this to pass, we do the simplest thing that could possibly work:
Edit user.rb
class User
def in_role?(role)
true
end
$ rspec spec/user_spec.rb --format specdoc
User - should be in any roles assigned to it
Finished in 0.018173 seconds
1 example, 0 failures
That passes, but we’re not done yet. Take a look again at the example:
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.should be_in_role("assigned role")
end
end
The description says that the User “should be in any roles assigned to it”, but we haven’t assigned any roles to
it. Let’s add that assignment to the example:
describe User do
it "should be in any roles assigned to it" do
user = User.new
user.assign_role("assigned role")
user.should be_in_role("assigned role")
end
end
$ rspec spec/user_spec.rb --format specdoc
User - should be in any roles assigned to it (ERROR - 1)
1) NoMethodError in 'User should be in any roles assigned to it' undefined method `assign_role' for
#<User:0x14ec784> ./user_spec.rb:6:
Finished in 0.018564 seconds
1 example, 1 failure
Following the advice in the output, we now add the assign_role method to User.
class User
def in_role?(role)
true
end
def assign_role(role)
end
end
$ rspec spec/user_spec.rb --format specdoc
User - should be in any roles assigned to it
Finished in 0.018998 seconds
1 example, 0 failures
Advantages :
- Testing improves your designs: Each target object that you test must have at least two clients: your production code, and your test case. These clients force you to decouple your code
- Testing reduces unnecessary code: When you write your test cases first, you get in the habit of writing only enough code to make the test case pass. You reduce the temptation to code features because you might need them later.
- Simple Development Procedure: Each test case that you write establishes a small problem. Solving that problem with code is rewarding.
- Testing allows more freedom: If you have test cases that will catch likely errors, you'll find that you're more willing to make improvements to your code.
- Efficient Product: If we can generate test cases for all the possible scenarios, then the result will be highly efficient, well documented product. TDD can lead to more modularized, flexible, and extensible code.
- Documentation: After using this technique it satisfies a bunch of requirements, also have documentation, describing the behavior of the system, and a good start at a test suite. Each test case backs up a fundamental requirement in the system
Disadvantages :
- Big time investment. For the simple case we lose about 20+% of the actual implementation, but for complicated cases you lose much more. Also more code to write. We have to create test cases for models, controllers and views ,which consumes a lot of time
- Additional Complexity. For complex cases test cases are harder to calculate.
- Design Impacts. Sometimes the design is not clear at the start and evolves as we go along - this will force us to redo the test which will generate a big time lose
- Continuous Tweaking. For data structures and black box algorithms unit tests would be perfect, but for algorithms that tend to be changed, tweaked or fine tuned, this can cause a big time investment that one might claim is not justified.