Chapter 2. Testing saves your bacon

This chapter covers

  • Writing tests with RSpec and Cucumber
  • Maintaining code through tests
  • Test- and behavior-driven development

Chapter 1 presented an extremely basic layout of a Rails application and an example of the scaffold generator.[1] One question remains, though: how do you make your Rails applications maintainable?

1 We won’t use the scaffold generator for the rest of the book because people tend to use it as a crutch, and it generates extraneous code.

The answer is that you write automated tests for the application as you develop it, and you write these all the time.

By writing automated tests for your application, you can quickly ensure that your application is working as intended. If you didn’t write tests, your alternative would be to check the entire application manually, which is time consuming and error prone. Automated testing saves you a ton of time in the long run and leads to fewer bugs. Humans make mistakes; programs (if coded correctly) do not. We’re going to be doing it right from step one.[2]

2 Unlike certain other books.

2.1. Test- and behavior-driven development

In the Ruby world a huge emphasis is placed on testing, specifically on test-driven development (TDD) and behavior-driven development (BDD). This chapter covers three testing tools, Test::Unit, RSpec, and Cucumber, in a basic fashion so you can quickly learn their format.

By learning good testing techniques now, you’ve got a solid way to make sure nothing is broken when you start to write your first real Rails application. If you didn’t test, there’s no telling what could go wrong in your code.

TDD is a methodology consisting of writing a failing test case first (usually using a testing tool such as Test::Unit), then writing the code to make the test pass, and finally refactoring the code. This process is commonly called red-green-refactor. The reasons for developing code this way are twofold. First, it makes you consider how the code should be running before it is used by anybody. Second, it gives you an automated test you can run as often as you like to ensure your code is still working as you intended. We’ll be using the Test::Unit tool for TDD.

BDD is a methodology based on TDD. You write an automated test to check the interaction between the different parts of the codebase rather than testing that each part works independently.

The two tools used for BDD are RSpec and Cucumber, both of which this book uses heavily.

Let’s begin by looking at TDD and Test::Unit.

2.2. Test-driven development

A cryptic yet true answer to the question “Why should I test?” is “because you are human.” Humans—the large majority of this book’s audience—make mistakes. It’s one of our favorite ways to learn. Because humans make mistakes, having a tool to inform them when they make one is helpful, isn’t it? Automated testing provides a quick safety net to inform developers when they make mistakes. By they, of course, we mean you. We want you to make as few mistakes as possible. We want you to save your bacon!

TDD and BDD also give you time to think through your decisions before you write any code. By first writing the test for the implementation, you are (or, at least, you should be) thinking through the implementation: the code you’ll write after the test and how you’ll make the test passes. If you find the test difficult to write, then perhaps the implementation could be improved. Unfortunately, there’s no clear way to quantify the difficulty of writing a test and working through it other than to consult with other people who are familiar with the process.

Once the test is implemented, you should go about writing some code that your test can pass. If you find yourself working backward—rewriting your test to fit a buggy implementation—it’s generally best to rethink the test and scrap the implementation. Test first, code later.

2.2.1. Why test?

Automated testing is much, much easier than manual testing. Have you ever gone through a website and manually filled in a form with specific values to make sure it conforms to your expectations? Wouldn’t it be faster and easier to have the computer do this work? Yes, it would, and that’s the beauty of automated testing: you won’t spend your time manually testing your code because you’ll have written test code to do that for you.

On the off chance you break something, the tests are there to tell you the what, when, how, and why of the breakage. Although tests can never be 100% guaranteed, your chances of getting this information without first having written tests are 0%. Nothing is worse than finding out something is broken through an early-morning phone call from an angry customer. Tests work toward preventing such scenarios by giving you and your client peace of mind. If the tests aren’t broken, chances are high (though not guaranteed) that the implementation isn’t either.

You’ll likely at some point face a situation in which something in your application breaks when a user attempts to perform an action you didn’t consider in your tests. With a base of tests, you can easily duplicate the scenario in which the user encountered the breakage, generate your own failed test, and use this information to fix the bug. This commonly used practice is called regression testing.

It’s valuable to have a solid base of tests in the application so you can spend time developing new features properly rather than fixing the old ones you didn’t do quite right. An application without tests is most likely broken in one way or another.

2.2.2. Writing your first test

The first testing library for Ruby was Test::Unit, which was written by Nathaniel Talbott back in 2000 and is now part of the Ruby core library. The documentation for this library gives a fantastic overview of its purpose, as summarized by the man himself:

The general idea behind unit testing is that you write a test method that makes certain assertions about your code, working against a test fixture. A bunch of these test methods are bundled up into a test suite and can be run any time the developer wants. The results of a run are gathered in a test result and displayed to the user through some UI.

Nathaniel Talbott

The UI Talbott references could be a terminal, a web page, or even a light.[3]

3 Such as the one GitHub has made: http://github.com/blog/653-our-new-build-status-indicator.

A common practice you’ll hopefully by now have experienced in the Ruby world is to let the libraries do a lot of the hard work for you. Sure, you could write a file yourself that loads one of your other files and runs a method and makes sure it works, but why do that when Test::Unit already provides that functionality for such little cost? Never re-invent the wheel when somebody’s done it for you.

Now you’re going to write a test, and you’ll write the code for it later. Welcome to TDD.

To try out Test::Unit, first create a new directory called example and in that directory make a file called example_test.rb. It’s good practice to suffix your filenames with _test so it’s obvious from the filename that it’s a test file. In this file, you’re going to define the most basic test possible, as shown in the following listing.

Listing 2.1. example/example_test.rb
require 'test/unit'

class ExampleTest < Test::Unit::TestCase
  def test_truth
    assert true
  end
end

To make this a Test::Unit test, you begin by requiring test/unit, which is part of Ruby’s standard library. This provides the Test::Unit::TestCase class inherited from on the next line. Inheriting from this class provides the functionality to run any method defined in this class whose name begins with test. Additionally, you can define tests by using the test method:

test "truth" do
  assert true
end

To run this file, you run ruby example_test.rb in the terminal. When this command completes, you see some output, the most relevant being two of the lines in the middle:

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

The first line is a singular period. This is Test::Unit’s way of indicating that it ran a test and the test passed. If the test had failed, it would show up as an F; if it had errored, an E. The second line provides statistics on what happened, specifically that there was one test and one assertion, and that nothing failed, there were no errors, and nothing was skipped. Great success!

The assert method in your test makes an assertion that the argument passed to it evaluates to true. This test passes given anything that’s not nil or false. When this method fails, it fails the test and raises an exception. Go ahead, try putting 1 there instead of true. It still works:

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

In the following listing, you remove the test_ from the beginning of your method and define it as simply a truth method.

Listing 2.2. example/example_test.rb, alternate truth test
def truth
  assert true
end

Test::Unit tells you there were no tests specified by running the default_test method internal to Test::Unit:

No tests were specified.
1 tests, 1 assertions, 1 failures, 0 errors

Remember to always prefix Test::Unit methods with test!

2.2.3. Saving bacon

Let’s make this a little more complex by creating a bacon_test.rb file and writing the test shown in the following listing.

Listing 2.3. example/bacon_test.rb
require 'test/unit'
class BaconTest < Test::Unit::TestCase
  def test_saved
    assert Bacon.saved?
  end
end

Of course, you want to ensure that your bacon[4] is always saved, and this is how you do it. If you now run the command to run this file, ruby bacon_test.rb, you get an error:

4 Both the metaphorical and the crispy kinds.

NameError: uninitialized constant BaconTest::Bacon

Your test is looking for a constant called Bacon and cannot find it because you haven’t yet defined the constant. For this test, the constant you want to define is a Bacon class. You can define this new class before or after the test. Note that in Ruby you usually must define constants and variables before you use them. In Test::Unit tests, the code is only run when it finishes evaluating it, which means you can define the Bacon class after the test. In the next listing, you follow the more conventional method of defining the class above the test.

Listing 2.4. example/bacon_test.rb
require 'test/unit'
class Bacon

end
class BaconTest < Test::Unit::TestCase
  def test_saved
    assert Bacon.saved?
  end
end

Upon rerunning the test, you get a different error:

NoMethodError: undefined method `saved?' for Bacon:Class

Progress! It recognizes there’s now a Bacon class, but there’s no saved? method for this class, so you must define one, as in the following listing.

Listing 2.5. example/bacon_test.rb
class Bacon
  def self.saved?
    true
  end
end

One more run of ruby bacon_test.rb and you can see that the test is now passing:

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Your bacon is indeed saved! Now any time that you want to check if it’s saved, you can run this file. If somebody else comes along and changes that true value to a false, then the test will fail:

F

  1) Failure:
test_saved(BaconTest) [bacon_test.rb:11]:
Failed assertion, no message given.

Test::Unit reports “Failed assertion, no message given” when an assertion fails. You should probably make that error message clearer! To do so, you can specify an additional argument to the assert method in your test, like this:

assert Bacon.saved?, "Our bacon was not saved :("

Now when you run the test, you get a clearer error message:

  1) Failure:
test_saved(BaconTest) [bacon_test.rb:11]:
Our bacon was not saved :(

And that, my friend, is the basics of TDD using Test::Unit. Although we don’t use this method in the book, it’s handy to know about because it establishes the basis for TDD in Ruby in case you wish to use it in the future. Test::Unit is also the default testing framework for Rails, so you may see it around in your travels. From this point on, we focus on RSpec and Cucumber, the two gems you’ll be using when you develop your next Rails application.

2.3. Behavior-driven development

BDD is similar to TDD, but the tests for BDD are written in an easier-to-understand language so that developers and clients alike can clearly understand what is being tested. The two tools we cover for BDD are RSpec and Cucumber.

RSpec tests are written in a Ruby domain-specific language (DSL), like this:

describe Bacon do
  it "is edible" do
   Bacon.edible?.should be_true
 end
end

The benefits of writing tests like this are that clients can understand precisely what the test is testing and then use these steps in acceptance testing;[5] a developer can read what the feature should do and then implement it; and finally, the test can be run as an automated test. With tests written in DSL, you have the three important elements of your business (the clients, the developers, and the code) all operating in the same language.

5 A process whereby people follow a set of instructions to ensure a feature is performing as intended.

RSpec is an extension of the methods already provided by Test::Unit. You can even use Test::Unit methods inside of RSpec tests if you wish. But we’re going to use the simpler, easier-to-understand syntax that RSpec provides.

Cucumber tests are written in a language called Gherkin, which goes like this:

Given I am reading a book
When I read this example
Then I should learn something

Each line indicates a step. The benefit of writing tests in the Gherkin language is that it’s closer to English than RSpec is, making it even easier for clients and developers to read.

2.3.1. RSpec

RSpec is a BDD tool written by Steven R. Baker and now maintained by David Chelimsky as a cleaner alternative to Test::Unit, with RSpec being built as an extension to Test::Unit. With RSpec, you write code known as specs that contain examples, which are synonymous to the tests you know from Test::Unit. In this example, you’re going to define the Bacon constant and then define the edible? method on it.

Let’s jump right in and install the rspec gem by running gem install rspec. You should see the following output:

Successfully installed rspec-core-2.6.4
Successfully installed rspec-expectations-2.6.4
Successfully installed rspec-mocks-2.6.4
Successfully installed rspec-2.6.4

You can see that the final line says the rspec gem is installed, with the version number specified after the name. Above this line, you also see a thank-you message and, underneath, the other gems that were installed. These gems are dependencies of the rspec gem, and as such, the rspec gem won’t work without them.

When the gem is installed, create a new directory for your tests called bacon anywhere you like, and inside that, create another directory called spec. If you’re running a UNIX-based operating system such as Linux or Mac OS X, you can run the mkdir -p bacon/spec command to create these two directories. This command will generate a bacon directory if it doesn’t already exist, and then generate in that directory a spec directory.

Inside the spec directory, create a file called bacon_spec.rb. This is the file you use to test your currently nonexistent Bacon class. Put the code from the following listing in spec/bacon_spec.rb.

Listing 2.6. bacon/spec/bacon_spec.rb
describe Bacon do
  it "is edible" do
    Bacon.edible?.should be_true
  end

end

You describe the (undefined) Bacon class and write an example for it, declaring that Bacon is edible. The describe block contains tests (examples) that describe the behavior of bacon. In this example, whenever you call edible? on Bacon, the result should be true. should serves a similar purpose to assert, which is to assert that its object matches the arguments passed to it. If the outcome is not what you say it should be, then RSpec raises an error and goes no further with that spec.

To run the spec, you run rspec spec in a terminal in the root of your bacon directory. You specify the spec directory as the first argument of this command so RSpec will run all the tests within that directory. This command can also take files as its arguments if you want to run tests only from those files.

When you run this spec, you get an uninitialized constant Object::Bacon error, because you haven’t yet defined your Bacon constant. To define it, create another directory inside your Bacon project folder called lib, and inside this directory, create a file called bacon.rb. This is the file where you define the Bacon constant, a class, as in the following listing.

Listing 2.7. bacon/lib/bacon.rb
class Bacon

end

You can now require this file in spec/bacon_spec.rb by placing the following line at the top of the file:

require 'bacon'

When you run your spec again, because you told it to load bacon, RSpec has added the lib directory on the same level as the spec directory to Ruby’s load path, and so it will find the lib/bacon.rb for your require. By requiring the lib/bacon.rb file, you ensure the Bacon constant is defined. The next time you run it, you get an undefined method for your new constant:

1) Bacon is edible
  Failure/Error: Bacon.edible?.should be_true
  NoMethodError:
   undefined method `edible?' for Bacon:Class

This means you need to define the edible? method on your Bacon class. Re-open lib/bacon.rb and add this method definition to the class:

def self.edible?
  true
end

Now the entire file looks like the following listing.

Listing 2.8. bacon/lib/bacon.rb
class Bacon
  def self.edible?
    true
  end
end

By defining the method as self.edible?, you define it for the class. If you didn’t prefix the method with self., it would define the method for an instance of the class rather than for the class itself. Running rspec spec now outputs a period, which indicates the test has passed. That’s the first test—done.

For the next test, you want to create many instances of the Bacon class and have the edible? method defined on them. To do this, open lib/bacon.rb and change the edible? class method to an instance method by removing the self. from before the method, as in the next listing.

Listing 2.9. bacon/lib/bacon.rb
class Bacon
  def edible?
    true
  end
end

When you run rspec spec again, you get the familiar error:

1) Bacon edible?
  Failure/Error: its(:edible?) { should be_true }
    expected false to be true

Oops! You broke a test! You should be changing the spec to suit your new ideas before changing the code! Let’s reverse the changes made in lib/bacon.rb, as in the following listing.

Listing 2.10. bacon/lib/bacon.rb
class Bacon
  def self.edible?
    true
  end
end

When you run rspec spec, it passes. Now let’s change the spec first, as in the next listing.

Listing 2.11. bacon/spec/bacon_spec.rb
describe Bacon do
  it "is edible" do
    Bacon.new.edible?.should be_true
  end
end

In this code, you instantiate a new object of the class rather than using the Bacon class. When you run rspec spec, it breaks once again:

NoMethodError in 'Bacon is edible'
undefined method `edible?' for #<Bacon:0x101deff38>

If you remove the self. from the edible? method, your test will now pass, as in the following listing.

Listing 2.12. Terminal
$ rspec spec
.
1 example, 0 failures

Now you can go about breaking your test once more by adding additional functionality: an expired! method, which will make your bacon inedible. This method sets an instance variable on the Bacon object called @expired to true, and you use it in your edible? method to check the bacon’s status.

First you must test that this expired! method is going to actually do what you think it should do. Create another example in spec/bacon_spec.rb so that the whole file now looks like the following listing.

Listing 2.13. bacon/spec/bacon_spec.rb
require 'bacon'
describe Bacon do
  it "is edible" do
    Bacon.new.edible?.should be_true
  end

  it "expired!" do
    bacon = Bacon.new
    bacon.expired!
    bacon.should be_expired
  end
end

When you find you’re repeating yourself, stop! You can see here that you’re defining a bacon variable to Bacon.new and that you’re also using Bacon.new in the first example. You shouldn’t be repeating yourself like that!

A nice way to tidy this up is to move the call to Bacon.new into a subject block. subject calls allow you to create an object to reference in all specs inside the describe block,[6] declaring it the subject (both literally and figuratively) of all the tests inside the describe block. You can define a subject like this:

6 Or inside a context block, which we use later. It works in a similar way to the describe blocks.

subject { Bacon.new }

In the context of the entire spec, it looks like the following listing.

Listing 2.14. bacon/spec/bacon_spec.rb
require 'bacon'

describe Bacon do

  subject { Bacon.new }

  it "is edible" do
    Bacon.new.edible?.should be_true
  end

  it "expired!" do
    bacon = Bacon.new
    bacon.expired!
    bacon.expired.should be_true
  end
end

Now that you have the subject, you can cut a lot of the code out of the first spec and refine it:

its(:edible?) { should be_true }

First, the its method takes the name of a method to call on the subject of these tests. The block specified should contain an assertion for the output of that method. Unlike before, you’re not calling should on an object, as you have done in previous tests, but rather on seemingly nothing at all. If you do this, RSpec figures out that you mean the subject you defined, so it calls should on that.

You can also reference the subject manually in your tests, as you’ll see when you write the expired! example shown in the following listing.

Listing 2.15. bacon/spec/bacon_spec.rb
it "expired!" do
  subject.expired!
  subject.should_not be_edible
end

Here, the expired! method must be called on the subject because it is only defined on your Bacon class. For readability’s sake, you explicitly call the should_not method on the subject and specify that edible? should return false.

If you run rspec spec again, your first spec still passes, but your second one fails because you have yet to define your expired! method. Let’s do that now in lib/bacon.rb, as shown in the following listing.

Listing 2.16. bacon/lib/bacon.rb
class Bacon
  def edible?
    true
  end

  def expired!
    self.expired = true
  end
end

By running rspec spec again, you get an undefined method error:

NoMethodError in 'Bacon expired!'
undefined method `expired=' for #<Bacon:0x101de6578>

This method is called by the following line in the previous example:

self.expired = true

To define this method, you can use the attr_accessor method provided by Ruby, as shown in listing 2.17; the attr prefix of the method means attribute. If you pass a Symbol (or collection of symbols) to this method, it defines methods for setting (expired=) and retrieving the attribute expired values, referred to as a setter and a getter respectively. It also defines an instance variable called @expired on every object of this class to store the value that was specified by the expired= method calls.

 

Warning

In Ruby you can call methods without the self. prefix. You specify the prefix because otherwise the interpreter will think that you’re defining a local variable. The rule for setter methods is that you should always use the prefix.

 

Listing 2.17. bacon/lib/bacon.rb
class Bacon
  attr_accessor :expired
  ...
end

With this in place, if you run rspec spec again, your example fails on the line following your previous failure:

Failure/Error: subject.should_not be_edible
  expected edible? to return false, got true

Even though this sets the expired attribute on the Bacon object, you’ve still hardcoded true in your edible? method. Now change the method to use the attribute method, as in the following listing.

Listing 2.18. bacon/lib/bacon.rb
def edible?
  !expired
end

When you run rspec spec again, both your specs will pass:

..

2 examples, 0 failures

Let’s go back in to lib/bacon.rb and remove the self. from the expired! method:

def expired!
  expired = true
end

If you run rspec spec again, you’ll see your second spec is now broken:

Failure/Error: Bacon expired!
expected edible? to return false, got true

Tests save you from making mistakes such as this. If you write the test first and then write the code to make the test pass, you have a solid base and can refactor the code to be clearer or smaller and finally ensure that it’s still working with the test you wrote in the first place. If the test still passes, then you’re probably doing it right.

If you change this method back now

def expired!
  self.expired = true
end

and then run your specs using rspec spec, you’ll see that they once again pass:

..

2 examples, 0 failures

Everything’s normal and working once again, which is great!

That ends our little foray into RSpec for now. You’ll use it again later when you develop your application. If you’d like to know more about RSpec, The RSpec Book: Behavior-Driven Development with RSpec, Cucumber, and Friends (David Chelimsky et al., Pragmatic Bookshelf, 2010) is recommended reading.

2.3.2. Cucumber

For this section, we retire the Bacon example and go for something more formal with Cucumber.

 

Note

This section assumes you have RSpec installed. If you don’t, use this command to install it: gem install rspec.

 

Whereas RSpec and Test::Unit are great for unit testing (testing a single part), Cucumber is mostly used for testing the entire integration stack.

Cucumber’s history is intertwined with RSpec, so the two are similar. In the beginning of BDD, as you know, there was RSpec. Shortly thereafter, there were RSpec Stories, which looked like the following listing.

Listing 2.19. Example
Scenario "savings account is in credit" do
  Given "my savings account balance is", 100 do |balance|
    @account = Account.new(balance)
  end
  ...
end

The idea behind RSpec Stories is that they are code- and human-readable stories that can be used for automated testing as well as quality assurance (QA) testing by stakeholders. Aslak Hellesoy rewrote RSpec Stories during October 2008 into what we know today as Cucumber. The syntax remains similar, as seen in the following listing.

Listing 2.20. Example
Scenario: Taking out money
  Given I have an account
  And my account balance is 100
  When I take out 10
  Then my account balance should be 90

What you see here is known as a scenario in Cucumber. Under the scenario’s title, the remainder of the lines are called steps, which are read by Cucumber’s parser and matched to step definitions, where the logic is kept. Scenarios are found inside a feature, which is a collection of common scenarios. For example, you may have one feature for dictating what happens when a user creates projects and another for when a user creates tickets.

Notice the keywords Given, And, When, and Then. These are just some of the keywords that Cucumber looks for to indicate that a line is a step. If you’re going to be using the same keyword on a new line, it’s best practice to instead use the And keyword because it reads better. Try reading the first two lines aloud from the previous listing, then replace the And with Given and try it again. It just sounds right to have an And there rather than a Given.

Given steps are used for setting up the scene for the scenario. This example sets up an account and gives it a balance of 100.

When steps are used for defining actions that should happen during the scenario. The example says, When I take out 10.

Then steps are used for declaring things that take place after the When steps have completed. This example says, When I take out 10 Then my account balance should be 90.

These different step types add a great deal of human readability to the scenarios in which they’re used, even though they can be used interchangeably. You could define all the steps as Givens, but it’s not really readable. Let’s now implement this example scenario in Cucumber. First, run mkdir -p accounts/features, which, much as in the RSpec example, creates a directory called accounts and a directory inside of that called features. In this features directory, create a file called account.feature. In this file, you write a feature definition, as shown in the following listing.

Listing 2.21. accounts/features/account.feature
Feature: My Account
  In order to manage my account
  As a money minder
  I want to ensure my money doesn't get lost

This listing lays out what this feature is about and is more useful to human readers (such as stakeholders) than it is to Cucumber.

Next, you put in your scenario underneath the feature definition, as in the following listing.

Listing 2.22. accounts/features/account.feature
Scenario: Taking out money
  Given I have an account
  And it has a balance of 100
  When I take out 10
  Then my balance should be 90

The whole feature should now look like the following listing.

Listing 2.23. accounts/features/account.feature
Feature: My Account
  In order to manage my account
  As a money minder
  I want to ensure my money doesn't get lost

  Scenario: Taking out money
    Given I have an account
    And it has a balance of 100
    When I take out 10
    Then my balance should be 90

As you can see in listing 2.23, it’s testing the whole stack of the transaction rather than a single unit. This process is called integration testing. You set the stage by using the Given steps, play out the scenario using When steps, and ensure that the outcome is as you expected by using Then steps. The And word is used when you want a step to be defined in the same way as the previous step, as seen in the first two lines of this scenario.

To run this file, you first need to ensure Cucumber is installed, which you can do by installing the Cucumber gem: gem install cucumber. When the Cucumber gem is installed, you can run this feature file by going into the accounts directory and running cucumber features, as in the next listing.

Listing 2.24. Terminal
Feature: My Account
  In order to manage my account
  As a money minder
  I want to ensure my money doesn't get lost

  Scenario: Taking out money
    Given I have an account
    And it has a balance of 100
    When I take out 10
    Then my balance should be 90

This output appears in color in the terminal with the steps of the scenario in yellow,[7] followed by a summary of this Cucumber run, and then a list of what code you used to define the missing steps (not shown in this example output), again in yellow. What this output doesn’t tell you is where to put the step definitions. Luckily, this book does. All these step definitions should go into a new file located at features/step_definitions/account_steps.rb. The file is called account_steps.rb and not account.rb to clearly separate it from any other Ruby files, so when looking for the steps file, you don’t get it confused with any other file. In this file, you can copy and paste in the steps Cucumber gave you, as in the following listing.

7 If you’re running Windows, you may need to install ANSICON in order to get the colored output. This process is described at https://github.com/adoxa/ansicon, and you can download ANSICON from http://adoxa.110mb.com/ansicon/.

Listing 2.25. features/step_definitions/account_steps.rb
Given /^I have an account$/ do
  pending # express the regexp above with the code you wish you had
end

Given /^it has a balance of (d+)$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

When /^I take out (d+)$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then /^my balance should be (d+)$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

If you run cucumber features again, you’ll see that all your steps are defined but not run (signaled by their blue coloring) except the very first one, which is yellow because it’s pending. Now you’re going to restructure the first step to make it not pending. It will now instantiate an Account object that you’ll use for the rest of this scenario.

Listing 2.26. features/step_definitions/account_steps.rb
Given /^I have an account$/ do
  @account = Account.new
end

Steps are defined by using regular expressions, which are used when you wish to match strings. In this case, you’re matching the step in the feature with this step definition by putting the text after the Given keyword into a regular expression. After the regular expression is the do Ruby keyword, which matches up with the end at the end. This syntax indicates a block, and this block is run (“called”) when this step definition is matched.

With this step defined, you can try running cucumber features/account.feature to see if the feature will pass. No—it fails with this error:

Given I have an account
  uninitialized constant Object::Account (NameError)

Similar to the beginning of the RSpec showcase, create a lib directory inside your accounts directory. To define this constant, you create a file in this new directory called account.rb. In this file you put code to define the class, shown in the following listing.

Listing 2.27. accounts/lib/account.rb
class Account

end

This file is not loaded automatically, of course: you have to require it just as you did in RSpec with lib/bacon.rb. Cucumber’s authors already thought of this and came up with a solution. Any file inside of features/support is automatically loaded, with one special file being loaded before all the others: features/support/env.rb. This file should be responsible for setting up the foundations for your tests. Now create features/support/env.rb and put these lines inside it:

$LOAD_PATH << File.expand_path('../../../lib', __FILE__)
require 'account'

When you run this feature again, the first step passes and the second one is pending:

Scenario: Taking out money
  Given I have an account
  And it has a balance of 100
    TODO (Cucumber::Pending)

Go back into features/step_definitions/account_steps.rb now and change the second step’s code to set the balance on your @account object, as shown in the next listing. Note in this listing that you change the block argument from arg1 to amount.

Listing 2.28. features/step_definitions/account_steps.rb
Given /^it has a balance of (d+)$/ do |amount|
  @account.balance = amount
end

With this step, you’ve used a capture group inside the regular expression. The capture group captures whatever it matches. In Cucumber, the match is returned as a variable, which is then used in the block. An important thing to remember is that these variables are always String objects.

When you run this feature again, this step fails because you haven’t yet defined the method on the Account class:

And it has a balance of 100
  undefined method `balance=' for #<Account:0xb7297b94> (NoMethodError)

To define this method, open lib/account.rb and change the code in this file to look exactly like the following listing.

Listing 2.29. accounts/lib/account.rb
class Account
  def balance=(amount)
    @balance = amount
  end
end

The method is defined as balance=. In Ruby, these methods are called setter methods and, just as their name suggests, they’re used for setting things. Setter methods are defined without the space between the method name and the = sign, but they can be called with or without the space, like this:

@account.balance=100
# or
@account.balance = 100

The object after the equals sign is passed in as the single argument for this method. In this method, you set the @balance instance variable to that value. Now when you run your feature, this step passes and the third one is the pending one:

Scenario: Taking out money
  Given I have an account
  And it has a balance of 100
  When I take out 10
    TODO (Cucumber::Pending)

Go back into features/step_definitions/account_steps.rb and change the third step to take some money from your account:

When /^I take out (d+)$/ do |amount|
  @account.balance = @account.balance - amount
end

Now when you run this feature, it’ll tell you there’s an undefined method balance, but didn’t you just define that?

When I take out 10
  undefined method `balance' for #<Account:0xb71c9a8c
                                    @balance=100> (NoMethodError)

Actually, the method you defined was balance= (with an equals sign), which is a setter method. balance (without an equals sign) in this example is a getter method, which is used for retrieving the variable you set in the setter method. Not all methods without equal signs are getter methods, however. To define this method, switch back into lib/account.rb and add this new method directly under the setter method, as shown in the following listing.

Listing 2.30. accounts/lib/account.rb
def balance=(amount)
  @balance = amount
end
def balance
  @balance
end

Here you define the balance= and balance methods you need. The first method is a setter method, which is used to set the @balance instance variable on an Account object to a specified value. The balance method returns that specific balance. When you run this feature again, you’ll see a new error:

When I take out 10
  String can't be coerced into Fixnum (TypeError)
  ./features/step_definitions/account_steps.rb:10:in `-'

This error occurred because you’re not storing the balance as a Fixnum but as a String. As mentioned earlier, the variable returned from the capture group for the second step definition is a String. To fix this, you coerce the object into a Fixnum by calling to_i[8] inside the setter method, as shown in the following listing.

8 For the sake of simplicity, we use to_i. Some will argue that to_f (converting to a floating-point number) is better to use for money. They’d be right. This is not a real-world system, only a contrived example. Chill.

Listing 2.31. accounts/lib/account.rb
def balance=(amount)
  @balance = amount.to_i
end

Now anything passed to the balance= method will be coerced into an integer. You also want to ensure that the other value is also a Fixnum. To do this, open features/step_definitions/account_steps.rb and change the third step to look exactly like the following listing.

Listing 2.32. features/step_definitions/account_steps.rb
When /^I take out (d+)$/ do |amount|
  @account.balance -= amount.to_i
end

That makes this third step pass, because you’re subtracting a Fixnum from a Fixnum. When you run this feature, you’ll see that this step is definitely passing and the final step is now pending:

Scenario: Taking out money
  Given I have an account
  And it has a balance of 100
  When I take out 10
  Then my balance should be 90
    TODO (Cucumber::Pending)

This final step asserts that your account balance is now 90. You can implement it in features/step_definitions/account_steps.rb, as shown in the following listing.

Listing 2.33. features/step_definitions/account_steps.rb
Then /^my balance should be (d+)$/ do |amount|
  @account.balance.should eql(amount.to_i)
end

Here you must coerce the amount variable into a Fixnum again so you’re comparing a Fixnum with a Fixnum. With this fourth and final step implemented, your entire scenario (which also means your entire feature) passes:

Scenario: Taking out money
  Given I have an account
  And it has a balance of 100
  When I take out 10
  Then my balance should be 90

1 scenario (1 passed)
4 steps (4 passed)

As you can see from this example, Cucumber allows you to write tests for your code in syntax that can be understood by developers, clients, and parsers alike. You’ll see a lot more of Cucumber when you use it in building your next Ruby on Rails application.

2.4. Summary

This chapter demonstrated how to apply TDD and BDD principles to test some rudimentary code. You can (and should!) apply these principles to all code you write, because testing the code ensures it’s maintainable from now into the future. You don’t have to use the gems shown in this chapter to test your Rails application; they are just preferred by a large portion of the community.

You’ll apply what you learned in this chapter to building a Rails application from scratch in upcoming chapters. You’ll use Cucumber from the outset by developing features to describe the behavior of your application and then implementing the necessary steps to make them pass. Thankfully, there’s another gem that generates some of these steps for you.

When you wish to test that a specific controller action is inaccessible, you use RSpec because it’s better suited for single-request testing. You use Cucumber when you want to test a series of requests, such as when creating a project. You’ll see plenty of examples of when to use Cucumber and when to use RSpec in later chapters.

Let’s get into it!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.226.255