CSC/ECE 517 Fall 2011/ch2 2e kt

From Expertiza_Wiki
Jump to navigation Jump to search

Overview

Why is unit testing important?

Evolution of Testing Architectures

Across all of the testing frameworks that Ruby has, most of them fall into two categories: Test Driven Development and Behavior Driven Development. These two types of development styles helped to drive the evolution of the Ruby testing frameworks we will be discussing.

Test Driven Development

Test Driven Development (or TDD) is a development style that relies on writing the tests for a given program first, thereby establishing the functionality, then proceeding to write the code to fulfill that criteria. As the code is being written, the tests can be run and examined, thus providing status on the development of the features. Once all of the tests pass for a given function, the development is complete and any refactoring can be done before finalizing the code. TDD has the benefit of providing for a short development cycle and encouraging simple designs. However, there are some shortcomings which have lead to the creation of the Behavior Development Cycle.

Behavior Driven Development

Behavior Driver Development (or BDD), originally proposed by Dan North<ref name = dannorth />, arose out of the fact that tests written in the TDD style are only clearly readable by the coders themselves. If a project manager without programming knowledge were to look at the tests as a description of the functionality to be provided, this would not very meaningful. BDD evolved though the idea that the tests could be written in such a way as to provide a set of project requirements and functions that is readable by the average person. This allows the tests to not only act as the testing framework for a project but also as an executable requirement list. This style still leverages the TDD development cycle benefits with the added readability for those outside of the development team. It should be noted that there is often a small learning curve associated with BDD testing frameworks compared to TDD.

Ruby Testing Frameworks

Some of the more popular Ruby testing frameworks include<ref name = rubytool />:

Since this will be a comparison across the different frameworks listed, using a common class to test against will provide us with a baseline for our comparisons. As such, we will use the following class implementing a bank account:

 class Account
   @balance
   @name
 
   attr_accessor :balance
   attr_accessor :name
 
   def initialize(amount)
     @balance = amount
   end
 
   def deposit(amount)
     @balance += amount
   end
 
   def addinterest(rate)
     @balance *= (1 + rate)
   end
 
   def withdrawal(amount)
     @balance -= amount
   end
 end

Back to top

Test::Unit

Overview

Test::Unit was one of the first widely adopted testing frameworks for Ruby, even being included as part of the standard library as of version 1.8. It is based on the SUnit Smalltalk framework <ref name = progruby /> and is very similar to testing frameworks in other languages such as JUnit (Java) and NUnit (.NET). The Test::Unit framework falls under the category of a Test Driven Design framework and relies on creating test cases, under which reside test methods. An individual test case consists of a class which inherits from Test::Unit::TestCase and contains test methods which, together, usually test a particular function or feature. Within each test case, only methods starting with the name "test" are automatically run, with the exception of the optional setup or teardown methods. These methods can be used to initialize and clean up any structures which may be used across the test methods. As of Ruby 1.9, Test::Unit has been replaced by MiniTest as the testing framework included in the standard library.

Functionality

Unit::Test integration into Eclipse via a built-in test runner

As Test::Unit is a Test Driven Development framework, it provides many of the low level testing constructs that you would expect to find in similar frameworks. It primarily relies on simple assertions to verify test cases. The functionality of Test::Unit is considered to be one of the more simple frameworks and, as such, one of the easier to learn.


assert assert_nil assert_not_nil assert_equal assert_not_equal
assert_in_delta assert_raise assert_nothing_raised assert_instance_of assert_kind_of
assert_respond_to assert_match assert_no_match assert_same assert_not_same
assert_operator assert_throws assert_send flunk


Test::Unit allows for the creation of test suites, which are collections of test cases, allowing for a higher level grouping of tests. Such test suites can be produced by creating a file which requires 'test/unit' as well as any other files which contain test cases.

When a test is usually run, it is called via ruby against the file containing the test case. In some cases, we wish to modify the way the test results are output; this is where a part of Test::Unit called "test runners" come in. These test runners allow the output of the test cases to be modified by overriding the default output methods. In this way, test outputs can be easily modified to fit new testing environments or even integration into IDEs.

Code Example

In order to create a test case for the Account class mentioned earlier, one needs to create a new class which inherits from Test::Unit::TestCase and requires 'test/unit' as well as the class under test, in this case 'Account.rb'. In order to get the test results, one simply has to run the ruby interpreter against the test file. For this example, we also include a failing test in order to point out the information provided in the event of a failed test.

 require "test/unit"
 require_relative("../Account.rb")
 
 class AccountTest < Test::Unit::TestCase
 
   def test_balance
     a = Account.new(100)
     assert_equal(100, a.balance())
   end
 
   def test_deposit 
     a = Account.new(100)
     assert_equal(200, a.deposit(100))
   end
   
   def test_withdrawal
     a = Account.new(100)
     assert_equal(50, a.withdrawal(50))
   end
   
   def test_name
     a = Account.new(100)
     a.name = "Checking"
     assert_not_nil(a.name())
   end
 
   def test_interest
     a = Account.new(100)
     assert_equal(150, a.addinterest(0.5))
   end
 
   def test_fail
     a = Account.new(100)
     assert_equal(200, a.balance())
   end
 end

From this we will get the output:

 Loaded suite AccountTest-ut
 Started
 ..F...
 Finished in 0.000782 seconds.
 
   1) Failure:
 test_fail(AccountTest) [AccountTest-ut.rb:40]:
 <200> expected but was
 <100>.
 
 6 tests, 6 assertions, 1 failures, 0 errors, 0 skips

Here we can see that in total all 6 test methods were run with one of them failing. The failing test method is indicated by name with the code line, expected output and actual output.

Back to top

Mini::Test

Overview

Mini::Test is one of the more recent additions to the Ruby Testing frameworks. Mini::Test was created to be a small, clean and fast replacement for the Test::Unit framework.

As previously mentioned, MiniTest is included in the standard library as of Ruby 1.9. (If you are running Ruby 1.8, you can run gem install minitest to get Mini::Test. Alternately, you can install the test-unit gem if you have Ruby 1.9 and want to get the Test::Unit functionality.) MiniTest provides both a Test Driven Development framework and a Behavior Driven Development Framework. Test cases in MiniTest::Unit are built similarly to those in Test::Unit - you create test cases with test methods containing assertions along with optional setup and teardown methods . Mini/spec (the BDD component of MiniTest) borrowed concepts from existing BDD frameworks, such as RSpec and Shoulda. Test cases are created using expectations instead of assertions (see Code Example - mini/spec below for details).

Because Mini::Test was based on familiar frameworks, using it is realtively intuitive. As creator, Ryan Davis says, "There is no magic". (View Ryan's presentation on Mini::Test at the Cascadia Ruby 2011 conference.) Mini::Test is simple and straightforward.

Functionality

Most of the assertions in Mini::Test::TestCase are the same as those in its predecessor with some new ones added. The most notable difference is in the negative assertions. In Test::Unit where you have an assert_not_something method, Mini::Test gave them a streamlined name of a refute_something method. (assert_not_raise and assert_not_throws are no longer available.) Mini::Test::Unit provides the following assertions:


assert assert_block assert_empty refute refute_empty
assert_equal assert_in_delta assert_in_epsilon refute_equal refute_in_delta refute_in_epsilon
assert_includes assert_instance_of assert_kind_of refute_includes refute_instance_of refute_kind_of
assert_match assert_nil assert_operator refute_match refute_nil refute_operator
assert_respond_to assert_same assert_output refute_respond_to refute_same
assert_raises assert_send assert_silent assert_throws


While mini/spec contains the following expectations:


must_be must_be_close_to must_be_empty wont_be wont_be_close_to wont_be_empty
must_be_instance_of must_be_kind_of must_be_nil wont_be_instance_of wont_be_kind_of wont_be_nil
must_be_same_as must_be_silent must_be_within_delta wont_be_same_as wont_be_within_delta
must_be_within_epsilon must_equal must_include wont_be_within_epsilon wont_equal wont_include
must_match must_output must_raise wont_match
must_respond_to must_send must_throw wont_respond_to

Additional Features

Aside from the API improvements, Mini::Test also provides some additional features such as test randomization. In unit testing, tests should run independent from each other (i.e. the outcome or resulting state(s) of one test should not affect another). By randomizing the order, Mini::Test prevents tests from becoming order-dependent. Should you need to repeat a particular order to test for such issues, Mini::Test provides the current seed as part of the output and gives you the option to run the test using this same seed.

Mini::Test also gives you the ability to skip tests that are not working correctly (for debug at a later time) as well as additional options for determining the performance of your test suite.

Code Example - Mini::Test:Unit

We follow the same steps to create a test case for our Account class that we did in Test::Unit Code Example , except that this class inherits from MiniTest:Unit::TestCase and requires 'minitest/autorun'. (Requiring minitest/unit does not cause the test methods to be automatically invoked when the test case is run, hence we use minitest/autorun which provides this functionality.) We also need to update the assertions to those provided with MiniTest - for this class the assert_not_nil was changed to refute_nil. The AccountTest test case is below.


require 'minitest/autorun'
require_relative 'account.rb'

class AccountTest < MiniTest::Unit::TestCase

  def setup
    @a = Account.new(100)
  end

  def test_deposit
    assert_equal(200, @a.deposit(100))
  end
 
  def test_withdrawal
    assert_equal(50, @a.withdrawal(50))
  end
 
  def test_name
    @a.name = "Checking"
    refute_nil(@a.name())
    assert_match("Checking", @a.name)
  end

  def test_interest
    assert_in_delta(130, @a.addinterest(0.333), 5)
  end

  def test_fail
    assert_equal(200, @a.balance())
  end
 
  def test_whatru
    assert_instance_of(Account, @a)
  end
     
end

Executing this test case gives the following results, showing that 6 tests were run with 1 faliure. Information is provided about the failure, including test name, expected output and actual output. Note that the seed value of the random test run is also provided.

Loaded suite C:/Users/Tracy2/Desktop/NCSU/CSC 541/Workspace/Account/test_account2
Started
.F....
Finished in 0.001000 seconds.

  1) Failure:
test_fail(AccountTest) [C:/Users/Tracy2/Desktop/NCSU/CSC 541/Workspace/Account/test_account2.rb:30]:
Expected 200, not 100.

6 tests, 8 assertions, 1 failures, 0 errors, 0 skips

Test run options: --seed 13360

Code Example - mini/spec

To create a test case using mini/spec, test assertions are changed to spec expectations. The format for each "test" becomes:

 describe "test" do
   it "should do something"
      spec expectation statment
   end
 end

This is the behaviour driven method of stating expectations instead of testing result values. The method def is replaced with a describe/it "should..." statement. Instead of using assert statements to check the action performed on the object, the action is performed and required (expected) to meet a given criteria. The setup and teardown functionalities are implemented using "before do" and "after do". The inherit statement is no longer used, but the require 'minitest/autorun' is. Each method is then converted to its spec counterpart using the above format and the appropriate expectations. The mini/spec test for the Account class is below.

 require 'minitest/autorun'
 require_relative 'account.rb'

 describe Account do

   before do
      @a = Account.new(100)
   end

   describe "deposit" do
     it "should add amount to balance" do
       @a.deposit(100).must_equal 200
     end
   end
 
   describe "withdraw" do
     it "should subtract amount from balance" do
       @a.withdrawal(50).must_equal 50
     end
   end
 
   describe "set name" do
     it "should set account name" do
       @a.name = "Checking"
       @a.name.wont_be_nil
       @a.name.must_match "Checking"
     end
   end
 
   describe "interest" do
     it "should add interest to balance" do
       @a.addinterest(0.333).must_be_within_delta(130, 5)
     end
   end

   describe "fail" do
     it "should fail" do
       @a.balance.must_equal 200
     end
   end

   describe "what are you" do
     it "should be an instance of Account" do
       @a.must_be_instance_of Account
     end
   end

 end

The results are similar to previous tests, giving information on failures. Note that once again the randomization seed is given (and is different from previous test case).

Loaded suite C:/Users/Tracy2/Desktop/NCSU/CSC 541/Workspace/Account/test_account3
Started
.F....
Finished in 0.003000 seconds.

  1) Failure:
test_0001_should_fail(AccountSpec::FailSpec) [C:/Users/Tracy2/Desktop/NCSU/CSC 541/Workspace/Account/test_account3.rb:39]:
Expected 200, not 100.

6 tests, 8 assertions, 1 failures, 0 errors, 0 skips

Test run options: --seed 21728

Back to top

RSpec

Overview

RSpec is a BDD testing framework that is built on top of Unit::Test and is one of the more popular unit testing frameworks<ref name = rubytool />. While it provides much of the same unit testing functionality, it's syntax is much different than that of Unit::Test. First of all, as a BDD framework, RSpec does not have test methods but rather a set of sentences describing each test case. Nested in each sentence is the code to complete the test. All of these sentences are contained under a "describe" block which houses sentences testing a common functionality or feature, similar to a test case class in Unit::Test. It also provides verbose test output which can be exported in a number of ways, including a requirements list.

Functionality

As mentioned above, each individual test is described by a sentence. These sentences start with the word "it", followed by a description of functionality that is to be tested, ending with a "do". Under each sentence, the actual testing code is written, where it is checked against a "should" statement. The describe method contains the plain english name of the function that we are testing, followed by all of the test sentences. RSpec also support before and after blocks which allow for consolidating common code that would normally be replicated in each test sentence. This could be used, for instance, to initialize the object under test for each test sentence all at once, as shown below. It also supports marking a test as pending which allows the developer to specify unfinished or broken functionality without failing a test.

Instead of using assert methods and it's variants, RSpec uses should statements. These statements consist of the object under test, followed by the method under test, the should modifier, and a query method or comparison. For example, the following code would test to see if the mileage method of the car object returns 12000.

 @car.mileage.should == 12000

It is this format that makes it very intuitive as to which is the expected and which is the actual value. In the case of assert_equal in Unit::Test, this is not immediately clear as the differentiation is positional. For a more extensive display of RSpec's syntax, we have the example code below.

Code Example

In order to generate the following output, the command rspec is run against the test file.

 require_relative("../Account.rb")
 
 describe "The Account" do  
   before(:each) do
     @a = Account.new(100)
   end
   
   it "should be created with a balance" do
     @a.balance.should == 100
   end
 
   it "should take a deposit" do
     @a.deposit(100).should == 200
   end
   
   it "should be capable of withdrawals" do
     @a.withdrawal(50).should == 50
   end
   
   it "should have a name" do
     @a.name = "Checking"
     @a.name.should == "Checking"
   end
 
   it "should calculate interest" do
     @a.addinterest(0.5).should == 150
   end
 
   it "should have a failure here as an example" do
     @a.balance.should == 200
   end
 
   it "should provide a bank statement" do
     pending "Not yet implemented"
   end
 end

Running the above code with the '--format doc' option we will get the output:

 The Account
   should be created with a balance
   should take a deposit
   should be capable of withdrawals
   should have a name
   should calculate interest
   should have a failure here as an example (FAILED - 1)
   should provide a bank statement (PENDING: Not yet implemented)
 
 Pending:
   The Account should provide a bank statement
     # Not yet implemented
     # ./AccountTest-rspec.rb:33
 
 Failures:
 
   1) The Account should have a failure here as an example
      Failure/Error: @a.balance.should == 200
        expected: 200
             got: 100 (using ==)
      # ./AccountTest-rspec.rb:30:in `block (2 levels) in <top (required)>'
 
 Finished in 0.00132 seconds
 7 examples, 1 failure, 1 pending
 
 Failed examples:
 
 rspec ./AccountTest-rspec.rb:29 # The Account should have a failure here as an example

Take note of the test sentences printed out in the documentation format at the top, each listing the status of the test in a humanly readable fashion. We also have more verbose output on the tests which failed and are pending.

Back to top

Shoulda

Overview

Shoulda isn't a complete testing framework - instead it works inside the existing frameworks of Test::Unit or RSpec. It combines the descriptive abilities of a BDD framework (on a scaled-down version) along with the unit tests of a TDD framework. A Shoulda test case can be comprised of Shoulda tests along with regular Test:Unit tests and RSpec tests. Shoulda gives programmers the ability to convert existing Test::Unit test suites with little work while using the Shoulda language to describe the tests. To install Shoulda run gem install shoulda

Functionality

Since Shoulda test cases are built on top of Test::Unit, all the assertions provided in Test::Unit are available in Shoulda. Shoulda test cases are a combination of descriptive statements and assertions. The basic test format is very simple and straight forward.

should "do something" do
  assert_whatever
end

Shoulda test cases require 'test/unit' as well as 'shoulda'. Shoulda provides setup do and teardown do as well as context do methods. Contexts give the programmer the ability to create blocks of code that have something in common. Each context can have its own setup and contexts can be nested. This means that test cases can perform setup for all tests and an additional setup that applies to just a subset of the tests. The format for the context statement is similar to that of the should statement:

context "Test Group 1" do
  ...
end

Code Example

Since Shoulda creates Test::Unit methods for each should block<ref name = progruby />, AccountTest must inherit from Test::Unit::TestCase. Each test is a combination of descriptive should statements and Test::Unit unit tests. In this example, we have added a unit test method - assert_correct - and then used that assertion within the Shoulda framework. This code also contains two contexts - with one nested inside the other. The first provides the setup to create the account (needed for all tests), while the second creates a name for the account (needed only for the "Have Name" test).

 require 'test/unit'
 require 'shoulda'
 require_relative 'account.rb'

  class AccountTest < Test::Unit::TestCase

    def assert_correct(target)
        assert_equal(target, @a.balance)
    end

    context "Basic Account Functions" do
      setup do
        @a = Account.new(100)
      end
  
      should "Have correct amount in Account" do
        assert_correct(100)
      end  

      should "Deposit funds correctly" do
        assert_equal(200, @a.deposit(100))
      end
   
      should "Withdraw funds correctly" do
        assert_equal(50, @a.withdrawal(50))
      end
   
      should "Add interest to account correctly" do
        assert_in_delta(130, @a.addinterest(0.333), 5)
      end
 
      should "Fail Test" do
        assert_equal(200, @a.balance())
      end
   
      should "be an instance of Account" do
        assert_instance_of(Account, @a)
      end
   
      context "Add Name Testing" do
        setup do
         @a.name = "Checking"
        end
     
        should "Have name" do
          assert_not_nil(@a.name())
          assert_match("Checking", @a.name())
        end
      end
   end
 end

This gives us the familiar results below.

 Loaded suite C:/Users/Tracy2/Desktop/NCSU/CSC 541/Workspace/Account/test3a
 Started
 ...F...
 Finished in 0.001000 seconds.

   1) Failure:
 test: Basic Account Functions should Fail Test. (AccountTest) [C:/Users/Tracy2/Desktop/NCSU/CSC 541/Workspace/Account/test3a.rb:33]:
 <200> expected but was
 <100>.

 7 tests, 8 assertions, 1 failures, 0 errors, 0 skips

 Test run options: --seed 25476

Back to Top

Cucumber

Overview

Cucumber is a behavior driven development framework. Cucumber performs automated tests that have been developed using functional descriptions. The language that Cucumber uses is Gherkin. Gherkin files use a set of special keywords to form its structure. In its basic form, a Gherkin form looks something like the following:

Feature: This is the feature title
   This is the description of the feature, which can
   span over multiple lines.

Scenario: What is going to happen
   Given this state  #the context
   When this happens #the event
   Then this state   #the outcomes (s)

This is then translated into a regular expression for each of the step definitions. For each regular expression, the programmer then defines the action he would like for his program to implement. This is known as working from the outside-in. Once these definitions and actions have been established, then the class can be built. Cucumber is quite involved. It is not just a testing framework, however, it is a total approach to programming. According to the Cucumber website, the basic process is:

  1. Describe behavior in plain text
  2. Write a step definition in Ruby
  3. Run and watch it fail
  4. Write code to make the step pass
  5. Run again and see the step pass
  6. Repeat steps 2-5 until green like a cuke
  7. Repeat 1-6 until the money runs out<ref>http://cukes.info/</ref>

There are a number of Cucumber resources including The Cucumber Book.

Code Example

In order to perform just a basic test on our Account class, we describe the desired outcome in a features file.

 Feature: Account Test Feature
     In order to test my Account program
     As a Developer
     I want to perform Account functions

 Scenario: Withdrawal Scenario
     Given I have $100 in my account
     When I withdraw $50 from my account
     Then I should have $50 left in my account

We then write regular expressions to define these steps and test the outcome of our withdrawal function.

 require 'rspec/expectations'
 require_relative '../../account.rb'

 Given /^I have \$(\d+) in my account$/ do |amount|
   @a = Account.new(amount.to_i)
 end

 When /^I withdraw \$(\d+) from my account$/ do |withdraw|
   @a.withdrawal(withdraw.to_i)
 end

 Then /^I should have \$(\d+) left in my account$/ do |balance|
   balance.to_i.should == @a.balance
 end

From this we get the following output:

There is quite a bit of setup to run Cucumber. There is a very easy to follow tutorial at The Secret Ninja Cucumber Scrolls.

Back to top

Criteria

  • IDE integration
  • Test output detail
  • Testing constructs available
  • TDD or BDD
  • Documentation
  • Learning curve and ease of use


Framework Matrix

Framework Website Documentation IDE Integration Type Ease of Use
Unit::Test RubyDoc Eclipse, RubyMine TDD Easy to learn; Use difficulty increases with project size
MiniTest::Unit GitHub RubyDoc Eclipse TDD/BDD Easy to learn; Easy to use
RSpec http://rspec.info/ http://rspec.info/ Eclipse, RubyMine BDD Slightly more difficult to learn; Easy to use
Shoulda GitHub RubyDoc Eclipse Easy to learn; Easy to use
Cucumber Cucumber Cucumber Wiki None BDD Difficult to learn; Difficult to use

References

<references> <ref name = dannorth> http://dannorth.net/introducing-bdd </ref> <ref name = rubytool> http://ruby-toolbox.com/categories/testing_frameworks.html </ref> <ref name = progruby> Programming Ruby: The Pragmatic Programmers' Guide, Second Ed. </ref> </references>