RubyAndRailsTesting/ruby-rails-testing - Igor Polevoy Java Wiki

quaggahooliganInternet and Web Development

Feb 5, 2013 (4 years and 2 months ago)

140 views

Ruby/Rails Testing

Why test??


How else do you know that the code does what
it should?


How else do you know what the code should
do?


How do you document code? Tests do not lie


How do you tell other people how to use your
code?

TDD misunderstood


Write production code


Write some tests to cover it


Check coverage reports


Have a drink, feel good about yourself :)



Get a pager in the middle of a night :(


Why? Because:


you wrote tests with a good understanding of the internals of
what you are testing (this is actually bad)



You tailored tests to the internal logic of your code


You did not foresee that a different scenario under which
your code could be executed

TDD,‏“outside‏in”


(1) Visualize goals


(2) Write contract (tests before any code), do
not have to write all tests for entire system, just
one unit/story/feature


(3) Run tests, see them fail


(4) Write just enough production code to make
failed tests go away


Repeat

Good TDD is really a BDD


Dave Astels wrote this interesting paper on the

virtues of TDD/BDD:


http://blog.daveastels.com/files/BDD_Intro.pdf


BDD == Specification, not verification


Blueprint that is also executable and verifiable.


General Testing Terminology


Unit test


test‏of‏a‏small‏“unit”‏of‏the‏system‏
(class, module, etc)



Integration test


several‏“units”‏are‏combined‏
into the same test


Functional test


test of system function as
provided in the specification


system
behavioral test

Rails testing terminology


Unit test == model test


Functional test == controller test


Integration test == story test (test across many
controllers)



Acceptance test == from point of view of
business (could be automated)


Rails test frameworks


Test::Unit


baked in


old style verification
(asserts)


RSpec


needs to be installed (soon to be
baked in)


-

new style expectations
(should(_not))

Rails tests are baked in (Test::Unit)



./script/generate scaffold User first_name:string last_name:string
\

ssn:string


create test/
functional
/
users_controller_test.rb




create test/
unit
/
user_test.rb


create test/
fixtures/users.yml


Automatically generates controller, unit test, as well as YML file for fixtures.

Directories:


fixtures/


functional/


integration/


unit/

Test::Unit


test case = subclass of Test::Unit::TestCase


test = public method of a test case, name beginning with test_ (latest version of Rails allows
it

method)



Test names should be descriptive.


Each test should test as small a piece of functionality as possible.


assertion = comparison of an expected result against an actuality


setup

method = code that runs before each test method.


teardown

method = code that runs after each test method.


test result


success = test succeeds


failure = an assertion failed


error = an exception or runtime error halted execution


green bar = successful test run


red bar = failed test run

Fixtures


one:


name: "The Ruby Programming Language"


isbn: "
0596516177
"


author: "David Flanagan and Yukihiro Matsumoto"


two:


name: "Programming Ruby
1.9
: The Pragmatic Programmers Guide"


isbn: "
1934356085
"


author: "Dave Thomas (Author), Chad Fowler, Andy Hunt"

Fixtures as Ruby templates

#Fixtures are YML files and Ruby
#templates:root/test/fixtures/comments.yml


<%
1
.upto(
10
) do | counter | %>

comment<%=counter%>:


text: MyString<%=counter%>


date:
2009
-
05
-
12

<%end%>



#after that:

puts comments(:comment
1
).text

puts comments(:comment
1
).date

puts comments(:comment
2
).text

puts comments(:comment
2
).date

...


Fixtures: general rule


Do not get carried away :)



Do not let your project turn into fixture
maintenance nightmare


When the tests are executed fixture data is
loaded into DB if fixtures method is used


Test DB is used, of course (not development),
see
database.yml

Test Transactional Isolation


simple_test/test/test_helper.rb


self.use_transactional_fixtures = true


All tests generating records in the DB will run in
the context of a transaction. One transaction
per Unit test.


At the end of a test execution, the transaction is
rolled back. This means that going from unit test
to unit test,
the only records in the DB are those
from fixtures
. This is good data isolation for
tests.

Execute tests

ruby test/unit/user_test.rb


(Run one test)



ruby test/unit/user_test.rb
-
n test_user_validity


(Run a specific test in the given file.)



ruby test/unit/user_test.rb
-
n /valid/


(Run any test with a name matching the given regular expression)



rake test:units


(Run the tests in test/unit)



rake test:functionals


(Run the tests in test/functional)



rake test:integration


(Run the tests in test/integration)



rake test


(Run all Unit:Test classes)


Test structure


setup


exercise


verify (assert expectations)



teardown

Test Example

class UserTest < Test::Unit::TestCase


fixtures :users


def setup

@error_messages = ActiveRecord::Errors.default_error_messages

@valid_user = users(:april)

@invalid_user = users(:invalid_user)



end


def test_user_validity

assert @valid_user.valid?


end


def test_user_invalid

assert !@invalid_user.valid?

assert @invalid_user.errors.invalid?(:username)


end

end

Test::Unit assertions


assert_block message block

assert_empty collection

assert_equal reference_value, value_to_check

assert_not_equal reference_value, value_to_check

assert_equal "fred" @user.name

assert_in_delta expected, actual, delta, message=""

assert_include item, collection

assert_instance_of class, obj

assert_instance_of User, @user

assert_kind_of class_or_module, obj

assert_kind_of Enumerable, @things

assert_match regexp, string

assert_no_match regexp, string

assert_match /
\
d{
3
}
\
.
\
d{
3
}
\
.
\
d{
4
}/ "
312.362.8000
"

assert_nil obj

assert_not_nil obj

assert_not_nil @user

assert_same ref
1
, ref
2

assert_not_same ref
1
, ref
2

assert_raise *exceptions block

assert_nothing_raised *exceptions block

assert_respond_to object, message

assert_respond_to @user, :name

flunk message

...

Test::Unit integration tests


Install:


ruby script/generate integration_test user_comment


Methods:
get, post, put, delete, head, xhr


Methods‏allow‏to‏“navigate”‏application‏as‏a‏
browser would


No network traffic, all simulated

Integration test assertions


assert_response


Asserts the HTTP status code of an HTTP request.



def test_login_action_succeeds


get :login


assert_response :success


end


Possible arguments are



* :success = Status code was
200


* :redirect = Status code was in range
300
-
399


* :missing = Status code was
404


* :error = Status code was in range
500
-
599


You can use literal numbers: assert_response
200


Integration test asserts


def test_should_not_get_new_without_login


get :new


assert_redirected_to

:controller => :user, :action => :login

end


def test_login_action_succeeds


get :login


assert_response :success


assert_template

"login"

end


def test_update_action_fails


login_user


put :update, :id => id, :item => @invalid_item


assert_template

"edit"

end


assert_difference
'Article.count' do


post :create, :article => {...}

end


assert_difference
'Article.count',
-
1
do


post :delete, :id => ...

end


Integration test assert_select

assert_select(selector,equality?,message?)


assert_select(element,selector,equality?,message?)



# At least one form element

assert_select "form"


# Form element includes four input fields

assert_select "form input",
4


# Page title is "Welcome"

assert_select "title", "Welcome"


# Page title is "Welcome" and there is only one title element

assert_select "title", {:count=>
1
, :text=>"Welcome"},


"Wrong title or more than one title element"


# Page contains no forms

assert_select "form", false, "This page must contain no forms"


# Test the content and style

assert_select "body div.header ul.menu"


...and many more

Refer to: http://cheat.errtheblog.com/s/assert_select/

Test routing

assert_generates

assert_recognizes

assert_routing


.. see code example


assert_routing(path, options, defaults={}, extras={}, message=nil)


Test Views


it "test structure of index page" do


assigns[:books] = [@book]


render 'books/index'


response.should have_tag('table')


response.should have_tag('table') do


with_tag('tr') do


with_tag('td', @title)


with_tag('td', @author)


with_tag('td', @isbn)


end


end


assert_select "a",
1
..
4
#
-

small expectation, expect
1
to
4
anchors.


end


Mimics assigns
as if they
came from
controller

RSpec


Inspired by Jbehave


Mocking based on Jmock


Current version 1.3.0


Soon‏to‏be‏“standard”‏BDD‏framework‏in‏Rails


Site: http://rspec.info/

RSpec installation


...turned out to be a hedache



ruby script/plugin install git://github.com/dchelimsky/rspec.git
-
r 'refs/tags/1.2.6'

ruby script/plugin install git://github.com/dchelimsky/rspec
-
rails.git
-
r 'refs/tags/1.2.6'

ruby script/generate rspec

RSpec continued


script/generate rspec_model Book name:string isbn:string author:string


exists app/models/


exists spec/models/


create spec/fixtures/


create app/models/book.rb


create spec/models/book_spec.rb


create spec/fixtures/books.yml


exists db/migrate


create db/migrate/20090517223213_create_books.rb


Simple RSpec

describe Account, " when first created" do



before do


@account = Account.new


end



it "should have a balance of $
0
" do


@account.balance.should eql(Money.new(
0
, :dollars))



end



after do


@account = nil


end


end


* before(:each) do ... end

* before(:all) do ... end

* describe
-

returns an ExampleGroup object, grouping together examples.

* it
-

returns an Example object that describes behavior.

* before/#after
-

like setup/teardown. Run before and after each example.

RSpec Expectation API

@user.name.should == "Fred"

@user.should === something

@user.name.should =~ /^Fre/


x.should be_true

x.should be_false

x.should be_nil

x.should_not be_nil


x.should be_valid # passes if x.valid? is true

collection.should be_empty

user.should_not be_under_age(
13
)



user.age.should be_a_kind_of(Fixnum)


[
1
,
2
,
3
].should include(
1
)


"foobar".should include("bar")


[
1
,
2
,
3
].should have(
3
).items


RSpec custom matchers

module BookMathers


class HaveAuthor


def initialize(expected)



end



def matches?(target)



@target = target


target.author != nil


end



def failure_message


"Book: #{@target} should have an author"


end


def negative_failure_message


"Author of: #{@target} should not be nil"


end


end


def have_author


HaveAuthor.new(nil)



end

end


it "should have author" do


b = Book.new :name => 'Tufted Titmouse (Wild Bird Guides) (Paperback)', :author =>










'Thomas C. Grubb',
:isbn => '
0811729672
'


b.should have_author #<<<<<<<<<<<<<<<<<<<<< using a custom matcher

end

RSpec Cheat Sheet


http://jay.mcgavren.com/blog/archives/786

by
Jay McGavren

Shared example groups

describe "all editions", :shared => true do


it "should behave like all editions" do


end

end


describe "SmallEdition" do


it_should_behave_like "all editions"



it "should also behave like a small edition" do


end

end


describe "LargeEdition" do


it_should_behave_like "all editions"



it "should also behave like a large edition" do


end

end

Pending examples




it "should save valid user"



it "should save valid user" do


pending("need to sort out the user requirements")



end


Mocking/stubbing

my_mock = mock("person")


my_mock = mock("person, :null_object => true) # ignores all messages,
returns itself on all calls

my_stub = stub("whatever", :name => "Fred") # my_stub.name returns
"Fred"


#mocking ActiveRecord:

user = User.new

User.should_receive(:find).with(id).and_return(user)



# Use block for a fine degree of control of a mock behavior

my_mock.should_receive(:sym) do |arg|


arg.should be_an_instance_of(Array)



arg.length.should ==
7

end


#Stubbing

obj.stub!(:to_i).and_return(
99
)




RULE!:if a mocked methods is not called during an example, the test will
fail
-

this one of the differences of stubs vs mocks

Mocks aren't stubs

courtesy of Martin Fawler

http://www.martinfowler.com/articles/mocksArentStubs.htm
l

Mock example

Mocking
YahooShoppingService.search

because this is outside service

and you do
not

want to access external or internal services during the build.


before(:all)



@results = [ {...},{...}] #details omitted

end


it “..” do


@searcher.should_receive(:search).and_return(@results)


#test class which depends on Yahoo searcher


...

end


show Demo

Running

rake spec

..or

rake

# since rails
2.3.2


..or

ruby script/spec spec/dir/

ruby script/spec spec/dir/specific_spec.rb


...and


rake spec:models

rake spec:controllers


...and many more

Cucumber


Cucumber‏is‏Aslak‏Hellesøy’s‏rewrite‏of‏
RSpec’s‏“Story‏runner”,‏which‏was‏originally‏
written by Dan North. (Which again was a
rewrite of his first implementation
-

“RBehave”).‏
Early‏versions‏of‏“Story‏runner”‏required‏that‏
stories be written in Ruby. Shortly after, David
Chelimsky added plain text support with
contributions from half a dozen other people.


Taken from: http://cukes.info/

Plain text feature

(even a business person can write this

)


STEP 1 (in a file account.feature)


Feature: Account


In order to have cash


I want to withdraw money from account



Scenario: Withdrawing money from account


Given account balance of 200.00


When I withdraw 50.00


Then the balance would be 150.00


And I should have 50.00 dollars



Execute Cucumber

STEP
2


Yellow output


UUUU


1
scenario (
1
undefined)


4
steps (
4
undefined)



You can implement step definitions for undefined steps with these snippets:


Given /^account balance of
200
\
.
00
$/ do


pending

end


When /^I withdraw
50
\
.
00
$/ do


pending

end


Then /^the balance whould be
150
\
.
00
$/ do


pending

end


Then /^I should have
50
\
.
00
dollars$/ do


pending

end


Write step definitions

STEP
3


file account_steps.rb:


require 'spec/expectations' # so we can call .should

require File.expand_path(File.dirname(__FILE__) + '/../../src/account')



Given /^account balance of (.*)$/ do |amount|


@account = Account.new(amount.to_f)



end


When /^I withdraw (.*)/ do |amount|


@account.withdraw amount.to_f


end


Then /^the balance whould be (.*)$/ do |balance|


@account.amount.should == balance.to_f

end


Then /^I should have (.*) dollars$/ do |cash|


cash.to_f.should ==
50.0

end

Execute Cucumber

STEP
4

$cucumber

no such file to load
--

/home/igor/cucumber/examples/simple
-
cucumber
-
example/src/account

Failed to load features/step_definitons/account_steps.rb (LoadError)



...more errors


Apparently a class Account is missing

Create Account class

STEP
5

class Account




attr_reader :amount





def initialize(amount)






@amount = amount



end




def withdraw(amount)





@amount = @amount
-

amount



end

end

Finish Account

STEP
6


$cucumber

....


1
scenario (
1
passed
)


4
steps (
4
passed
)





Start new feature with step
1

Getting Rails on Cucumber/Webrat

$sudo gem install rspec rspec
-
rails cucumber webrat

$rails
-
d mysql cucumber
-
example

# adjust database.yml if necessary

$cd cucumber
-
example/



$rake db:create:all

$rake db:migrate

$ruby script/generate cucumber

$./script/generate scaffold Task description:string

$rake db:migrate

./script/server


Rails on Cucumber

Feature file:


Feature: Tasks

In order to keep track of tasks

People should be able to

Create a list of tasks


Scenario: List Tasks

Given that I have created a task "task
1
"

When I go to the tasks page

Then I should see "task
1
"


Step definition file:

Given /^that I have created a task "(.*)"$/ do |desc|


Task.create!(:description => desc) #<<<<< ActiveRecord

end


When /^I go to the tasks page$/ do


visit "/tasks" #<<<<< Webrat

end


Then /^I should see "(.*)"$/ do |text|


response.body.should =~ /#{text}/m

end



Demo Cucumber


Standalone app


Rails app

Possibilities are endless


Can you write all tests in RSpec? Yes. Real
benefit of Cucumber is having people
completely unfamiliar with code write actionable
testable scenarios (priceless)


Test Java with JRuby + Cucumber


Test anything that easy integrates with Ruby


Might need organizational changes in
development groups
-

BAs writing features and
stories.

BDD is an ocean


Start small, work a habit of writing
feature/story/test first, before production code is
written


Use Selenium for JS


heavy sites


?