Basic ruby testing environment (hardmock, behaviors, rake tasks).

git-svn-id: http://cmock.svn.sourceforge.net/svnroot/cmock/trunk@6 bf332499-1b4d-0410-844d-d2d48d5cc64c
This commit is contained in:
mkarlesky
2008-06-01 18:33:36 +00:00
parent 42fba80136
commit 5cada7dbb9
52 changed files with 4901 additions and 44 deletions
+2
View File
@@ -3,6 +3,8 @@ ROOT_PATH = File.expand_path(File.dirname(__FILE__) + "/../")
# Setup our load path:
[
'lib',
'vendor/behaviors/lib',
'vendor/hardmock/lib',
].each do |dir|
$LOAD_PATH.unshift(File.join(ROOT_PATH, dir))
end
+21 -3
View File
@@ -4,6 +4,24 @@ require 'rake'
require 'rake/clean'
require 'rake/testtask'
task :default do
sh 'spec.cmd test\unit'
end
task :default => [ 'tests:all' ]
namespace :tests do
desc "Run unit and system tests"
task :all => [ 'units', 'system' ]
Rake::TestTask.new('units') do |t|
t.pattern = 'test//unit/*_test.rb'
t.verbose = true
end
Rake::TestTask.new('system') do |t|
t.pattern = 'test//system/*_test.rb'
t.verbose = true
end
end
+29
View File
@@ -0,0 +1,29 @@
require File.expand_path(File.dirname(__FILE__)) + "/../test_helper"
class Thing
def initialize foo, bar
@foo = foo
@bar = bar
end
def snafu
return @foo.val + @bar.val
end
end
class ThingTest < Test::Unit::TestCase
def setup
create_mocks :foo, :bar
@thing = Thing.new(@foo, @bar)
end
def teardown
end
should "perform a stupid simple test" do
@foo.expect.val.returns(1)
@bar.expect.val.returns(2)
assert_equal(3, @thing.snafu)
end
end
+9
View File
@@ -0,0 +1,9 @@
require File.expand_path(File.dirname(__FILE__)) + "/../config/environment"
require 'test/unit'
require 'behaviors'
require 'hardmock'
class Test::Unit::TestCase
extend Behaviors
end
-41
View File
@@ -1,41 +0,0 @@
$here = File.dirname(__FILE__)
require "#{$here}/../../config/environment"
require 'cmock'
describe CMock do
before(:each) do
@interface_parser = mock('InterfaceParser')
@cmock = CMock.new('mocks', [], @interface_parser)
end
it "should default mocks path to 'mocks'" do
@cmock.mocks_path.should == 'mocks'
end
it "should allow mocks path to be specified in constructor" do
@cmock = CMock.new('yoohoo')
@cmock.mocks_path.should == 'yoohoo'
end
it "should default includes to empty array" do
@cmock.includes.should == []
end
it "should allow includes to be specified in constructor" do
includes = ['fun', 'stuff']
@cmock = CMock.new('blah', includes)
@cmock.includes.should == includes
end
it "should generate a mock module" do
@cmock.should respond_to(:generate)
end
it "should delegate to parser to extract interface" do
module_header = 'my_module.h'
@interface_parser.should_receive(:extract_interface).with(module_header)
@cmock.generate(module_header)
end
end
+29
View File
@@ -0,0 +1,29 @@
require File.expand_path(File.dirname(__FILE__)) + "/../test_helper"
class Thing
def initialize foo, bar
@foo = foo
@bar = bar
end
def snafu
return @foo.val + @bar.val
end
end
class ThingTest < Test::Unit::TestCase
def setup
create_mocks :foo, :bar
@thing = Thing.new(@foo, @bar)
end
def teardown
end
should "perform a stupid simple test" do
@foo.expect.val.returns(1)
@bar.expect.val.returns(2)
assert_equal(3, @thing.snafu)
end
end
+9
View File
@@ -0,0 +1,9 @@
Manifest.txt
Rakefile
lib/behaviors.rb
lib/behaviors/reporttask.rb
test/behaviors_tasks_test.rb
test/behaviors_test.rb
test/tasks_test/lib/user.rb
test/tasks_test/Rakefile
test/tasks_test/test/user_test.rb
+19
View File
@@ -0,0 +1,19 @@
require 'rake'
require 'rubygems'
require 'hoe'
Hoe.new('behaviors','1.0.3') do |p|
p.author = "Atomic Object LLC"
p.email = "dev@atomicobject.com"
p.url = "http://behaviors.rubyforge.org"
p.summary = "behavior-driven unit test helper"
p.description = <<-EOS
Behaviors allows for Test::Unit test case methods to be defined as
human-readable descriptions of program behavior. It also provides
Rake tasks to list the behaviors of your project.
EOS
p.test_globs = ['test/*_test.rb']
p.changes = <<-EOS
EOS
end
+76
View File
@@ -0,0 +1,76 @@
=begin rdoc
= Usage
Behaviors provides a single method: should.
Instead of naming test methods like:
def test_something
end
You declare test methods like:
should "perform action" do
end
You may omit the body of a <tt>should</tt> method to describe unimplemented behavior.
should "perform other action"
When you run your unit tests, empty <tt>should</tt> methods will appear as an 'UNIMPLEMENTED CASE' along with the described behavior.
This is useful for sketching out planned behavior quickly.
Simply <tt>extend Behaviors</tt> in your <tt>TestCase</tt> to start using behaviors.
require 'test/unit'
require 'behaviors'
require 'user'
class UserTest < Test::Unit::TestCase
extend Behaviors
...
end
= Motivation
Test methods typically focus on the name of the method under test instead of its behavior.
Creating test methods with <tt>should</tt> statements focuses on the behavior of an object.
This helps you to think about the role of the object under test.
Using a behavior-driven approach prevents the danger in assuming a one-to-one mapping of method names to
test method names.
As always, you get the most value by writing the tests first.
For a more complete BDD framework, try RSpec http://rspec.rubyforge.org/
= Rake tasks
You can define a <tt>Behaviors::ReportTask</tt> in your <tt>Rakefile</tt> to generate rake tasks that
summarize the behavior of your project.
These tasks are named <tt>behaviors</tt> and <tt>behaviors_html</tt>. They will output to the
console or an html file in the <tt>doc</tt> directory with a list all of your <tt>should</tt> tests.
Behaviors::ReportTask.new do |t|
t.pattern = 'test/**/*_test.rb'
end
You may also initialize the <tt>ReportTask</tt> with a custom name to associate with a particular suite of tests.
Behaviors::ReportTask.new(:widget_subsystem) do |t|
t.pattern = 'test/widgets/*_test.rb'
end
The html report will be placed in the <tt>doc</tt> directory by default.
You can override this default by setting the <tt>html_dir</tt> in the <tt>ReportTask</tt>.
Behaviors::ReportTask.new do |t|
t.pattern = 'test/**/*_test.rb'
t.html_dir = 'behaviors_html_reports'
end
=end
module Behaviors
def should(behave,&block)
mname = "test_should_#{behave}"
if block
define_method mname, &block
else
puts ">>> UNIMPLEMENTED CASE: #{name.sub(/Test$/,'')} should #{behave}"
end
end
end
+158
View File
@@ -0,0 +1,158 @@
require 'rake'
require 'rake/tasklib'
module Behaviors
include Rake
class ReportTask < TaskLib
attr_accessor :pattern
attr_accessor :html_dir
def initialize(name=:behaviors)
@name = name
@html_dir = 'doc'
yield self if block_given?
define
end
def define
desc "List behavioral definitions for the classes specified (use for=<regexp> to further limit files included in report)"
task @name do
specifications.each do |spec|
puts "#{spec.name} should:\n"
spec.requirements.each do |req|
puts " - #{req}"
end
end
end
desc "Generate html report of behavioral definitions for the classes specified (use for=<regexp> to further limit files included in report)"
task "#{@name}_html" do
require 'erb'
txt =<<-EOS
<html>
<head>
<style>
div.title
{
width: 600px;
font: bold 14pt trebuchet ms;
}
div.specification
{
font: bold 12pt trebuchet ms;
border: solid 1px black;
width: 600px;
padding: 5px;
margin: 5px;
}
ul.requirements
{
font: normal 11pt verdana;
padding-left: 0;
margin-left: 0;
border-bottom: 1px solid gray;
width: 600px;
}
ul.requirements li
{
list-style: none;
margin: 0;
padding: 0.25em;
border-top: 1px solid gray;
}
</style>
</head>
<body>
<div class="title">Specifications</div>
<% specifications.each do |spec| %>
<div class="specification">
<%= spec.name %> should:
<ul class="requirements">
<% spec.requirements.each do |req| %>
<li><%= req %></li>
<% end %>
</ul>
</div>
<% end %>
</body>
</html>
EOS
output_dir = File.expand_path(@html_dir)
mkdir_p output_dir
output_filename = output_dir + "/behaviors.html"
File.open(output_filename,"w") do |f|
f.write ERB.new(txt).result(binding)
end
puts "(Wrote #{output_filename})"
end
end
private
def test_files
test_list = FileList[@pattern]
if ENV['for']
test_list = test_list.grep(/#{ENV['for']}/i)
end
test_list
end
def specifications
test_files.map do |file|
spec = OpenStruct.new
m = %r".*/([^/].*)_test.rb".match(file)
class_name = titleize(m[1]) if m[1]
spec.name = class_name
spec.requirements = []
File::readlines(file).each do |line|
if line =~ /^\s*should\s+\(?\s*["'](.*)["']/
spec.requirements << $1
end
end
spec
end
end
############################################################
# STOLEN FROM inflector.rb
############################################################
#--
# Copyright (c) 2005 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
def titleize(word)
humanize(underscore(word)).gsub(/\b([a-z])/) { $1.capitalize }
end
def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
end
def humanize(lower_case_and_underscored_word)
lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
end
end
end
+73
View File
@@ -0,0 +1,73 @@
require 'test/unit'
require 'fileutils'
class BehaviorsTasksTest < Test::Unit::TestCase
include FileUtils
def setup
@here = File.expand_path(File.dirname(__FILE__))
@base_cmd = RUBY_PLATFORM[/mswin/] ? 'rake.cmd ' : 'rake '
end
#
# HELPERS
#
def run_behaviors_task
run_cmd "behaviors"
end
def run_behaviors_html_task
run_cmd "behaviors_html"
end
def run_cmd(cmd)
cd "#{@here}/tasks_test" do
@report = %x[ #{@base_cmd} #{cmd} ]
end
end
def see_html_task_output_message
@html_output_filename = "#{@here}/tasks_test/behaviors_doc/behaviors.html"
assert_match(/Wrote #{@html_output_filename}/, @report)
end
def see_that_html_report_file_exits
assert File.exists?(@html_output_filename), "html output file should exist"
end
def html_report_file_should_contain(user_behaviors)
file_contents = File.read(@html_output_filename)
user_behaviors.each do |line|
assert_match(/#{line}/, file_contents)
end
rm_rf File.dirname(@html_output_filename)
end
#
# TESTS
#
def test_that_behaviors_tasks_should_list_behavioral_definitions_for_the_classes_under_test
run_behaviors_task
user_behaviors = [
"User should:",
" - be able set user name and age during construction",
" - be able to get user name and age",
" - be able to ask if a user is an adult"
]
assert_match(/#{user_behaviors.join("\n")}/, @report)
end
def test_that_behaviors_tasks_should_list_behavioral_definitions_for_the_classes_under_test_in_html_output
run_behaviors_html_task
see_html_task_output_message
see_that_html_report_file_exits
user_behaviors = [
"User should:",
"be able set user name and age during construction",
"be able to get user name and age",
"be able to ask if a user is an adult"
]
html_report_file_should_contain user_behaviors
end
end
+50
View File
@@ -0,0 +1,50 @@
require 'test/unit'
require File.expand_path(File.dirname(__FILE__)) + '/../lib/behaviors'
require 'stringio'
loading_developer_test_class_stdout = StringIO.new
saved_stdout = $stdout.dup
$stdout = loading_developer_test_class_stdout
class DeveloperTest
extend Behaviors
attr_accessor :flunk_msg, :tested_code
should "test their code" do
@tested_code = true
end
should "go to meetings"
end
$stdout = saved_stdout
loading_developer_test_class_stdout.rewind
$loading_developer_test_class_output = loading_developer_test_class_stdout.read
class BehaviorsTest < Test::Unit::TestCase
def setup
@target = DeveloperTest.new
assert_nil @target.tested_code, "block called too early"
end
#
# TESTS
#
def test_should_called_with_a_block_defines_a_test
assert @target.methods.include?("test_should_test their code"), "Missing test method"
@target.send("test_should_test their code")
assert @target.tested_code, "block not called"
end
def test_should_called_without_a_block_does_not_create_a_test_method
assert !@target.methods.include?("test_should_go to meetings"), "Should not have method"
end
def test_should_called_without_a_block_will_give_unimplemented_output_when_class_loads
unimplemented_output = "UNIMPLEMENTED CASE: Developer should go to meetings"
assert_match(/#{unimplemented_output}/, $loading_developer_test_class_output)
end
end
+19
View File
@@ -0,0 +1,19 @@
require 'rake'
require 'rake/testtask'
here = File.expand_path(File.dirname(__FILE__))
require "#{here}/../../lib/behaviors/reporttask"
desc 'Default: run unit tests.'
task :default => :test
Rake::TestTask.new(:test) do |t|
t.libs << "#{here}/../../lib"
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
Behaviors::ReportTask.new(:behaviors) do |t|
t.pattern = 'test/**/*_test.rb'
t.html_dir = 'behaviors_doc'
end
+2
View File
@@ -0,0 +1,2 @@
class User
end
+17
View File
@@ -0,0 +1,17 @@
require 'test/unit'
require 'behaviors'
require 'user'
class UserTest < Test::Unit::TestCase
extend Behaviors
def setup
end
should "be able set user name and age during construction"
should "be able to get user name and age"
should "be able to ask if a user is an adult"
def test_DELETEME
end
end
+78
View File
@@ -0,0 +1,78 @@
Hardmock 1.3.7
* BUG FIX: expects! could not setup expectations for more than one concrete method on an object, since the method aliasing and rewriting was only taking place when the background mock instance was first created. This logic has been updated and now you can do all the things you'd expect.
Hardmock 1.3.6
* BUG FIX: In Rails apps (and others) Hardmock and Fixtures battled viciously over "setup" and "teardown" and "method_added" (and any other clever test enhancement tool, namely Mocha) causing unpredictable results, notably failure to auto-verify mocks after teardown (leading to false positive tests).
* The newly-added TestUnitBeforeAfter provides TestCase.before_setup and TestCase.after_teardown -- formal test wrapping hooks -- lets Hardmock provide its preparation and auto-verify behavior without contending for setup/teardown supremacy.
Hardmock 1.3.5
* Aliased should_receive => expects and and_return => returns for easier transition from rspec mock and flexmock users.
Hardmock 1.3.4
* Prevents accidental stubbing and mocking on NilClasses
Hardmock 1.3.3
* stubs! and expects! no longer require that their target methods exist in reality (this used to prevent you from stubbing methods that "exist" by virtue of "method_missing"
* Tweaked inner metaclass code to avoid collisions with rspec's "metaid" stuff
* Moved this project's Rake tasks into rake_tasks... otherwise Rails will load them, if Hardmock is installed as a Rails plugin
* Alias added: 'verify_hardmocks' is now an alias for 'verify_mocks' (some internal projects were using this modified method name as a means of cooexisting with mocha)
Hardmock 1.3.2
November 2007
* adds 'with' as an alternate syntax for specifying argument expectations.
Hardmock 1.3.1
October 2007
* Can use stubs! on a mock object
* expects! now generates mocked methods that can safely transfer runtime blocks to the mock instance itself
* No longer need to call "prepare_hardmock_control" when using stubs in the absence of mocks
* Stubs of concrete class or instance methods are restored to original state in teardown
Hardmock 1.3.0
October 2007
* Adds stubs! and expects! method to all objects and classes to support concrete stubbing/mocking.
Hardmock 1.2.3
Sat Apr 28 01:16:15 EDT 2007
* Re-release of 1.2.2 (which was canceled)... tasks moved to lib/tasks
Hardmock 1.2.2
Sat Apr 28 00:41:30 EDT 2007
* assert_error has been broken out into its own lib file
* Gem package can now run all tests successfully
* Internal code refactoring; a number of classes that were defined in hardmock.rb are now in their own files
Hardmock 1.2.1
Sat Apr 28 00:41:30 EDT 2007
* (botched release, see 1.2.2)
Hardmock 1.2.0
* You can now use "expect" in place of "expects" if you must.
* "inspect" has been added to the list of methods NOT erased by MethodCleanout.
Hardmock 1.1.0
* "expects" replaces "expect" ("expect" now raises Hardmock::DeprecationError)
* "verify_mocks" is now implicit in teardown, you needn't call it anymore
* Mocking methods that Mock would otherwise inherit from Object (eg, to_s) is now possible
* require 'hardmock' is all that's required to use the library now; no need to include in TestCase
(previously called CMock, translated to Hardmock on 2006-12-10)
+7
View File
@@ -0,0 +1,7 @@
Copyright (c) 2006,2007 David Crosby at Atomic Object, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+70
View File
@@ -0,0 +1,70 @@
== Hardmock
Strict, ordered mock objects using very lightweight syntax in your tests.
== How
The basic procedure for using Hardmock in your tests is:
* require 'hardmock' (this happens automatically when being used as a Rails plugin)
* Create some mocks
* Setup some expectations
* Execute the target code
* Verification of calls is automatic in =teardown=
The expectations you set when using mocks are <b>strict</b> and <b>ordered</b>.
Expectations you declare by creating and using mocks are all considered together.
* Hardmock::Mock#expects will show you more examples
* Hardmock::SimpleExpectation will teach you more about expectation methods
== Example
create_mocks :garage, :car
# Set some expectations
@garage.expects.open_door
@car.expects.start(:choke)
@car.expects.drive(:reverse, 5.mph)
# Execute the code (this code is usually, obviously, in your class under test)
@garage.open_door
@car.start :choke
@car.drive :reverse, 5.mph
verify_mocks # OPTIONAL, teardown will do this for you
Expects <tt>@garage.open_door</tt>, <tt>@car.start(:choke)</tt> and <tt>@car.drive(:reverse, 5.mph)</tt> to be called in that order, with those specific arguments.
* Violations of expectations, such as mis-ordered calls, calls on wrong objects, or incorrect methods result in Hardmock::ExpectationError
* <tt>verify_mocks</tt> will raise VerifyError if not all expectations have been met.
== Download and Install
* Homepage: http://hardmock.rubyforge.org
* GEM or TGZ or ZIP: http://rubyforge.org/frs/?group_id=2742
* Rails plugin: script/plugin install
* SVN access: svn co svn://rubyforge.org/var/svn/hardmock/trunk
* Developer SVN access: svn co svn://developername@rubyforge.org/var/svn/hardmock/trunk
== Setup for Test::Unit
require 'hardmock'
require 'assert_error' # OPTIONAL: this adds the TestUnit extension 'assert_error'
NOTE: If installed as a Rails plugin, init.rb does this for you... nothing else is needed.
== Setup for RSpec
Get this into your spec helper or environment or Rakefile or wherever you prefer:
Spec::Runner.configure do |configuration|
configuration.include Hardmock
configuration.after(:each) {verify_mocks}
end
This puts the implicit conveniences into your spec context, like "create_mocks" etc, and also provides for automatic
"verify_mocks" after each Example is run.
== Author
* David Crosby crosby at http://atomicobject.com
* (c) 2006,2007 Atomic Object LLC
+8
View File
@@ -0,0 +1,8 @@
require 'rake'
require 'rubygems'
HARDMOCK_VERSION = "1.3.7"
Dir["rake_tasks/*.rake"].each { |f| load f }
task :default => [ 'test:all' ]
+12
View File
@@ -0,0 +1,12 @@
# The path to the root directory of your application.
APP_ROOT = File.join(File.dirname(__FILE__), '..')
ADDITIONAL_LOAD_PATHS = []
ADDITIONAL_LOAD_PATHS.concat %w(
lib
).map { |dir| "#{APP_ROOT}/#{dir}" }.select { |dir| File.directory?(dir) }
# Prepend to $LOAD_PATH
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
# Require any additional libraries needed
+23
View File
@@ -0,0 +1,23 @@
require 'test/unit/assertions'
module Test::Unit #:nodoc:#
module Assertions #:nodoc:#
# A better 'assert_raise'. +patterns+ can be one or more Regexps, or a literal String that
# must match the entire error message.
def assert_error(err_type,*patterns,&block)
assert_not_nil block, "assert_error requires a block"
assert((err_type and err_type.kind_of?(Class)), "First argument to assert_error has to be an error type")
err = assert_raise(err_type) do
block.call
end
patterns.each do |pattern|
case pattern
when Regexp
assert_match(pattern, err.message)
else
assert_equal pattern, err.message
end
end
end
end
end
+14
View File
@@ -0,0 +1,14 @@
require 'test/unit/testcase'
class Test::Unit::TestCase
include Hardmock
end
require 'test_unit_before_after'
Test::Unit::TestCase.before_setup do |test|
test.prepare_hardmock_control
end
Test::Unit::TestCase.after_teardown do |test|
test.verify_mocks
end
+86
View File
@@ -0,0 +1,86 @@
require 'hardmock/method_cleanout'
require 'hardmock/mock'
require 'hardmock/mock_control'
require 'hardmock/utils'
require 'hardmock/errors'
require 'hardmock/trapper'
require 'hardmock/expector'
require 'hardmock/expectation'
require 'hardmock/expectation_builder'
require 'hardmock/stubbing'
module Hardmock
# Create one or more new Mock instances in your test suite.
# Once created, the Mocks are accessible as instance variables in your test.
# Newly built Mocks are added to the full set of Mocks for this test, which will
# be verified when you call verify_mocks.
#
# create_mocks :donkey, :cat # Your test now has @donkey and @cat
# create_mock :dog # Test now has @donkey, @cat and @dog
#
# The first call returned a hash { :donkey => @donkey, :cat => @cat }
# and the second call returned { :dog => @dog }
#
# For more info on how to use your mocks, see Mock and Expectation
#
def create_mocks(*mock_names)
prepare_hardmock_control unless @main_mock_control
mocks = {}
mock_names.each do |mock_name|
raise ArgumentError, "'nil' is not a valid name for a mock" if mock_name.nil?
mock_name = mock_name.to_s
mock_object = Mock.new(mock_name, @main_mock_control)
mocks[mock_name.to_sym] = mock_object
self.instance_variable_set "@#{mock_name}", mock_object
end
@all_mocks ||= {}
@all_mocks.merge! mocks
return mocks.clone
end
def prepare_hardmock_control
if @main_mock_control.nil?
@main_mock_control = MockControl.new
$main_mock_control = @main_mock_control
else
raise "@main_mock_control is already setup for this test!"
end
end
alias :create_mock :create_mocks
# Ensures that all expectations have been met. If not, VerifyException is
# raised.
#
# <b>You normally won't need to call this yourself.</b> Within Test::Unit::TestCase, this will be done automatically at teardown time.
#
# * +force+ -- if +false+, and a VerifyError or ExpectationError has already occurred, this method will not raise. This is to help you suppress repeated errors when if you're calling #verify_mocks in the teardown method of your test suite. BE WARNED - only use this if you're sure you aren't obscuring useful information. Eg, if your code handles exceptions internally, and an ExpectationError gets gobbled up by your +rescue+ block, the cause of failure for your test may be hidden from you. For this reason, #verify_mocks defaults to force=true as of Hardmock 1.0.1
def verify_mocks(force=true)
return unless @main_mock_control
return if @main_mock_control.disappointed? and !force
@main_mock_control.verify
ensure
@main_mock_control.clear_expectations if @main_mock_control
$main_mock_control = nil
reset_stubs
end
alias :verify_hardmocks :verify_mocks
# Purge the main MockControl of all expectations, restore all concrete stubbed/mocked methods
def clear_expectations
@main_mock_control.clear_expectations if @main_mock_control
reset_stubs
$main_mock_control = nil
end
def reset_stubs
Hardmock.restore_all_replaced_methods
end
end
require 'extend_test_unit'
+22
View File
@@ -0,0 +1,22 @@
module Hardmock
# Raised when:
# * Unexpected method is called on a mock object
# * Bad arguments passed to an expected call
class ExpectationError < StandardError #:nodoc:#
end
# Raised for methods that should no longer be called. Hopefully, the exception message contains helpful alternatives.
class DeprecationError < StandardError #:nodoc:#
end
# Raised when stubbing fails
class StubbingError < StandardError #:nodoc:#
end
# Raised when it is discovered that an expected method call was never made.
class VerifyError < StandardError #:nodoc:#
def initialize(msg,unmet_expectations)
super("#{msg}:" + unmet_expectations.map { |ex| "\n * #{ex.to_s}" }.join)
end
end
end
+229
View File
@@ -0,0 +1,229 @@
require 'hardmock/utils'
module Hardmock
class Expectation
include Utils
attr_reader :block_value
def initialize(options) #:nodoc:
@options = options
end
def apply_method_call(mock,mname,args,block) #:nodoc:
unless @options[:mock].equal?(mock)
raise anger("Wrong object", mock,mname,args)
end
unless @options[:method] == mname
raise anger("Wrong method",mock,mname,args)
end
# Tester-defined block to invoke at method-call-time:
expectation_block = @options[:block]
expected_args = @options[:arguments]
# if we have a block, we can skip the argument check if none were specified
unless (expected_args.nil? || expected_args.empty?) && expectation_block && !@options[:suppress_arguments_to_block]
unless expected_args == args
raise anger("Wrong arguments",mock,mname,args)
end
end
relayed_args = args.dup
if block
if expectation_block.nil?
# Can't handle a runtime block without an expectation block
raise ExpectationError.new("Unexpected block provided to #{to_s}")
else
# Runtime blocks are passed as final argument to the expectation block
unless @options[:suppress_arguments_to_block]
relayed_args << block
else
# Arguments suppressed; send only the block
relayed_args = [block]
end
end
end
# Run the expectation block:
@block_value = expectation_block.call(*relayed_args) if expectation_block
raise @options[:raises] unless @options[:raises].nil?
return_value = @options[:returns]
if return_value.nil?
return @block_value
else
return return_value
end
end
# Set the return value for an expected method call.
# Eg,
# @cash_machine.expects.withdraw(20,:dollars).returns(20.00)
def returns(val)
@options[:returns] = val
self
end
alias_method :and_return, :returns
# Set the arguments for an expected method call.
# Eg,
# @cash_machine.expects.deposit.with(20, "dollars").returns(:balance => "20")
def with(*args)
@options[:arguments] = args
self
end
# Rig an expected method to raise an exception when the mock is invoked.
#
# Eg,
# @cash_machine.expects.withdraw(20,:dollars).raises "Insufficient funds"
#
# The argument can be:
# * an Exception -- will be used directly
# * a String -- will be used as the message for a RuntimeError
# * nothing -- RuntimeError.new("An Error") will be raised
def raises(err=nil)
case err
when Exception
@options[:raises] = err
when String
@options[:raises] = RuntimeError.new(err)
else
@options[:raises] = RuntimeError.new("An Error")
end
self
end
# Convenience method: assumes +block_value+ is set, and is set to a Proc
# (or anything that responds to 'call')
#
# light_event = @traffic_light.trap.subscribe(:light_changes)
#
# # This code will meet the expectation:
# @traffic_light.subscribe :light_changes do |color|
# puts color
# end
#
# The color-handling block is now stored in <tt>light_event.block_value</tt>
#
# The block can be invoked like this:
#
# light_event.trigger :red
#
# See Mock#trap and Mock#expects for information on using expectation objects
# after they are set.
#
def trigger(*block_arguments)
unless block_value
raise ExpectationError.new("No block value is currently set for expectation #{to_s}")
end
unless block_value.respond_to?(:call)
raise ExpectationError.new("Can't apply trigger to #{block_value} for expectation #{to_s}")
end
block_value.call *block_arguments
end
# Used when an expected method accepts a block at runtime.
# When the expected method is invoked, the block passed to
# that method will be invoked as well.
#
# NOTE: ExpectationError will be thrown upon running the expected method
# if the arguments you set up in +yields+ do not properly match up with
# the actual block that ends up getting passed.
#
# == Examples
# <b>Single invocation</b>: The block passed to +lock_down+ gets invoked
# once with no arguments:
#
# @safe_zone.expects.lock_down.yields
#
# # (works on code that looks like:)
# @safe_zone.lock_down do
# # ... this block invoked once
# end
#
# <b>Multi-parameter blocks:</b> The block passed to +each_item+ gets
# invoked twice, with <tt>:item1</tt> the first time, and with
# <tt>:item2</tt> the second time:
#
# @fruit_basket.expects.each_with_index.yields [:apple,1], [:orange,2]
#
# # (works on code that looks like:)
# @fruit_basket.each_with_index do |fruit,index|
# # ... this block invoked with fruit=:apple, index=1,
# # ... and then with fruit=:orange, index=2
# end
#
# <b>Arrays can be passed as arguments too</b>... if the block
# takes a single argument and you want to pass a series of arrays into it,
# that will work as well:
#
# @list_provider.expects.each_list.yields [1,2,3], [4,5,6]
#
# # (works on code that looks like:)
# @list_provider.each_list do |list|
# # ... list is [1,2,3] the first time
# # ... list is [4,5,6] the second time
# end
#
# <b>Return value</b>: You can set the return value for the method that
# accepts the block like so:
#
# @cruncher.expects.do_things.yields(:bean1,:bean2).returns("The Results")
#
# <b>Raising errors</b>: You can set the raised exception for the method that
# accepts the block. NOTE: the error will be raised _after_ the block has
# been invoked.
#
# # :bean1 and :bean2 will be passed to the block, then an error is raised:
# @cruncher.expects.do_things.yields(:bean1,:bean2).raises("Too crunchy")
#
def yields(*items)
@options[:suppress_arguments_to_block] = true
if items.empty?
# Yield once
@options[:block] = lambda do |block|
if block.arity != 0 and block.arity != -1
raise ExpectationError.new("The given block was expected to have no parameter count; instead, got #{block.arity} to <#{to_s}>")
end
block.call
end
else
# Yield one or more specific items
@options[:block] = lambda do |block|
items.each do |item|
if item.kind_of?(Array)
if block.arity == item.size
# Unfold the array into the block's arguments:
block.call *item
elsif block.arity == 1
# Just pass the array in
block.call item
else
# Size mismatch
raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
end
else
if block.arity != 1
# Size mismatch
raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
end
block.call item
end
end
end
end
self
end
def to_s # :nodoc:
format_method_call_string(@options[:mock],@options[:method],@options[:arguments])
end
private
def anger(msg, mock,mname,args)
ExpectationError.new("#{msg}: expected call <#{to_s}> but was <#{format_method_call_string(mock,mname,args)}>")
end
end
end
+9
View File
@@ -0,0 +1,9 @@
require 'hardmock/expectation'
module Hardmock
class ExpectationBuilder #:nodoc:
def build_expectation(options)
Expectation.new(options)
end
end
end
+26
View File
@@ -0,0 +1,26 @@
require 'hardmock/method_cleanout'
require 'hardmock/errors'
module Hardmock
class Expector #:nodoc:
include MethodCleanout
def initialize(mock,mock_control,expectation_builder)
@mock = mock
@mock_control = mock_control
@expectation_builder = expectation_builder
end
def method_missing(mname, *args, &block)
expectation = @expectation_builder.build_expectation(
:mock => @mock,
:method => mname,
:arguments => args,
:block => block)
@mock_control.add_expectation expectation
expectation
end
end
end
+33
View File
@@ -0,0 +1,33 @@
module Hardmock #:nodoc:
module MethodCleanout #:nodoc:
SACRED_METHODS = %w{
__id__
__send__
equal?
object_id
send
nil?
class
kind_of?
respond_to?
inspect
method
to_s
instance_variables
instance_eval
==
hm_metaclass
hm_meta_eval
hm_meta_def
}
def self.included(base) #:nodoc:
base.class_eval do
instance_methods.each do |m|
undef_method m unless SACRED_METHODS.include?(m.to_s)
end
end
end
end
end
+180
View File
@@ -0,0 +1,180 @@
module Hardmock
# Mock is used to set expectations in your test. Most of the time you'll use
# <tt>#expects</tt> to create expectations.
#
# Aside from the scant few control methods (like +expects+, +trap+ and +_verify+)
# all calls made on a Mock instance will be immediately applied to the internal
# expectation mechanism.
#
# * If the method call was expected and all the parameters match properly, execution continues
# * If the expectation was configured with an expectation block, the block is invoked
# * If the expectation was set up to raise an error, the error is raised now
# * If the expectation was set up to return a value, it is returned
# * If the method call was _not_ expected, or the parameter values are wrong, an ExpectationError is raised.
class Mock
include Hardmock::MethodCleanout
# Create a new Mock instance with a name and a MockControl to support it.
# If not given, a MockControl is made implicitly for this Mock alone; this means
# expectations for this mock are not tied to other expectations in your test.
#
# It's not recommended to use a Mock directly; see Hardmock and
# Hardmock#create_mocks for the more wholistic approach.
def initialize(name, mock_control=nil)
@name = name
@control = mock_control || MockControl.new
@expectation_builder = ExpectationBuilder.new
end
def inspect
"<Mock #{@name}>"
end
# Begin declaring an expectation for this Mock.
#
# == Simple Examples
# Expect the +customer+ to be queried for +account+, and return <tt>"The
# Account"</tt>:
# @customer.expects.account.returns "The Account"
#
# Expect the +withdraw+ method to be called, and raise an exception when it
# is (see Expectation#raises for more info):
# @cash_machine.expects.withdraw(20,:dollars).raises("not enough money")
#
# Expect +customer+ to have its +user_name+ set
# @customer.expects.user_name = 'Big Boss'
#
# Expect +customer+ to have its +user_name+ set, and raise a RuntimeException when
# that happens:
# @customer.expects('user_name=', "Big Boss").raises "lost connection"
#
# Expect +evaluate+ to be passed a block, and when that happens, pass a value
# to the block (see Expectation#yields for more info):
# @cruncher.expects.evaluate.yields("some data").returns("some results")
#
#
# == Expectation Blocks
# To do special handling of expected method calls when they occur, you
# may pass a block to your expectation, like:
# @page_scraper.expects.handle_content do |address,request,status|
# assert_not_nil address, "Can't abide nil addresses"
# assert_equal "http-get", request.method, "Can only handle GET"
# assert status > 200 and status < 300, status, "Failed status"
# "Simulated results #{request.content.downcase}"
# end
# In this example, when <tt>page_scraper.handle_content</tt> is called, its
# three arguments are passed to the <i>expectation block</i> and evaluated
# using the above assertions. The last value in the block will be used
# as the return value for +handle_content+
#
# You may specify arguments to the expected method call, just like any normal
# expectation, and those arguments will be pre-validated before being passed
# to the expectation block. This is useful when you know all of the
# expected values but still need to do something programmatic.
#
# If the method being invoked on the mock accepts a block, that block will be
# passed to your expectation block as the last (or only) argument. Eg, the
# convenience method +yields+ can be replaced with the more explicit:
# @cruncher.expects.evaluate do |block|
# block.call "some data"
# "some results"
# end
#
# The result value of the expectation block becomes the return value for the
# expected method call. This can be overidden by using the +returns+ method:
# @cruncher.expects.evaluate do |block|
# block.call "some data"
# "some results"
# end.returns("the actual value")
#
# <b>Additionally</b>, the resulting value of the expectation block is stored
# in the +block_value+ field on the expectation. If you've saved a reference
# to your expectation, you may retrieve the block value once the expectation
# has been met.
#
# evaluation_event = @cruncher.expects.evaluate do |block|
# block.call "some data"
# "some results"
# end.returns("the actual value")
#
# result = @cruncher.evaluate do |input|
# puts input # => 'some data'
# end
# # result is 'the actual value'
#
# evaluation_event.block_value # => 'some results'
#
def expects(*args, &block)
expector = Expector.new(self,@control,@expectation_builder)
# If there are no args, we return the Expector
return expector if args.empty?
# If there ARE args, we set up the expectation right here and return it
expector.send(args.shift.to_sym, *args, &block)
end
alias_method :expect, :expects
alias_method :should_receive, :expects
# Special-case convenience: #trap sets up an expectation for a method
# that will take a block. That block, when sent to the expected method, will
# be trapped and stored in the expectation's +block_value+ field.
# The Expectation#trigger method may then be used to invoke that block.
#
# Like +expects+, the +trap+ mechanism can be followed by +raises+ or +returns+.
#
# _Unlike_ +expects+, you may not use an expectation block with +trap+. If
# the expected method takes arguments in addition to the block, they must
# be specified in the arguments to the +trap+ call itself.
#
# == Example
#
# create_mocks :address_book, :editor_form
#
# # Expect a subscription on the :person_added event for @address_book:
# person_event = @address_book.trap.subscribe(:person_added)
#
# # The runtime code would look like:
# @address_book.subscribe :person_added do |person_name|
# @editor_form.name = person_name
# end
#
# # At this point, the expectation for 'subscribe' is met and the
# # block has been captured. But we're not done:
# @editor_form.expects.name = "David"
#
# # Now invoke the block we trapped earlier:
# person_event.trigger "David"
#
# verify_mocks
def trap(*args)
Trapper.new(self,@control,ExpectationBuilder.new)
end
def method_missing(mname,*args) #:nodoc:
block = nil
block = Proc.new if block_given?
@control.apply_method_call(self,mname,args,block)
end
def _control #:nodoc:
@control
end
def _name #:nodoc:
@name
end
# Verify that all expectations are fulfilled. NOTE: this method triggers
# validation on the _control_ for this mock, so all Mocks that share the
# MockControl with this instance will be included in the verification.
#
# <b>Only use this method if you are managing your own Mocks and their controls.</b>
#
# Normal usage of Hardmock doesn't require you to call this; let
# Hardmock#verify_mocks do it for you.
def _verify
@control.verify
end
end
end
+53
View File
@@ -0,0 +1,53 @@
require 'hardmock/utils'
module Hardmock
class MockControl #:nodoc:
include Utils
attr_accessor :name
def initialize
clear_expectations
end
def happy?
@expectations.empty?
end
def disappointed?
@disappointed
end
def add_expectation(expectation)
# puts "MockControl #{self.object_id.to_s(16)} adding expectation: #{expectation}"
@expectations << expectation
end
def apply_method_call(mock,mname,args,block)
# Are we even expecting any sort of call?
if happy?
@disappointed = true
raise ExpectationError.new("Surprise call to #{format_method_call_string(mock,mname,args)}")
end
begin
@expectations.shift.apply_method_call(mock,mname,args,block)
rescue Exception => ouch
@disappointed = true
raise ouch
end
end
def verify
# puts "MockControl #{self.object_id.to_s(16)} verify: happy? #{happy?}"
@disappointed = !happy?
raise VerifyError.new("Unmet expectations", @expectations) unless happy?
end
def clear_expectations
@expectations = []
@disappointed = false
end
end
end
+210
View File
@@ -0,0 +1,210 @@
# Stubbing support
#
# Stubs methods on classes and instances
#
# Why's "metaid.rb" stuff crunched down:
class Object #:nodoc:#
def hm_metaclass #:nodoc:#
class << self
self
end
end
def hm_meta_eval(&blk) #:nodoc:#
hm_metaclass.instance_eval(&blk)
end
def hm_meta_def(name, &blk) #:nodoc:#
hm_meta_eval { define_method name, &blk }
end
end
module Hardmock
# == Hardmock: Stubbing and Mocking Concrete Methods
#
# Hardmock lets you stub and/or mock methods on concrete classes or objects.
#
# * To "stub" a concrete method is to rig it to return the same thing always, disregarding any arguments.
# * To "mock" a concrete method is to surplant its funcionality by delegating to a mock object who will cover this behavior.
#
# Mocked methods have their expectations considered along with all other mock object expectations.
#
# If you use stubbing or concrete mocking in the absence (or before creation) of other mocks, you need to invoke <tt>prepare_hardmock_control</tt>.
# Once <tt>verify_mocks</tt> or <tt>clear_expectaions</tt> is called, the overriden behavior in the target objects is restored.
#
# == Examples
#
# River.stubs!(:sounds_like).returns("gurgle")
#
# River.expects!(:jump).returns("splash")
#
# rogue.stubs!(:sounds_like).returns("pshshsh")
#
# rogue.expects!(:rawhide_tanning_solvents).returns("giant snapping turtles")
#
module Stubbing
# Exists only for documentation
end
class ReplacedMethod #:nodoc:#
attr_reader :target, :method_name
def initialize(target, method_name)
@target = target
@method_name = method_name
Hardmock.track_replaced_method self
end
end
class StubbedMethod < ReplacedMethod #:nodoc:#
def invoke(args)
raise @raises if @raises
@return_value
end
def returns(stubbed_return)
@return_value = stubbed_return
end
def raises(err)
err = RuntimeError.new(err) unless err.kind_of?(Exception)
@raises = err
end
end
class ::Object
def stubs!(method_name)
method_name = method_name.to_s
already_stubbed = Hardmock.has_replaced_method?(self, method_name)
stubbed_method = Hardmock::StubbedMethod.new(self, method_name)
unless _is_mock? or already_stubbed
if methods.include?(method_name.to_s)
hm_meta_eval do
alias_method "_hardmock_original_#{method_name}".to_sym, method_name.to_sym
end
end
end
hm_meta_def method_name do |*args|
stubbed_method.invoke(args)
end
stubbed_method
end
def expects!(method_name, *args, &block)
if self._is_mock?
raise Hardmock::StubbingError, "Cannot use 'expects!(:#{method_name})' on a Mock object; try 'expects' instead"
end
method_name = method_name.to_s
@_my_mock = Mock.new(_my_name, $main_mock_control) if @_my_mock.nil?
unless Hardmock.has_replaced_method?(self, method_name)
# Track the method as replaced
Hardmock::ReplacedMethod.new(self, method_name)
# Preserver original implementation of the method by aliasing it away
if methods.include?(method_name)
hm_meta_eval do
alias_method "_hardmock_original_#{method_name}".to_sym, method_name.to_sym
end
end
# Re-define the method to utilize our patron mock instance.
# (This global-temp-var thing is hokey but I was having difficulty generating
# code for the meta class.)
begin
$method_text_temp = %{
def #{method_name}(*args,&block)
@_my_mock.__send__(:#{method_name}, *args, &block)
end
}
class << self
eval $method_text_temp
end
ensure
$method_text_temp = nil
end
end
return @_my_mock.expects(method_name, *args, &block)
end
def _is_mock?
self.kind_of?(Mock)
end
def _my_name
self.kind_of?(Class) ? self.name : self.class.name
end
def _clear_mock
@_my_mock = nil
end
end
class ::NilClass
# Use this only if you really mean it
alias_method :intentionally_stubs!, :stubs!
# Use this only if you really mean it
alias_method :intentionally_expects!, :expects!
# Overridden to protect against accidental nil reference self delusion
def stubs!(mname)
raise StubbingError, "Cannot stub #{mname} method on nil. (If you really mean to, try 'intentionally_stubs!')"
end
# Overridden to protect against accidental nil reference self delusion
def expects!(mname, *args)
raise StubbingError, "Cannot mock #{mname} method on nil. (If you really mean to, try 'intentionally_expects!')"
end
end
class << self
def track_replaced_method(replaced_method)
all_replaced_methods << replaced_method
end
def all_replaced_methods
$all_replaced_methods ||= []
end
def has_replaced_method?(obj, method_name)
hits = all_replaced_methods.select do |replaced|
(replaced.target.object_id == obj.object_id) and (replaced.method_name.to_s == method_name.to_s)
end
return !hits.empty?
end
def restore_all_replaced_methods
all_replaced_methods.each do |replaced|
unless replaced.target._is_mock?
backed_up = "_hardmock_original_#{replaced.method_name}"
if replaced.target.methods.include?(backed_up)
replaced.target.hm_meta_eval do
alias_method replaced.method_name.to_sym, backed_up.to_sym
end
end
replaced.target._clear_mock
end
end
all_replaced_methods.clear
end
end
end
+31
View File
@@ -0,0 +1,31 @@
require 'test/unit/assertions'
require 'hardmock/errors'
module Hardmock
class Trapper #:nodoc:
include Hardmock::MethodCleanout
def initialize(mock,mock_control,expectation_builder)
@mock = mock
@mock_control = mock_control
@expectation_builder = expectation_builder
end
def method_missing(mname, *args)
if block_given?
raise ExpectationError.new("Don't pass blocks when using 'trap' (setting exepectations for '#{mname}')")
end
the_block = lambda { |target_block| target_block }
expectation = @expectation_builder.build_expectation(
:mock => @mock,
:method => mname,
:arguments => args,
:suppress_arguments_to_block => true,
:block => the_block)
@mock_control.add_expectation expectation
expectation
end
end
end
+9
View File
@@ -0,0 +1,9 @@
module Hardmock
module Utils #:nodoc:
def format_method_call_string(mock,mname,args)
arg_string = args.map { |a| a.inspect }.join(', ')
call_text = "#{mock._name}.#{mname}(#{arg_string})"
end
end
end
+169
View File
@@ -0,0 +1,169 @@
require 'test/unit'
require 'test/unit/testcase'
require 'test/unit/assertions'
module Test #:nodoc:#
module Unit #:nodoc:#
# == TestCase Modifications
#
# Monkey-patch to provide a formal mechanism for appending actions to be executed after teardown.
# Use after_teardown to define one or more actions to be executed after teardown for ALL tests.
#
# COMING SOON?
# * (maybe?) Hooks for before_teardown, after_setup, on_error
# * (maybe?) Options for positional control, eg, after_teardown :before_other_actions
# * (maybe?) Provide tagging/filtering so action execution can be controlled specifically?
#
# == Usage
#
# Invoke TestCase.after_teardown with optional parameter, which will be invoked with a reference
# to the test instance that has just been torn down.
#
# Example:
#
# Test::Unit::TestCase.after_teardown do |test|
# test.verify_mocks
# end
#
# == Justification
#
# There are a number of tools and libraries that play fast-n-loose with setup and teardown by
# wrapping them, and by overriding method_added as a means of upholding special setup/teardown
# behavior, usually by re-wrapping newly defined user-level setup/teardown methods.
# mocha and active_record/fixtures (and previously, hardmock) will fight for this
# territory with often unpredictable results.
#
# We wouldn't have to battle if Test::Unit provided a formal pre- and post- hook mechanism.
#
class TestCase
class << self
# Define an action to be run after teardown. Subsequent calls result in
# multiple actions. The block will be given a reference to the test
# being executed.
#
# Example:
#
# Test::Unit::TestCase.after_teardown do |test|
# test.verify_mocks
# end
def after_teardown(&block)
post_teardown_actions << block
end
# Used internally. Access the list of post teardown actions for to be
# used by all tests.
def post_teardown_actions
@@post_teardown_actions ||= []
end
# Define an action to be run before setup. Subsequent calls result in
# multiple actions, EACH BEING PREPENDED TO THE PREVIOUS.
# The block will be given a reference to the test being executed.
#
# Example:
#
# Test::Unit::TestCase.before_setup do |test|
# test.prepare_hardmock_control
# end
def before_setup(&block)
pre_setup_actions.unshift block
end
# Used internally. Access the list of post teardown actions for to be
# used by all tests.
def pre_setup_actions
@@pre_setup_actions ||= []
end
end
# OVERRIDE: This is a reimplementation of the default "run", updated to
# execute actions after teardown.
def run(result)
yield(STARTED, name)
@_result = result
begin
execute_pre_setup_actions(self)
setup
__send__(@method_name)
rescue Test::Unit::AssertionFailedError => e
add_failure(e.message, auxiliary_backtrace_filter(e.backtrace))
rescue Exception
raise if should_passthru_exception($!) # See implementation; this is for pre-1.8.6 compat
add_error($!)
ensure
begin
teardown
rescue Test::Unit::AssertionFailedError => e
add_failure(e.message, auxiliary_backtrace_filter(e.backtrace))
rescue Exception
raise if should_passthru_exception($!) # See implementation; this is for pre-1.8.6 compat
add_error($!)
ensure
execute_post_teardown_actions(self)
end
end
result.add_run
yield(FINISHED, name)
end
private
# Run through the after_teardown actions, treating failures and errors
# in the same way that "run" does: they are reported, and the remaining
# actions are executed.
def execute_post_teardown_actions(test_instance)
self.class.post_teardown_actions.each do |action|
begin
action.call test_instance
rescue Test::Unit::AssertionFailedError => e
add_failure(e.message, auxiliary_backtrace_filter(e.backtrace))
rescue Exception
raise if should_passthru_exception($!)
add_error($!)
end
end
end
# Run through the before_setup actions.
# Failures or errors cause execution to stop.
def execute_pre_setup_actions(test_instance)
self.class.pre_setup_actions.each do |action|
# begin
action.call test_instance
# rescue Test::Unit::AssertionFailedError => e
# add_failure(e.message, auxiliary_backtrace_filter(e.backtrace))
# rescue Exception
# raise if should_passthru_exception($!)
# add_error($!)
# end
end
end
# Make sure that this extension doesn't show up in failure backtraces
def auxiliary_backtrace_filter(trace)
trace.reject { |x| x =~ /test_unit_before_after/ }
end
# Is the given error of the type that we allow to fly out (rather than catching it)?
def should_passthru_exception(ex)
return passthrough_exception_types.include?($!.class)
end
# Provide a list of exception types that are to be allowed to explode out.
# Pre-ruby-1.8.6 doesn't use this functionality, so the PASSTHROUGH_EXCEPTIONS
# constant won't be defined. This methods defends against that and returns
# an empty list instead.
def passthrough_exception_types
begin
return PASSTHROUGH_EXCEPTIONS
rescue NameError
# older versions of test/unit do not have PASSTHROUGH_EXCEPTIONS constant
return []
end
end
end
end
end
+19
View File
@@ -0,0 +1,19 @@
require 'rake/rdoctask'
require File.expand_path(File.dirname(__FILE__) + "/rdoc_options.rb")
namespace :doc do
desc "Generate RDoc documentation"
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "Hardmock: Strict expectation-based mock object library "
add_rdoc_options(rdoc.options)
rdoc.rdoc_files.include('lib/**/*.rb', 'README','CHANGES','LICENSE')
}
task :show => [ 'doc:rerdoc' ] do
sh "open doc/index.html"
end
end
+4
View File
@@ -0,0 +1,4 @@
def add_rdoc_options(options)
options << '--line-numbers' << '--inline-source' << '--main' << 'README' << '--title' << 'Hardmock'
end
+22
View File
@@ -0,0 +1,22 @@
require 'rake/testtask'
namespace :test do
desc "Run unit tests"
Rake::TestTask.new("units") { |t|
t.libs << "test"
t.pattern = 'test/unit/*_test.rb'
t.verbose = true
}
desc "Run functional tests"
Rake::TestTask.new("functional") { |t|
t.libs << "test"
t.pattern = 'test/functional/*_test.rb'
t.verbose = true
}
desc "Run all the tests"
task :all => [ 'test:units', 'test:functional' ]
end
+52
View File
@@ -0,0 +1,52 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'assert_error'
class AssertErrorTest < Test::Unit::TestCase
it "specfies an error type and message that should be raised" do
assert_error RuntimeError, "Too funky" do
raise RuntimeError.new("Too funky")
end
end
it "flunks if the error message is wrong" do
err = assert_raise Test::Unit::AssertionFailedError do
assert_error RuntimeError, "not good" do
raise RuntimeError.new("Too funky")
end
end
assert_match(/not good/i, err.message)
assert_match(/too funky/i, err.message)
end
it "flunks if the error type is wrong" do
err = assert_raise Test::Unit::AssertionFailedError do
assert_error StandardError, "Too funky" do
raise RuntimeError.new("Too funky")
end
end
assert_match(/StandardError/i, err.message)
assert_match(/RuntimeError/i, err.message)
end
it "can match error message text using a series of Regexps" do
assert_error StandardError, /too/i, /funky/i do
raise StandardError.new("Too funky")
end
end
it "flunks if the error message doesn't match all the Regexps" do
err = assert_raise Test::Unit::AssertionFailedError do
assert_error StandardError, /way/i, /too/i, /funky/i do
raise StandardError.new("Too funky")
end
end
assert_match(/way/i, err.message)
end
it "can operate without any message specification" do
assert_error StandardError do
raise StandardError.new("ooof")
end
end
end
+178
View File
@@ -0,0 +1,178 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'fileutils'
class AutoVerifyTest < Test::Unit::TestCase
def setup
@expect_unmet_expectations = true
end
def teardown
remove_temp_test_file
end
#
# TESTS
#
it "auto-verifies all mocks in teardown" do
write_and_execute_test
end
it "auto-verifies even if user defines own teardown" do
@teardown_code =<<-EOM
def teardown
# just in the way
end
EOM
write_and_execute_test
end
should "not obscure normal failures when verification fails" do
@test_code =<<-EOM
def test_setup_doomed_expectation
create_mock :automobile
@automobile.expects.start
flunk "natural failure"
end
EOM
@expect_failures = 1
write_and_execute_test
end
should "not skip user-defined teardown when verification fails" do
@teardown_code =<<-EOM
def teardown
puts "User teardown"
end
EOM
write_and_execute_test
assert_output_contains(/User teardown/)
end
it "is quiet when verification is ok" do
@test_code =<<-EOM
def test_ok
create_mock :automobile
@automobile.expects.start
@automobile.start
end
EOM
@teardown_code =<<-EOM
def teardown
puts "User teardown"
end
EOM
@expect_unmet_expectations = false
@expect_failures = 0
@expect_errors = 0
write_and_execute_test
assert_output_contains(/User teardown/)
end
should "auto-verify even if user teardown explodes" do
@teardown_code =<<-EOM
def teardown
raise "self destruct"
end
EOM
@expect_errors = 2
write_and_execute_test
assert_output_contains(/self destruct/)
end
it "plays nice with inherited teardown methods" do
@full_code ||=<<-EOTEST
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock'
class Test::Unit::TestCase
def teardown
puts "Test helper teardown"
end
end
class DummyTest < Test::Unit::TestCase
def test_prepare_to_die
create_mock :automobile
@automobile.expects.start
end
end
EOTEST
write_and_execute_test
assert_output_contains(/Test helper teardown/)
end
#
# HELPERS
#
def temp_test_file
File.expand_path(File.dirname(__FILE__) + "/tear_down_verification_test.rb")
end
def run_test(tbody)
File.open(temp_test_file,"w") { |f| f.print(tbody) }
@test_output = `ruby #{temp_test_file} 2>&1`
end
def formatted_test_output
if @test_output
@test_output.split(/\n/).map { |line| "> #{line}" }.join("\n")
else
"(NO TEST OUTPUT!)"
end
end
def remove_temp_test_file
FileUtils::rm_f temp_test_file
end
def assert_results(h)
if @test_output !~ /#{h[:tests]} tests, [0-9]+ assertions, #{h[:failures]} failures, #{h[:errors]} errors/
flunk "Test results didn't match #{h.inspect}:\n#{formatted_test_output}"
end
end
def assert_output_contains(*patterns)
patterns.each do |pattern|
if @test_output !~ pattern
flunk "Test output didn't match #{pattern.inspect}:\n#{formatted_test_output}"
end
end
end
def assert_output_doesnt_contain(*patterns)
patterns.each do |pattern|
assert @test_output !~ pattern, "Output shouldn't match #{pattern.inspect} but it does."
end
end
def write_and_execute_test
@test_code ||=<<-EOM
def test_setup_doomed_expectation
create_mock :automobile
@automobile.expects.start
end
EOM
@full_code ||=<<-EOTEST
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock'
class DummyTest < Test::Unit::TestCase
#{@teardown_code}
#{@test_code}
end
EOTEST
run_test @full_code
if @expect_unmet_expectations
assert_output_contains(/unmet expectations/i, /automobile/, /start/)
else
assert_output_doesnt_contain(/unmet expectations/i, /automobile/, /start/)
end
@expect_tests ||= 1
@expect_failures ||= 0
@expect_errors ||= 1
assert_results :tests => @expect_tests, :failures => @expect_failures, :errors => @expect_errors
end
end
@@ -0,0 +1,396 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock'
class DirectMockUsageTest < Test::Unit::TestCase
def setup
@bird = Mock.new('bird')
end
def teardown
end
#
# TESTS
#
it "raises VerifyError if expected method not called" do
@bird.expects.flap_flap
err = assert_raise VerifyError do
@bird._verify
end
assert_match(/unmet expectations/i, err.message)
end
should "not raise when expected calls are made in order" do
@bird.expects.flap_flap
@bird.expects.bang
@bird.expects.plop
@bird.flap_flap
@bird.bang
@bird.plop
@bird._verify
end
it "raises ExpectationError when unexpected method are called" do
@bird.expects.flap_flap
err = assert_raise ExpectationError do
@bird.shoot
end
assert_match(/wrong method/i, err.message)
end
it "raises ExpectationError on bad arguments" do
@bird.expects.flap_flap(:swoosh)
err = assert_raise ExpectationError do
@bird.flap_flap(:rip)
end
assert_match(/wrong arguments/i, err.message)
end
it "raises VerifyError when not all expected methods are called" do
@bird.expects.flap_flap
@bird.expects.bang
@bird.expects.plop
@bird.flap_flap
err = assert_raise VerifyError do
@bird._verify
end
assert_match(/unmet expectations/i, err.message)
end
it "raises ExpectationError when calls are made out of order" do
@bird.expects.flap_flap
@bird.expects.bang
@bird.expects.plop
@bird.flap_flap
err = assert_raise ExpectationError do
@bird.plop
end
assert_match(/wrong method/i, err.message)
end
it "returns the configured value" do
@bird.expects.plop.returns(':P')
assert_equal ':P', @bird.plop
@bird._verify
@bird.expects.plop.returns(':x')
assert_equal ':x', @bird.plop
@bird._verify
end
it "returns nil when no return is specified" do
@bird.expects.plop
assert_nil @bird.plop
@bird._verify
end
it "raises the configured exception" do
err = RuntimeError.new('shaq')
@bird.expects.plop.raises(err)
actual_err = assert_raise RuntimeError do
@bird.plop
end
assert_same err, actual_err, 'should be the same error'
@bird._verify
end
it "raises a RuntimeError when told to 'raise' a string" do
@bird.expects.plop.raises('shaq')
err = assert_raise RuntimeError do
@bird.plop
end
assert_match(/shaq/i, err.message)
@bird._verify
end
it "raises a default RuntimeError" do
@bird.expects.plop.raises
err = assert_raise RuntimeError do
@bird.plop
end
assert_match(/error/i, err.message)
@bird._verify
end
it "is quiet when correct arguments given" do
thing = Object.new
@bird.expects.plop(:big,'one',thing)
@bird.plop(:big,'one',thing)
@bird._verify
end
it "raises ExpectationError when wrong number of arguments specified" do
thing = Object.new
@bird.expects.plop(:big,'one',thing)
err = assert_raise ExpectationError do
# more
@bird.plop(:big,'one',thing,:other)
end
assert_match(/wrong arguments/i, err.message)
@bird._verify
@bird.expects.plop(:big,'one',thing)
err = assert_raise ExpectationError do
# less
@bird.plop(:big,'one')
end
assert_match(/wrong arguments/i, err.message)
@bird._verify
@bird.expects.plop
err = assert_raise ExpectationError do
# less
@bird.plop(:big)
end
assert_match(/wrong arguments/i, err.message)
@bird._verify
end
it "raises ExpectationError when arguments don't match" do
thing = Object.new
@bird.expects.plop(:big,'one',thing)
err = assert_raise ExpectationError do
@bird.plop(:big,'two',thing,:other)
end
assert_match(/wrong arguments/i, err.message)
@bird._verify
end
it "can use a block for custom reactions" do
mitt = nil
@bird.expects.plop { mitt = :ball }
assert_nil mitt
@bird.plop
assert_equal :ball, mitt, 'didnt catch the ball'
@bird._verify
@bird.expects.plop { raise 'ball' }
err = assert_raise RuntimeError do
@bird.plop
end
assert_match(/ball/i, err.message)
@bird._verify
end
it "passes mock-call arguments to the expectation block" do
ball = nil
mitt = nil
@bird.expects.plop {|arg1,arg2|
ball = arg1
mitt = arg2
}
assert_nil ball
assert_nil mitt
@bird.plop(:ball,:mitt)
assert_equal :ball, ball
assert_equal :mitt, mitt
@bird._verify
end
it "validates arguments if specified in addition to a block" do
ball = nil
mitt = nil
@bird.expects.plop(:ball,:mitt) {|arg1,arg2|
ball = arg1
mitt = arg2
}
assert_nil ball
assert_nil mitt
@bird.plop(:ball,:mitt)
assert_equal :ball, ball
assert_equal :mitt, mitt
@bird._verify
ball = nil
mitt = nil
@bird.expects.plop(:bad,:stupid) {|arg1,arg2|
ball = arg1
mitt = arg2
}
assert_nil ball
assert_nil mitt
err = assert_raise ExpectationError do
@bird.plop(:ball,:mitt)
end
assert_match(/wrong arguments/i, err.message)
assert_nil ball
assert_nil mitt
@bird._verify
ball = nil
mitt = nil
@bird.expects.plop(:ball,:mitt) {|arg1,arg2|
ball = arg1
mitt = arg2
}
assert_nil ball
assert_nil mitt
err = assert_raise ExpectationError do
@bird.plop(:ball)
end
assert_match(/wrong arguments/i, err.message)
assert_nil ball
assert_nil mitt
@bird._verify
end
it "passes runtime blocks to the expectation block as the final argument" do
runtime_block_called = false
got_arg = nil
# Eg, bird expects someone to subscribe to :tweet using the 'when' method
@bird.expects.when(:tweet) { |arg1, block|
got_arg = arg1
block.call
}
@bird.when(:tweet) do
runtime_block_called = true
end
assert_equal :tweet, got_arg, "Wrong arg"
assert runtime_block_called, "The runtime block should have been invoked by the user block"
@bird.expects.when(:warnk) { |e,blk| }
err = assert_raise ExpectationError do
@bird.when(:honk) { }
end
assert_match(/wrong arguments/i, err.message)
@bird._verify
end
it "passes the runtime block to the expectation block as sole argument if no other args come into play" do
runtime_block_called = false
@bird.expects.subscribe { |block| block.call }
@bird.subscribe do
runtime_block_called = true
end
assert runtime_block_called, "The runtime block should have been invoked by the user block"
end
it "provides nil as final argument if expectation block seems to want a block" do
invoked = false
@bird.expects.kablam(:scatter) { |shot,block|
assert_equal :scatter, shot, "Wrong shot"
assert_nil block, "The expectation block should get a nil block when user neglects to pass one"
invoked = true
}
@bird.kablam :scatter
assert invoked, "Expectation block not invoked"
@bird._verify
end
it "can set explicit return after an expectation block" do
got = nil
@bird.expects.kablam(:scatter) { |shot|
got = shot
}.returns(:death)
val = @bird.kablam :scatter
assert_equal :death, val, "Wrong return value"
assert_equal :scatter, got, "Wrong argument"
@bird._verify
end
it "can raise after an expectation block" do
got = nil
@bird.expects.kablam(:scatter) do |shot|
got = shot
end.raises "hell"
err = assert_raise RuntimeError do
@bird.kablam :scatter
end
assert_match(/hell/i, err.message)
@bird._verify
end
it "stores the semantic value of the expectation block after it executes" do
expectation = @bird.expects.kablam(:slug) { |shot|
"The shot was #{shot}"
}
assert_not_nil expectation, "Expectation nil"
assert_nil expectation.block_value, "Block value should start out nil"
ret_val = @bird.kablam :slug
assert_equal "The shot was slug", expectation.block_value
assert_equal "The shot was slug", ret_val, "Block value should also be used for return"
@bird._verify
end
it "uses the value of the expectation block as the default return value" do
@bird.expects.kablam(:scatter) { |shot|
"The shot was #{shot}"
}
val = @bird.kablam :scatter
assert_equal "The shot was scatter", val, "Wrong return value"
@bird._verify
end
it "returns the Expectation even if 'returns' is used" do
expectation = @bird.expects.kablam(:slug) { |shot|
"The shot was #{shot}"
}.returns :hosed
assert_not_nil expectation, "Expectation nil"
assert_nil expectation.block_value, "Block value should start out nil"
ret_val = @bird.kablam :slug
assert_equal "The shot was slug", expectation.block_value
assert_equal :hosed, ret_val, "Block value should also be used for return"
@bird._verify
end
it "returns the Expectation even if 'raises' is used" do
expectation = @bird.expects.kablam(:slug) { |shot|
"The shot was #{shot}"
}.raises "aiee!"
assert_not_nil expectation, "Expectation nil"
assert_nil expectation.block_value, "Block value should start out nil"
err = assert_raise RuntimeError do
@bird.kablam :slug
end
assert_match(/aiee!/i, err.message)
assert_equal "The shot was slug", expectation.block_value
@bird._verify
end
it "supports assignment-style methods" do
@bird.expects.size = "large"
@bird.size = "large"
@bird._verify
end
it "supports assignments and raising (using explicit-method syntax)" do
@bird.expects('size=','large').raises "boom"
err = assert_raise RuntimeError do
@bird.size = "large"
end
assert_match(/boom/i, err.message)
end
end
+434
View File
@@ -0,0 +1,434 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock'
require 'assert_error'
class HardmockTest < Test::Unit::TestCase
#
# TESTS
#
it "conveniently creates mocks using create_mock and create_mocks" do
h = create_mock :donkey
assert_equal [ :donkey ], h.keys
assert_mock_exists :donkey
assert_same @donkey, h[:donkey]
assert_equal [ :donkey ], @all_mocks.keys, "Wrong keyset for @all_mocks"
h2 = create_mocks :cat, 'dog' # symbol/string indifference at this level
assert_equal [:cat,:dog].to_set, h2.keys.to_set, "Wrong keyset for second hash"
assert_equal [:cat,:dog,:donkey].to_set, @all_mocks.keys.to_set, "@all_mocks wrong"
assert_mock_exists :cat
assert_same @cat, h2[:cat]
assert_mock_exists :dog
assert_same @dog, h2[:dog]
assert_mock_exists :donkey
end
it "provides literal 'expects' syntax" do
assert_nil @order, "Should be no @order yet"
create_mock :order
assert_not_nil @order, "@order should be built"
# Setup an expectation
@order.expects.update_stuff :key1 => 'val1', :key2 => 'val2'
# Use the mock
@order.update_stuff :key1 => 'val1', :key2 => 'val2'
# Verify
verify_mocks
# See that it's ok to do it again
verify_mocks
end
it "supports 'with' for specifying argument expectations" do
create_mocks :car
@car.expects(:fill).with('gas','booze')
@car.fill('gas', 'booze')
verify_mocks
end
it "supports several mocks at once" do
create_mocks :order_builder, :order, :customer
@order_builder.expects.create_new_order.returns @order
@customer.expects.account_number.returns(1234)
@order.expects.account_no = 1234
@order.expects.save!
# Run "the code"
o = @order_builder.create_new_order
o.account_no = @customer.account_number
o.save!
verify_mocks
end
it "enforces inter-mock call ordering" do
create_mocks :order_builder, :order, :customer
@order_builder.expects.create_new_order.returns @order
@customer.expects.account_number.returns(1234)
@order.expects.account_no = 1234
@order.expects.save!
# Run "the code"
o = @order_builder.create_new_order
err = assert_raise ExpectationError do
o.save!
end
assert_match(/wrong object/i, err.message)
assert_match(/order.save!/i, err.message)
assert_match(/customer.account_number/i, err.message)
assert_error VerifyError, /unmet expectations/i do
verify_mocks
end
end
class UserPresenter
def initialize(args)
view = args[:view]
model = args[:model]
model.when :data_changes do
view.user_name = model.user_name
end
view.when :user_edited do
model.user_name = view.user_name
end
end
end
it "makes MVP testing simple" do
mox = create_mocks :model, :view
data_change = @model.expects.when(:data_changes) { |evt,block| block }
user_edit = @view.expects.when(:user_edited) { |evt,block| block }
UserPresenter.new mox
# Expect user name transfer from model to view
@model.expects.user_name.returns 'Da Croz'
@view.expects.user_name = 'Da Croz'
# Trigger data change event in model
data_change.block_value.call
# Expect user name transfer from view to model
@view.expects.user_name.returns '6:8'
@model.expects.user_name = '6:8'
# Trigger edit event in view
user_edit.block_value.call
verify_mocks
end
it "continues to function after verify, if verification error is controlled" do
mox = create_mocks :model, :view
data_change = @model.expects.when(:data_changes) { |evt,block| block }
user_edit = @view.expects.when(:user_edited) { |evt,block| block }
UserPresenter.new mox
# Expect user name transfer from model to view
@model.expects.user_name.returns 'Da Croz'
@view.expects.user_name = 'Da Croz'
assert_error ExpectationError, /model.monkey_wrench/i do
@model.monkey_wrench
end
# This should raise because of unmet expectations
assert_error VerifyError, /unmet expectations/i, /user_name/i do
verify_mocks
end
# See that the non-forced verification remains quiet
assert_nothing_raised VerifyError do
verify_mocks(false)
end
@model.expects.never_gonna_happen
assert_error VerifyError, /unmet expectations/i, /never_gonna_happen/i do
verify_mocks
end
end
class UserPresenterBroken
def initialize(args)
view = args[:view]
model = args[:model]
model.when :data_changes do
view.user_name = model.user_name
end
# no view stuff, will break appropriately
end
end
it "flunks for typical Presenter constructor wiring failure" do
mox = create_mocks :model, :view
data_change = @model.expects.when(:data_changes) { |evt,block| block }
user_edit = @view.expects.when(:user_edited) { |evt,block| block }
UserPresenterBroken.new mox
err = assert_raise VerifyError do
verify_mocks
end
assert_match(/unmet expectations/i, err.message)
assert_match(/view.when\(:user_edited\)/i, err.message)
end
it "provides convenient event-subscription trap syntax for MVP testing" do
mox = create_mocks :model, :view
data_change = @model.trap.when(:data_changes)
user_edit = @view.trap.when(:user_edited)
UserPresenter.new mox
# Expect user name transfer from model to view
@model.expects.user_name.returns 'Da Croz'
@view.expects.user_name = 'Da Croz'
# Trigger data change event in model
data_change.trigger
# Expect user name transfer from view to model
@view.expects.user_name.returns '6:8'
@model.expects.user_name = '6:8'
# Trigger edit event in view
user_edit.trigger
verify_mocks
end
it "raises if you try to pass an expectation block to 'trap'" do
create_mock :model
assert_error Hardmock::ExpectationError, /blocks/i, /trap/i do
@model.trap.when(:some_event) do raise "huh?" end
end
end
class Grinder
def initialize(objects)
@chute = objects[:chute]
@bucket = objects[:bucket]
@blade = objects[:blade]
end
def grind(slot)
@chute.each_bean(slot) do |bean|
@bucket << @blade.chop(bean)
end
end
end
it "lets you write clear iteration-oriented expectations" do
grinder = Grinder.new create_mocks(:blade, :chute, :bucket)
# Style 1: assertions on method args is done explicitly in block
@chute.expects.each_bean { |slot,block|
assert_equal :side_slot, slot, "Wrong slot"
block.call :bean1
block.call :bean2
}
@blade.expects.chop(:bean1).returns(:grounds1)
@bucket.expects('<<', :grounds1)
@blade.expects.chop(:bean2).returns(:grounds2)
@bucket.expects('<<', :grounds2)
# Run "the code"
grinder.grind(:side_slot)
verify_mocks
# Style 2: assertions on method arguments done implicitly in the expectation code
@chute.expects.each_bean(:main_slot) { |slot,block|
block.call :bean3
}
@blade.expects.chop(:bean3).returns(:grounds3)
@bucket.expects('<<', :grounds3)
grinder.grind :main_slot
verify_mocks
end
it "further supports iteration testing using 'yield'" do
grinder = Grinder.new create_mocks(:blade, :chute, :bucket)
@chute.expects.each_bean(:side_slot).yields :bean1, :bean2
@blade.expects.chop(:bean1).returns(:grounds1)
@bucket.expects('<<', :grounds1)
@blade.expects.chop(:bean2).returns(:grounds2)
@bucket.expects('<<', :grounds2)
grinder.grind :side_slot
verify_mocks
end
class HurtLocker
attr_reader :caught
def initialize(opts)
@locker = opts[:locker]
@store = opts[:store]
end
def do_the_thing(area,data)
@locker.with_lock(area) do
@store.eat(data)
end
rescue => oops
@caught = oops
end
end
it "makes mutex-style locking scenarios easy to test" do
hurt = HurtLocker.new create_mocks(:locker, :store)
@locker.expects.with_lock(:main).yields
@store.expects.eat("some info")
hurt.do_the_thing(:main, "some info")
verify_mocks
end
it "makes it easy to simulate error in mutex-style locking scenarios" do
hurt = HurtLocker.new create_mocks(:locker, :store)
err = StandardError.new('fmshooop')
@locker.expects.with_lock(:main).yields
@store.expects.eat("some info").raises(err)
hurt.do_the_thing(:main, "some info")
assert_same err, hurt.caught, "Expected that error to be handled internally"
verify_mocks
end
it "actually returns 'false' instead of nil when mocking boolean return values" do
create_mock :car
@car.expects.ignition_on?.returns(true)
assert_equal true, @car.ignition_on?, "Should be true"
@car.expects.ignition_on?.returns(false)
assert_equal false, @car.ignition_on?, "Should be false"
end
it "can mock most methods inherited from object using literal syntax" do
target_methods = %w|id clone display dup eql? ==|
create_mock :foo
target_methods.each do |m|
eval %{@foo.expects(m, "some stuff")}
eval %{@foo.#{m} "some stuff"}
end
end
it "provides 'expect' as an alias for 'expects'" do
create_mock :foo
@foo.expect.boomboom
@foo.boomboom
verify_mocks
end
it "provides 'should_receive' as an alias for 'expects'" do
create_mock :foo
@foo.should_receive.boomboom
@foo.boomboom
verify_mocks
end
it "provides 'and_return' as an alias for 'returns'" do
create_mock :foo
@foo.expects(:boomboom).and_return :brick
assert_equal :brick, @foo.boomboom
verify_mocks
end
it "does not interfere with a core subset of Object methods" do
create_mock :foo
@foo.method(:inspect)
@foo.inspect
@foo.to_s
@foo.instance_variables
@foo.instance_eval("")
verify_mocks
end
it "can raise errors from within an expectation block" do
create_mock :cat
@cat.expects.meow do |arg|
assert_equal "mix", arg
raise 'HAIRBALL'
end
assert_error RuntimeError, 'HAIRBALL' do
@cat.meow("mix")
end
end
it "can raise errors AFTER an expectation block" do
create_mock :cat
@cat.expects.meow do |arg|
assert_equal "mix", arg
end.raises('HAIRBALL')
assert_error RuntimeError, 'HAIRBALL' do
@cat.meow("mix")
end
end
it "raises an immediate error if a mock is created with a nil name (common mistake: create_mock @cat)" do
# I make this mistake all the time: Typing in an instance var name instead of a symbol in create_mocks.
# When you do that, you're effectively passing nil(s) in as mock names.
assert_error ArgumentError, /'nil' is not a valid name for a mock/ do
create_mocks @apples, @oranges
end
end
it "overrides 'inspect' to make nice output" do
create_mock :hay_bailer
assert_equal "<Mock hay_bailer>", @hay_bailer.inspect, "Wrong output from 'inspect'"
end
it "raises if prepare_hardmock_control is invoked after create_mocks, or more than once" do
create_mock :hi_there
create_mocks :another, :one
assert_error RuntimeError, /already setup/ do
prepare_hardmock_control
end
end
should "support alias verify_hardmocks" do
create_mock :tree
@tree.expects(:grow)
assert_error VerifyError, /unmet/i do
verify_hardmocks
end
end
#
# HELPERS
#
def assert_mock_exists(name)
assert_not_nil @all_mocks, "@all_mocks not here yet"
mo = @all_mocks[name]
assert_not_nil mo, "Mock '#{name}' not in @all_mocks"
assert_kind_of Mock, mo, "Wrong type of object, wanted a Mock"
assert_equal name.to_s, mo._name, "Mock '#{name}' had wrong name"
ivar = self.instance_variable_get("@#{name}")
assert_not_nil ivar, "Mock '#{name}' not set as ivar"
assert_same mo, ivar, "Mock '#{name}' ivar not same as instance in @all_mocks"
assert_same @main_mock_control, mo._control, "Mock '#{name}' doesn't share the main mock control"
end
end
+479
View File
@@ -0,0 +1,479 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock'
require 'assert_error'
class StubbingTest < Test::Unit::TestCase
#
# TESTS
#
it "stubs a class method (and un-stubs after reset_stubs)" do
assert_equal "stones and gravel", Concrete.pour
assert_equal "glug glug", Jug.pour
Concrete.stubs!(:pour).returns("dust and plaster")
3.times do
assert_equal "dust and plaster", Concrete.pour
end
assert_equal "glug glug", Jug.pour, "Jug's 'pour' method broken"
assert_equal "stones and gravel", Concrete._hardmock_original_pour, "Original 'pour' method not aliased"
assert_equal "For roads", Concrete.describe, "'describe' method broken"
reset_stubs
assert_equal "stones and gravel", Concrete.pour, "'pour' method not restored"
assert_equal "For roads", Concrete.describe, "'describe' method broken after verify"
end
it "stubs several class methods" do
Concrete.stubs!(:pour).returns("sludge")
Concrete.stubs!(:describe).returns("awful")
Jug.stubs!(:pour).returns("milk")
assert_equal "sludge", Concrete.pour
assert_equal "awful", Concrete.describe
assert_equal "milk", Jug.pour
reset_stubs
assert_equal "stones and gravel", Concrete.pour
assert_equal "For roads", Concrete.describe
assert_equal "glug glug", Jug.pour
end
it "stubs instance methods" do
slab = Concrete.new
assert_equal "bonk", slab.hit
slab.stubs!(:hit).returns("slap")
assert_equal "slap", slab.hit, "'hit' not stubbed"
reset_stubs
assert_equal "bonk", slab.hit, "'hit' not restored"
end
it "stubs instance methods without breaking class methods or other instances" do
slab = Concrete.new
scrape = Concrete.new
assert_equal "an instance", slab.describe
assert_equal "an instance", scrape.describe
assert_equal "For roads", Concrete.describe
slab.stubs!(:describe).returns("new instance describe")
assert_equal "new instance describe", slab.describe, "'describe' on instance not stubbed"
assert_equal "an instance", scrape.describe, "'describe' on 'scrape' instance broken"
assert_equal "For roads", Concrete.describe, "'describe' class method broken"
reset_stubs
assert_equal "an instance", slab.describe, "'describe' instance method not restored"
assert_equal "an instance", scrape.describe, "'describe' on 'scrape' instance broken after restore"
assert_equal "For roads", Concrete.describe, "'describe' class method broken after restore"
end
should "allow stubbing of nonexistant class methods" do
Concrete.stubs!(:funky).returns('juice')
assert_equal 'juice', Concrete.funky
end
should "allow stubbing of nonexistant instance methods" do
chunk = Concrete.new
chunk.stubs!(:shark).returns('bite')
assert_equal 'bite', chunk.shark
end
should "allow re-stubbing" do
Concrete.stubs!(:pour).returns("one")
assert_equal "one", Concrete.pour
Concrete.stubs!(:pour).raises("hell")
assert_error RuntimeError, /hell/ do
Concrete.pour
end
Concrete.stubs!(:pour).returns("two")
assert_equal "two", Concrete.pour
reset_stubs
assert_equal "stones and gravel", Concrete.pour
end
it "does nothing with a runtime block when simply stubbing" do
slab = Concrete.new
slab.stubs!(:hit) do |nothing|
raise "BOOOMM!"
end
slab.hit
reset_stubs
end
it "can raise errors from a stubbed method" do
Concrete.stubs!(:pour).raises(StandardError.new("no!"))
assert_error StandardError, /no!/ do
Concrete.pour
end
end
it "provides string syntax for convenient raising of RuntimeErrors" do
Concrete.stubs!(:pour).raises("never!")
assert_error RuntimeError, /never!/ do
Concrete.pour
end
end
#
# Per-method mocking on classes or instances
#
it "mocks specific methods on existing classes, and returns the class method to normal after verification" do
assert_equal "stones and gravel", Concrete.pour, "Concrete.pour is already messed up"
Concrete.expects!(:pour).returns("ALIGATORS")
assert_equal "ALIGATORS", Concrete.pour
verify_mocks
assert_equal "stones and gravel", Concrete.pour, "Concrete.pour not restored"
end
it "flunks if expected class method is not invoked" do
Concrete.expects!(:pour).returns("ALIGATORS")
assert_error(Hardmock::VerifyError, /Concrete.pour/, /unmet expectations/i) do
verify_mocks
end
clear_expectations
end
it "supports all normal mock functionality for class methods" do
Concrete.expects!(:pour, "two tons").returns("mice")
Concrete.expects!(:pour, "three tons").returns("cats")
Concrete.expects!(:pour, "four tons").raises("Can't do it")
Concrete.expects!(:pour) do |some, args|
"==#{some}+#{args}=="
end
assert_equal "mice", Concrete.pour("two tons")
assert_equal "cats", Concrete.pour("three tons")
assert_error(RuntimeError, /Can't do it/) do
Concrete.pour("four tons")
end
assert_equal "==first+second==", Concrete.pour("first","second")
end
it "enforces inter-mock ordering when mocking class methods" do
create_mocks :truck, :foreman
@truck.expects.backup
Concrete.expects!(:pour, "something")
@foreman.expects.shout
@truck.backup
assert_error Hardmock::ExpectationError, /wrong/i, /expected call/i, /Concrete.pour/ do
@foreman.shout
end
assert_error Hardmock::VerifyError, /unmet expectations/i, /foreman.shout/ do
verify_mocks
end
clear_expectations
end
should "allow mocking non-existant class methods" do
Concrete.expects!(:something).returns("else")
assert_equal "else", Concrete.something
end
it "mocks specific methods on existing instances, then restore them after verify" do
slab = Concrete.new
assert_equal "bonk", slab.hit
slab.expects!(:hit).returns("slap")
assert_equal "slap", slab.hit, "'hit' not stubbed"
verify_mocks
assert_equal "bonk", slab.hit, "'hit' not restored"
end
it "flunks if expected instance method is not invoked" do
slab = Concrete.new
slab.expects!(:hit)
assert_error Hardmock::VerifyError, /unmet expectations/i, /Concrete.hit/ do
verify_mocks
end
clear_expectations
end
it "supports all normal mock functionality for instance methods" do
slab = Concrete.new
slab.expects!(:hit, "soft").returns("hey")
slab.expects!(:hit, "hard").returns("OOF")
slab.expects!(:hit).raises("stoppit")
slab.expects!(:hit) do |some, args|
"==#{some}+#{args}=="
end
assert_equal "hey", slab.hit("soft")
assert_equal "OOF", slab.hit("hard")
assert_error(RuntimeError, /stoppit/) do
slab.hit
end
assert_equal "==first+second==", slab.hit("first","second")
end
it "enforces inter-mock ordering when mocking instance methods" do
create_mocks :truck, :foreman
slab1 = Concrete.new
slab2 = Concrete.new
@truck.expects.backup
slab1.expects!(:hit)
@foreman.expects.shout
slab2.expects!(:hit)
@foreman.expects.whatever
@truck.backup
slab1.hit
@foreman.shout
assert_error Hardmock::ExpectationError, /wrong/i, /expected call/i, /Concrete.hit/ do
@foreman.whatever
end
assert_error Hardmock::VerifyError, /unmet expectations/i, /foreman.whatever/ do
verify_mocks
end
clear_expectations
end
should "allow mocking non-existant instance methods" do
slab = Concrete.new
slab.expects!(:wholly).returns('happy')
assert_equal 'happy', slab.wholly
end
should "support concrete expectations that deal with runtime blocks" do
Concrete.expects!(:pour, "a lot") do |how_much, block|
assert_equal "a lot", how_much, "Wrong how_much arg"
assert_not_nil block, "nil runtime block"
assert_equal "the block value", block.call, "Wrong runtime block value"
end
Concrete.pour("a lot") do
"the block value"
end
end
it "can stub methods on mock objects" do
create_mock :horse
@horse.stubs!(:speak).returns("silence")
@horse.stubs!(:hello).returns("nothing")
@horse.expects(:canter).returns("clip clop")
assert_equal "silence", @horse.speak
assert_equal "clip clop", @horse.canter
assert_equal "silence", @horse.speak
assert_equal "silence", @horse.speak
assert_equal "nothing", @horse.hello
assert_equal "nothing", @horse.hello
verify_mocks
reset_stubs
end
it "can stub the new method and return values" do
Concrete.stubs!(:new).returns("this value")
assert_equal "this value", Concrete.new, "did not properly stub new class method"
reset_stubs
end
it "can mock the new method and return values" do
Concrete.expects!(:new).with("foo").returns("hello")
Concrete.expects!(:new).with("bar").returns("world")
assert_equal "hello", Concrete.new("foo"), "did not properly mock out new class method"
assert_equal "world", Concrete.new("bar"), "did not properly mock out new class method"
verify_mocks
reset_stubs
end
it "can mock several different class methods at once" do
sim_code = lambda do |input|
record = Multitool.find_record(input)
report = Multitool.generate_report(record)
Multitool.format_output(report)
end
@identifier = "the id"
@record = "the record"
@report = "the report"
@output = "the output"
Multitool.expects!(:find_record).with(@identifier).returns(@record)
Multitool.expects!(:generate_report).with(@record).returns(@report)
Multitool.expects!(:format_output).with(@report).returns(@output)
result = sim_code.call(@identifier)
assert_equal @output, result, "Wrong output"
end
it "can handle a mix of different and repeat class method mock calls" do
prep = lambda {
Multitool.expects!(:find_record).with("A").returns("1")
Multitool.expects!(:generate_report).with("1")
Multitool.expects!(:find_record).with("B").returns("2")
Multitool.expects!(:generate_report).with("2")
}
prep[]
Multitool.generate_report(Multitool.find_record("A"))
Multitool.generate_report(Multitool.find_record("B"))
prep[]
Multitool.generate_report(Multitool.find_record("A"))
assert_error Hardmock::ExpectationError, /Wrong arguments/, /find_record\("B"\)/, /find_record\("C"\)/ do
Multitool.generate_report(Multitool.find_record("C"))
end
clear_expectations
end
it "can mock several concrete instance methods at once" do
inst = OtherMultitool.new
sim_code = lambda do |input|
record = inst.find_record(input)
report = inst.generate_report(record)
inst.format_output(report)
end
@identifier = "the id"
@record = "the record"
@report = "the report"
@output = "the output"
inst.expects!(:find_record).with(@identifier).returns(@record)
inst.expects!(:generate_report).with(@record).returns(@report)
inst.expects!(:format_output).with(@report).returns(@output)
result = sim_code.call(@identifier)
assert_equal @output, result, "Wrong output"
end
it "verifies all concrete expects! from several different expectations" do
Multitool.expects!(:find_record)
Multitool.expects!(:generate_report)
Multitool.expects!(:format_output)
Multitool.find_record
Multitool.generate_report
assert_error Hardmock::VerifyError, /unmet expectations/i, /format_output/i do
verify_mocks
end
end
it "will not allow expects! to be used on a mock object" do
create_mock :cow
assert_error Hardmock::StubbingError, /expects!/, /mock/i, /something/ do
@cow.expects!(:something)
end
end
it "does not allow stubbing on nil objects" do
[ nil, @this_is_nil ].each do |nil_obj|
assert_error Hardmock::StubbingError, /cannot/i, /nil/i, /intentionally/ do
nil_obj.stubs!(:wont_work)
end
end
end
it "does not allow concrete method mocking on nil objects" do
[ nil, @this_is_nil ].each do |nil_obj|
assert_error Hardmock::StubbingError, /cannot/i, /nil/i, /intentionally/ do
nil_obj.expects!(:wont_work)
end
end
end
it "provides an alternate method for stubbing on nil objects" do
@this_is_nil.intentionally_stubs!(:bogus).returns('output')
assert_equal 'output', @this_is_nil.bogus
end
it "provides an alternate method for mocking concreate methods on nil objects" do
@this_is_nil.intentionally_expects!(:bogus).returns('output')
assert_error Hardmock::VerifyError, /unmet expectations/i, /NilClass.bogus/ do
verify_mocks
end
end
#
# HELPERS
#
class Concrete
def initialize; end
def self.pour
"stones and gravel"
end
def self.describe
"For roads"
end
def hit
"bonk"
end
def describe
"an instance"
end
end
class Jug
def self.pour
"glug glug"
end
end
class Multitool
def self.find_record(*a)
raise "The real Multitool.find_record was called with #{a.inspect}"
end
def self.generate_report(*a)
raise "The real Multitool.generate_report was called with #{a.inspect}"
end
def self.format_output(*a)
raise "The real Multitool.format_output was called with #{a.inspect}"
end
end
class OtherMultitool
def find_record(*a)
raise "The real OtherMultitool#find_record was called with #{a.inspect}"
end
def generate_report(*a)
raise "The real OtherMultitool#generate_report was called with #{a.inspect}"
end
def format_output(*a)
raise "The real OtherMultitool#format_output was called with #{a.inspect}"
end
end
end
+43
View File
@@ -0,0 +1,43 @@
here = File.expand_path(File.dirname(__FILE__))
$: << here
require "#{here}/../config/environment"
require 'test/unit'
require 'fileutils'
require 'logger'
require 'find'
require 'yaml'
require 'set'
require 'ostruct'
class Test::Unit::TestCase
include FileUtils
def poll(time_limit)
(time_limit * 10).to_i.times do
return true if yield
sleep 0.1
end
return false
end
def self.it(str, &block)
make_test_case "it", str, &block
end
def self.should(str, &block)
make_test_case "should", str, &block
end
def self.make_test_case(prefix, str, &block)
tname = self.name.sub(/Test$/,'')
if block
define_method "test #{prefix} #{str}" do
instance_eval &block
end
else
puts ">>> UNIMPLEMENTED CASE: #{tname}: #{str}"
end
end
end
+19
View File
@@ -0,0 +1,19 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/expectation_builder'
class ExpectationBuilderTest < Test::Unit::TestCase
include Hardmock
def test_build_expectation
builder = ExpectationBuilder.new
ex = builder.build_expectation( :stuff => 'inside' )
assert_not_nil ex, "Didn't build an expectation"
assert_kind_of Expectation, ex, "Wrong type!"
# Shhhh... fragile, yes, whatever. The functional tests do the
# real testing of this anyway
assert_equal({:stuff => 'inside'}, ex.instance_variable_get('@options'), "Hash not sent to SimpleExpectation constructor")
end
end
+372
View File
@@ -0,0 +1,372 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/expectation'
require 'hardmock/errors'
require 'assert_error'
class ExpectationTest < Test::Unit::TestCase
include Hardmock
def setup
@mock = TheMock.new
end
#
# HELPERS
#
class TheMock
def _name; 'the_mock'; end
end
class OtherMock
def _name; 'other_mock'; end
end
#
# TESTS
#
def test_to_s
ex = Expectation.new( :mock => @mock, :method => 'a_func', :arguments => [1, "two", :three, { :four => 4 }] )
assert_equal %|the_mock.a_func(1, "two", :three, {:four=>4})|, ex.to_s
end
def test_apply_method_call
se = Expectation.new(:mock => @mock, :method => 'some_func',
:arguments => [1,'two',:three] )
# Try it good:
assert_nothing_raised ExpectationError do
se.apply_method_call( @mock, 'some_func', [1,'two',:three], nil )
end
# Bad func name:
err = assert_raise ExpectationError do
se.apply_method_call( @mock, 'wrong_func', [1,'two',:three], nil )
end
assert_match(/wrong method/i, err.message)
assert_match(/wrong_func/i, err.message)
assert_match(/[1, "two", :three]/i, err.message)
assert_match(/some_func/i, err.message)
assert_match(/the_mock/i, err.message)
# Wrong mock
err = assert_raise ExpectationError do
se.apply_method_call( OtherMock.new, 'some_func', [1,'two',:three], nil )
end
assert_match(/[1, "two", :three]/i, err.message)
assert_match(/some_func/i, err.message)
assert_match(/the_mock/i, err.message)
assert_match(/other_mock/i, err.message)
# Wrong args
err = assert_raise ExpectationError do
se.apply_method_call( @mock, 'some_func', [1,'two',:four], nil)
end
assert_match(/[1, "two", :three]/i, err.message)
assert_match(/[1, "two", :four]/i, err.message)
assert_match(/wrong arguments/i, err.message)
assert_match(/some_func/i, err.message)
end
def test_apply_method_call_should_call_proc_when_given
# now with a proc
thinger = nil
the_proc = Proc.new { thinger = :shaq }
se = Expectation.new(:mock => @mock, :method => 'some_func',
:block => the_proc)
# Try it good:
assert_nil thinger
assert_nothing_raised ExpectationError do
se.apply_method_call(@mock, 'some_func', [], nil)
end
assert_equal :shaq, thinger, 'wheres shaq??'
end
def test_apply_method_call_passes_runtime_block_as_last_argument_to_expectation_block
passed_block = nil
exp_block_called = false
exp_block = Proc.new { |blk|
exp_block_called = true
passed_block = blk
}
se = Expectation.new(:mock => @mock, :method => 'some_func', :block => exp_block,
:arguments => [])
set_flag = false
runtime_block = Proc.new { set_flag = true }
assert_nil passed_block, "Passed block should be nil"
assert !set_flag, "set_flag should be off"
# Go
se.apply_method_call( @mock, 'some_func', [], runtime_block)
# Examine the passed block
assert exp_block_called, "Expectation block not called"
assert_not_nil passed_block, "Should have been passed a block"
assert !set_flag, "set_flag should still be off"
passed_block.call
assert set_flag, "set_flag should be on"
end
def test_apply_method_call_fails_when_theres_no_expectation_block_to_handle_the_runtime_block
se = Expectation.new(:mock => @mock, :method => 'some_func', :arguments => [])
runtime_block = Proc.new { set_flag = true }
err = assert_raise ExpectationError do
se.apply_method_call( @mock, 'some_func', [], runtime_block)
end
assert_match(/unexpected block/i, err.message)
assert_match(/the_mock.some_func()/i, err.message)
end
def test_returns
se = Expectation.new(:mock => @mock, :method => 'some_func',
:arguments => [1,'two',:three])
se.returns "A value"
assert_equal "A value", se.apply_method_call(@mock, 'some_func', [1,'two',:three], nil)
end
def test_apply_method_call_captures_block_value
the_proc = lambda { "in the block" }
se = Expectation.new(:mock => @mock, :method => 'do_it', :arguments => [], :block => the_proc)
assert_nil se.block_value, "Block value starts out nil"
se.apply_method_call(@mock, 'do_it', [], nil)
assert_equal "in the block", se.block_value, "Block value not captured"
end
def test_trigger
# convenience method for block_value.call
target = false
inner_proc = lambda { target = true }
the_proc = lambda { inner_proc }
se = Expectation.new(:mock => @mock, :method => 'do_it', :arguments => [], :block => the_proc)
assert_nil se.block_value, "Block value starts out nil"
se.apply_method_call(@mock, 'do_it', [], nil)
assert_not_nil se.block_value, "Block value not set"
assert !target, "Target should still be false"
se.trigger
assert target, "Target not true!"
end
def test_trigger_with_arguments
# convenience method for block_value.call
target = nil
inner_proc = lambda { |one,two| target = [one,two] }
the_proc = lambda { inner_proc }
se = Expectation.new(:mock => @mock, :method => 'do_it', :arguments => [], :block => the_proc)
assert_nil se.block_value, "Block value starts out nil"
se.apply_method_call(@mock, 'do_it', [], nil)
assert_not_nil se.block_value, "Block value not set"
assert_nil target, "target should still be nil"
se.trigger 'cat','dog'
assert_equal ['cat','dog'], target
end
def test_trigger_nil_block_value
se = Expectation.new(:mock => @mock, :method => 'do_it', :arguments => [])
assert_nil se.block_value, "Block value starts out nil"
se.apply_method_call(@mock, 'do_it', [], nil)
assert_nil se.block_value, "Block value should still be nil"
err = assert_raise ExpectationError do
se.trigger
end
assert_match(/do_it/i, err.message)
assert_match(/block value/i, err.message)
end
def test_trigger_non_proc_block_value
the_block = lambda { "woops" }
se = Expectation.new(:mock => @mock, :method => 'do_it', :arguments => [], :block => the_block)
se.apply_method_call(@mock, 'do_it', [], nil)
assert_equal "woops", se.block_value
err = assert_raise ExpectationError do
se.trigger
end
assert_match(/do_it/i, err.message)
assert_match(/trigger/i, err.message)
assert_match(/woops/i, err.message)
end
def test_proc_used_for_return
the_proc = lambda { "in the block" }
se = Expectation.new(:mock => @mock, :method => 'do_it', :arguments => [], :block => the_proc)
assert_equal "in the block", se.apply_method_call(@mock, 'do_it', [], nil)
assert_equal "in the block", se.block_value, "Captured block value affected wrongly"
end
def test_explicit_return_overrides_proc_return
the_proc = lambda { "in the block" }
se = Expectation.new(:mock => @mock, :method => 'do_it', :arguments => [], :block => the_proc)
se.returns "the override"
assert_equal "the override", se.apply_method_call(@mock, 'do_it', [], nil)
assert_equal "in the block", se.block_value, "Captured block value affected wrongly"
end
def test_yields
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot] )
se.yields :bean1, :bean2
things = []
a_block = lambda { |thinger| things << thinger }
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
assert_equal [:bean1,:bean2], things, "Wrong things"
end
def test_yields_block_takes_no_arguments
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot] )
se.yields
things = []
a_block = lambda { things << 'OOF' }
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
assert_equal ['OOF'], things
end
def test_yields_params_to_block_takes_no_arguments
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot] )
se.yields :wont_fit
things = []
a_block = lambda { things << 'WUP' }
err = assert_raise ExpectationError do
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
end
assert_match(/wont_fit/i, err.message)
assert_match(/arity -1/i, err.message)
assert_equal [], things, "Wrong things"
end
def test_yields_with_returns
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot] ,
:returns => 'the results')
exp = se.yields :bean1, :bean2
assert_same se, exp, "'yields' needs to return a reference to the expectation"
things = []
a_block = lambda { |thinger| things << thinger }
returned = se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
assert_equal [:bean1,:bean2], things, "Wrong things"
assert_equal 'the results', returned, "Wrong return value"
end
def test_yields_with_raises
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot],
:raises => RuntimeError.new("kerboom"))
exp = se.yields :bean1, :bean2
assert_same se, exp, "'yields' needs to return a reference to the expectation"
things = []
a_block = lambda { |thinger| things << thinger }
err = assert_raise RuntimeError do
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
end
assert_match(/kerboom/i, err.message)
assert_equal [:bean1,:bean2], things, "Wrong things"
end
def test_yields_and_inner_block_explodes
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot])
exp = se.yields :bean1, :bean2
assert_same se, exp, "'yields' needs to return a reference to the expectation"
things = []
a_block = lambda { |thinger|
things << thinger
raise "nasty"
}
err = assert_raise RuntimeError do
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
end
assert_match(/nasty/i, err.message)
assert_equal [:bean1], things, "Wrong things"
end
def test_yields_with_several_arrays
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot] )
se.yields ['a','b'], ['c','d']
things = []
a_block = lambda { |thinger| things << thinger }
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
assert_equal [ ['a','b'], ['c','d'] ], things, "Wrong things"
end
def test_yields_tuples
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot] )
se.yields ['a','b','c'], ['d','e','f']
things = []
a_block = lambda { |left,mid,right|
things << { :left => left, :mid => mid, :right => right }
}
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
assert_equal [
{:left => 'a', :mid => 'b', :right => 'c' },
{:left => 'd', :mid => 'e', :right => 'f' },
], things, "Wrong things"
end
def test_yields_tuples_size_mismatch
se = Expectation.new(:mock => @mock, :method => 'each_bean', :arguments => [:side_slot] )
se.yields ['a','b','c'], ['d','e','f']
things = []
a_block = lambda { |left,mid|
things << { :left => left, :mid => mid }
}
err = assert_raise ExpectationError do
se.apply_method_call(@mock,'each_bean',[:side_slot],a_block)
end
assert_match(/arity/i, err.message)
assert_match(/the_mock.each_bean/i, err.message)
assert_match(/"a", "b", "c"/i, err.message)
assert_equal [], things, "Wrong things"
end
def test_yields_bad_block_arity
se = Expectation.new(:mock => @mock, :method => 'do_later', :arguments => [] )
se.yields
assert_error Hardmock::ExpectationError, /block/i, /expected/i, /no param/i, /got 2/i do
se.apply_method_call(@mock,'do_later',[],lambda { |doesnt,match| raise "Surprise!" } )
end
end
def test_that_arguments_can_be_added_to_expectation
expectation = Expectation.new(:mock => @mock, :method => "each_bean")
assert_same expectation, expectation.with("jello", "for", "cosby"), "should have returned the same expectation"
err = assert_raise ExpectationError do
expectation.apply_method_call(@mock, 'each_bean', [], nil)
end
assert_match(/wrong arguments/i, err.message)
assert_nothing_raised(ExpectationError) do
expectation.apply_method_call(@mock, 'each_bean', ["jello", "for", "cosby"], nil)
end
end
end
+57
View File
@@ -0,0 +1,57 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/expector'
class ExpectorTest < Test::Unit::TestCase
include Hardmock
class MyControl
attr_reader :added
def add_expectation(expectation)
@added ||= []
@added << expectation
end
end
class ExpBuilder
attr_reader :options
def build_expectation(options)
@options = options
"dummy expectation"
end
end
def try_it_with(method_name)
mock = Object.new
mock_control = MyControl.new
builder = ExpBuilder.new
exp = Expector.new(mock, mock_control, builder)
output = exp.send(method_name,:with, 1, 'sauce')
assert_same mock, builder.options[:mock]
assert_equal method_name, builder.options[:method].to_s
assert_equal [:with,1,'sauce'], builder.options[:arguments]
assert_nil builder.options[:block]
assert_equal [ "dummy expectation" ], mock_control.added,
"Wrong expectation added to control"
assert_equal "dummy expectation", output, "Expectation should have been returned"
end
#
# TESTS
#
def test_method_missing
try_it_with 'wonder_bread'
try_it_with 'whatever'
end
def test_methods_that_wont_trigger_method_missing
mock = Object.new
mock_control = MyControl.new
builder = ExpBuilder.new
exp = Expector.new(mock, mock_control, builder)
assert_equal mock, exp.instance_eval("@mock")
end
end
+36
View File
@@ -0,0 +1,36 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/method_cleanout'
class MethodCleanoutTest < Test::Unit::TestCase
class Victim
OriginalMethods = instance_methods
include Hardmock::MethodCleanout
end
def setup
@victim = Victim.new
end
def test_should_remove_most_methods_from_a_class
expect_removed = Victim::OriginalMethods.reject { |m|
Hardmock::MethodCleanout::SACRED_METHODS.include?(m)
}
expect_removed.each do |m|
assert !@victim.respond_to?(m), "should not have method #{m}"
end
end
def test_should_leave_the_sacred_methods_defined
Hardmock::MethodCleanout::SACRED_METHODS.each do |m|
next if m =~ /^hm_/
assert @victim.respond_to?(m), "Sacred method '#{m}' was removed unexpectedly"
end
end
def test_should_include_certain_important_methods_in_the_sacred_methods_list
%w|__id__ __send__ equal? object_id send nil? class kind_of? respond_to? inspect method to_s instance_variables instance_eval|.each do |m|
assert Hardmock::MethodCleanout::SACRED_METHODS.include?(m), "important method #{m} is not included in SACRED_METHODS"
end
end
end
+175
View File
@@ -0,0 +1,175 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/utils'
require 'hardmock/errors'
require 'hardmock/mock_control'
class MockControlTest < Test::Unit::TestCase
include Hardmock
def setup
@unmock = OpenStruct.new( :_name => 'fakemock' )
@control = MockControl.new
assert @control.happy?, "Control should start out happy"
end
def teardown
end
#
# HELPERS
#
class MyExp
attr_reader :mock, :mname, :args, :block
def apply_method_call(mock, mname, args, block)
@mock = mock
@mname = mname
@args = args
@block = block
end
end
class BoomExp < MyExp
def apply_method_call(mock, mname, args, block)
super
raise "BOOM"
end
end
#
# TESTS
#
def test_add_exepectation_and_apply_method_call
e1 = MyExp.new
@control.add_expectation e1
assert !@control.happy?
@control.apply_method_call @unmock, 'some_func', [ 'the', :args ], nil
assert @control.happy?
assert_same @unmock, e1.mock, "Wrong mock"
assert_equal 'some_func', e1.mname, "Wrong method"
assert_equal [ 'the', :args ], e1.args, "Wrong args"
@control.verify
end
def test_add_exepectation_and_apply_method_call_with_block
e1 = MyExp.new
@control.add_expectation e1
assert !@control.happy?
runtime_block = Proc.new { "hello" }
@control.apply_method_call @unmock, 'some_func', [ 'the', :args ], runtime_block
assert @control.happy?
assert_same @unmock, e1.mock, "Wrong mock"
assert_equal 'some_func', e1.mname, "Wrong method"
assert_equal [ 'the', :args ], e1.args, "Wrong args"
assert_equal "hello", e1.block.call, "Wrong block in expectation"
@control.verify
end
def test_add_expectation_then_verify
e1 = MyExp.new
@control.add_expectation e1
assert !@control.happy?, "Shoudn't be happy"
err = assert_raise VerifyError do
@control.verify
end
assert_match(/unmet expectations/i, err.message)
@control.apply_method_call @unmock, 'some_func', [ 'the', :args ], nil
assert @control.happy?
assert_same @unmock, e1.mock, "Wrong mock"
assert_equal 'some_func', e1.mname, "Wrong method"
assert_equal [ 'the', :args ], e1.args, "Wrong args"
@control.verify
end
def test_expectation_explosion
be1 = BoomExp.new
@control.add_expectation be1
err = assert_raise RuntimeError do
@control.apply_method_call @unmock, 'a func', [:arg], nil
end
assert_match(/BOOM/i, err.message)
assert_same @unmock, be1.mock
assert_equal 'a func', be1.mname
assert_equal [:arg], be1.args
end
def test_disappointment_on_bad_verify
@control.add_expectation MyExp.new
assert !@control.happy?, "Shouldn't be happy"
assert !@control.disappointed?, "too early to be disappointed"
# See verify fails
err = assert_raise VerifyError do
@control.verify
end
assert_match(/unmet expectations/i, err.message)
assert !@control.happy?, "Still have unmet expectation"
assert @control.disappointed?, "We should be disappointed following that failure"
@control.apply_method_call @unmock, 'something', [], nil
assert @control.happy?, "Should be happy"
assert @control.disappointed?, "We should be skeptical"
@control.verify
assert !@control.disappointed?, "Should be non-disappointed"
end
def test_disappointment_from_surprise_calls
assert @control.happy?, "Should be happy"
assert !@control.disappointed?, "too early to be disappointed"
# See verify fails
err = assert_raise ExpectationError do
@control.apply_method_call @unmock, "something", [], nil
end
assert_match(/surprise/i, err.message)
assert @control.happy?, "Happiness is an empty list of expectations"
assert @control.disappointed?, "We should be disappointed following that failure"
@control.verify
assert !@control.disappointed?, "Disappointment should be gone"
end
def test_disappointment_from_bad_calls
be1 = BoomExp.new
assert !@control.disappointed?, "Shouldn't be disappointed"
@control.add_expectation be1
assert !@control.disappointed?, "Shouldn't be disappointed"
err = assert_raise RuntimeError do
@control.apply_method_call @unmock, 'a func', [:arg], nil
end
assert_match(/BOOM/i, err.message)
assert @control.disappointed?, "Should be disappointed"
assert_same @unmock, be1.mock
assert_equal 'a func', be1.mname
assert_equal [:arg], be1.args
assert @control.happy?, "Happiness is an empty list of expectations"
@control.verify
assert !@control.disappointed?, "Disappointment should be gone"
end
end
+279
View File
@@ -0,0 +1,279 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/method_cleanout'
require 'hardmock/mock'
require 'hardmock/mock_control'
require 'hardmock/expectation_builder'
require 'hardmock/expector'
require 'hardmock/trapper'
class MockTest < Test::Unit::TestCase
include Hardmock
def test_build_with_control
mc1 = MockControl.new
mock = Mock.new('hi', mc1)
assert_equal 'hi', mock._name, "Wrong name"
assert_same mc1, mock._control, "Wrong contol"
end
def test_basics
mock = Mock.new('a name')
assert_equal 'a name', mock._name, "Wrong name for mock"
assert_not_nil mock._control, "Nil control in mock"
end
def test_expects
mock = Mock.new('order')
control = mock._control
assert control.happy?, "Mock should start out satisfied"
mock.expects.absorb_something(:location, 'garbage')
assert !control.happy?, "mock control should be unhappy"
# Do the call
mock.absorb_something(:location, 'garbage')
assert control.happy?, "mock control should be happy again"
# Verify
assert_nothing_raised Exception do
mock._verify
end
end
def test_expects_using_arguments_for_method_and_arguments
mock = Mock.new('order')
mock.expects(:absorb_something, :location, 'garbage')
mock.absorb_something(:location, 'garbage')
mock._verify
end
def test_expects_using_arguments_for_method_and_arguments_with_block
mock = Mock.new('order')
mock.expects(:absorb_something, :location, 'garbage') { |a,b,block|
assert_equal :location, a, "Wrong 'a' argument"
assert_equal 'garbage', b, "Wrong 'b' argument"
assert_equal 'innards', block.call, "Wrong block"
}
mock.absorb_something(:location, 'garbage') do "innards" end
mock._verify
end
def test_expects_using_string_method_name
mock = Mock.new('order')
mock.expects('absorb_something', :location, 'garbage')
mock.absorb_something(:location, 'garbage')
mock._verify
end
def test_expects_assignment
mock = Mock.new('order')
mock.expects.account_number = 1234
mock.account_number = 1234
mock._verify
end
def test_expects_assigment_using_arguments_for_method_and_arguments
mock = Mock.new('order')
mock.expects(:account_number=, 1234)
mock.account_number = 1234
mock._verify
end
def test_expects_assigment_using_string_method_name
mock = Mock.new('order')
mock.expects('account_number=', 1234)
mock.account_number = 1234
mock._verify
end
def test_expects_assignment_and_return_is_overruled_by_ruby_syntax
# Prove that we can set up a return but that it doesn't mean much,
# because ruby's parser will 'do the right thing' as regards semantic
# values for assignment. (That is, the rvalue of the assignment)
mock = Mock.new('order')
mock.expects(:account_number=, 1234).returns "gold"
got = mock.account_number = 1234
mock._verify
assert_equal 1234, got, "Expected rvalue"
end
def test_expects_assignment_and_raise
mock = Mock.new('order')
mock.expects(:account_number=, 1234).raises StandardError.new("kaboom")
err = assert_raise StandardError do
mock.account_number = 1234
end
assert_match(/kaboom/i, err.message)
mock._verify
end
def test_expects_multiple
mock = Mock.new('order')
control = mock._control
assert control.happy?
mock.expects.one_thing :hi, { :goose => 'neck' }
mock.expects.another 5,6,7
assert !control.happy?
mock.one_thing :hi, { :goose => 'neck' }
assert !control.happy?
mock.another 5,6,7
assert control.happy?
end
def test_surprise_call
mock = Mock.new('order')
err = assert_raise ExpectationError do
mock.uh_oh
end
assert_match(/surprise/i, err.message)
assert_match(/uh_oh/i, err.message)
err = assert_raise ExpectationError do
mock.whoa :horse
end
assert_match(/surprise/i, err.message)
assert_match(/order\.whoa\(:horse\)/i, err.message)
end
def test_wrong_call
mock = Mock.new('order')
mock.expects.pig 'arse'
err = assert_raise ExpectationError do
mock.whoa :horse
end
assert_match(/wrong method/i, err.message)
assert_match(/order\.whoa\(:horse\)/i, err.message)
assert_match(/order\.pig\("arse"\)/i, err.message)
end
def test_wrong_arguments
mock = Mock.new('order')
mock.expects.go_fast(:a, 1, 'three')
err = assert_raise ExpectationError do
mock.go_fast :a, 1, 'not right'
end
assert_match(/wrong argument/i, err.message)
assert_match(/order\.go_fast\(:a, 1, "three"\)/i, err.message)
assert_match(/order\.go_fast\(:a, 1, "not right"\)/i, err.message)
end
def test_expects_and_return
mock = Mock.new('order')
mock.expects.delivery_date.returns Date.today
assert_equal Date.today, mock.delivery_date
mock._verify
end
def test_expects_and_return_with_arguments
mock = Mock.new('order')
mock.expects.delivery_date(:arf,14).returns(Date.today)
assert_equal Date.today, mock.delivery_date(:arf,14)
mock._verify
end
def test_expects_and_raise
mock = Mock.new('order')
mock.expects.delivery_date.raises StandardError.new("bloof")
err = assert_raise StandardError do
mock.delivery_date
end
assert_match(/bloof/i, err.message)
mock._verify
# Try convenience argument String
mock.expects.pow.raises "hell"
err = assert_raise RuntimeError do
mock.pow
end
assert_match(/hell/i, err.message)
mock._verify
# Try convenience argument nothing
mock.expects.pow.raises
err = assert_raise RuntimeError do
mock.pow
end
assert_match(/an error/i, err.message)
mock._verify
end
def test_expects_a_runtime_block
mock = Mock.new('order')
got_val = nil
mock.expects.when(:something) { |e,block|
got_val = block.call
}
mock.when :something do "hi there" end
assert_equal "hi there", got_val, "Expectation block not invoked"
mock._verify
end
def test_trap_block
mock = Mock.new('order')
exp = mock.trap.observe
# use it
mock.observe { "burp" }
assert_equal "burp", exp.block_value.call
end
def test_trap_arguments_and_block
mock = Mock.new('order')
exp = mock.trap.subscribe(:data_changed)
# use it
mock.subscribe(:data_changed) { "burp" }
assert_equal "burp", exp.block_value.call
mock._verify
end
def test_trap_arguments_and_block_wrong_num_args
mock = Mock.new('order')
exp = mock.trap.subscribe(:data_changed)
assert_raise ExpectationError do
mock.subscribe(:data_changed,1) { "burp" }
end
mock._verify
end
def test_trap_arguments_and_block_wrong_args
mock = Mock.new('order')
exp = mock.trap.subscribe(:data_changed)
assert_raise ExpectationError do
mock.subscribe("no good") { "burp" }
end
mock._verify
end
def test_trap_is_not_leniant_about_arguments
mock = Mock.new('order')
exp = mock.trap.subscribe
assert_raise ExpectationError do
mock.subscribe("no good") { "burp" }
end
mock._verify
end
end
+452
View File
@@ -0,0 +1,452 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
class TestUnitBeforeAfter < Test::Unit::TestCase
#
# after_teardown
#
it "adds TestCase.after_teardown hook for appending post-teardown actions" do
write_and_run_test :use_after_teardown => true
see_in_order "Loaded suite",
"THE SETUP",
"A TEST",
"THE TEARDOWN",
"1st after_teardown",
"2nd after_teardown",
"Finished in"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
end
should "execute all after_teardowns, even if the main teardown flunks" do
write_and_run_test :use_after_teardown => true, :flunk_in_teardown => true
see_in_order "Loaded suite",
"THE SETUP",
"A TEST",
"F",
"1st after_teardown",
"2nd after_teardown",
"Finished in",
"1) Failure:",
"test_something(MyExampleTest) [_test_file_temp.rb:20]:",
"FLUNK IN TEARDOWN"
see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
end
should "execute all after_teardowns, even if the main teardown explodes" do
write_and_run_test :use_after_teardown => true, :raise_in_teardown => true
see_in_order "Loaded suite",
"THE SETUP",
"A TEST",
"E",
"1st after_teardown",
"2nd after_teardown",
"Finished in",
"RuntimeError: ERROR IN TEARDOWN"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 1
end
should "execute all after_teardowns, even if some of them flunk" do
write_and_run_test :use_after_teardown => true, :flunk_in_after_teardown => true
see_in_order "Loaded suite",
"THE SETUP",
"A TEST",
"THE TEARDOWN",
"1st after_teardown",
"F",
"2nd after_teardown",
"Finished in",
"1) Failure:",
"test_something(MyExampleTest) [_test_file_temp.rb:7]:",
"Flunk in first after_teardown",
"2) Failure:",
"test_something(MyExampleTest) [_test_file_temp.rb:10]:",
"Flunk in second after_teardown"
see_results :tests => 1, :assertions => 2, :failures => 2, :errors => 0
end
should "execute all after_teardowns, even if some of them explode" do
write_and_run_test :use_after_teardown => true, :raise_in_after_teardown => true
see_in_order "Loaded suite",
"THE SETUP",
"A TEST",
"THE TEARDOWN",
"1st after_teardown",
"E",
"2nd after_teardown",
"Finished in",
"RuntimeError: Error in first after_teardown",
"RuntimeError: Error in second after_teardown"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 2
end
it "will run after_teardowns in the absence of a regular teardown" do
write_and_run_test :omit_teardown => true, :use_after_teardown => true
see_in_order "Loaded suite",
"THE SETUP",
"A TEST",
"1st after_teardown",
"2nd after_teardown",
"Finished in"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
end
should "not interfere with normal test writing" do
write_and_run_test
see_in_order "Loaded suite",
"THE SETUP",
"A TEST",
"THE TEARDOWN",
"Finished in"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
end
it "provides a cleaned-up backtrace" do
write_and_run_test :with_failure => true
see_in_order "Loaded suite",
"THE SETUP",
"A FAILING TEST",
"F", "THE TEARDOWN",
"Finished in",
"1) Failure:",
"test_something(MyExampleTest) [_test_file_temp.rb:17]:",
"Instrumented failure.",
"<false> is not true."
see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
end
it "provides a cleaned-up backtrace, but not TOO cleaned up" do
write_and_run_test :with_failure => true, :use_helpers => true
see_in_order "Loaded suite",
"THE SETUP",
"A FAILING TEST",
"F", "THE TEARDOWN",
"Finished in",
"1) Failure:",
"test_something(MyExampleTest)\n",
"[_test_file_temp.rb:25:in `tripwire'",
"_test_file_temp.rb:21:in `my_helper'",
"_test_file_temp.rb:17:in `test_something']:",
"Instrumented failure.",
"<false> is not true."
see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
end
should "not interfere with passthrough exception types" do
if is_modern_test_unit?
write_and_run_test :raise_nasty_in_test => true
see_in_no_particular_order "Loaded suite",
"THE TEARDOWN",
"_test_file_temp.rb:16:in `test_something': NASTY ERROR (NoMemoryError)"
see_no_results
end
end
#
# before_setup
#
it "adds TestCase.before_setup hook for prepending pre-setup actions" do
write_and_run_test :use_before_setup => true
see_in_order "Loaded suite",
"3rd before_setup",
"2nd before_setup",
"1st before_setup",
"THE SETUP",
"A TEST",
"THE TEARDOWN",
"Finished in"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
end
should "stop executing the test on the first failure withing a before_setup action" do
write_and_run_test :use_before_setup => true, :flunk_in_before_setup => true
see_in_order "Loaded suite",
"3rd before_setup",
"2nd before_setup",
"FTHE TEARDOWN",
"1) Failure:",
"test_something(MyExampleTest) [_test_file_temp.rb:10]:",
"Flunk in 2nd before_setup."
see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
end
should "stop executing the test on the first error within a before_setup action" do
write_and_run_test :use_before_setup => true, :raise_in_before_setup => true
see_in_order "Loaded suite",
"3rd before_setup",
"2nd before_setup",
"ETHE TEARDOWN",
"Finished in",
"test_something(MyExampleTest):",
"RuntimeError: Error in 2nd before_setup",
"_test_file_temp.rb:10",
"/hardmock/lib/test_unit_before_after.rb:", ":in `call'"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 1
end
it "will run before_setup actions in the absence of a regular setup" do
write_and_run_test :omit_setup => true, :use_before_setup => true
see_in_order "Loaded suite",
"3rd before_setup",
"2nd before_setup",
"1st before_setup",
"A TEST",
"THE TEARDOWN",
"Finished in"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
end
it "allows before_setup and after_teardown to be used at the same time" do
write_and_run_test :use_before_setup => true, :use_after_teardown => true
see_in_order "Loaded suite",
"3rd before_setup",
"2nd before_setup",
"1st before_setup",
"A TEST",
"THE TEARDOWN",
"1st after_teardown",
"2nd after_teardown",
"Finished in"
see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
end
#
# HELPERS
#
def teardown
remove_test
end
def test_filename
"_test_file_temp.rb"
end
def remove_test
rm_f test_filename
end
def write_and_run_test(opts={})
write(test_filename, generate_test_code(opts))
run_test
end
def run_test
@output = `ruby #{test_filename} 2>&1`
end
def write(fname, code)
File.open(fname,"w") do |f|
f.print code
end
end
def show_output
puts "-- BEGIN TEST OUTPUT"
puts @output
puts "-- END TEST OUTPUT"
end
def see_in_order(*phrases)
idx = 0
phrases.each do |txt|
idx = @output.index(txt, idx)
if idx.nil?
if @output.index(txt)
flunk "Phrase '#{txt}' is out-of-order in test output:\n#{@output}"
else
flunk "Phrase '#{txt}' not found in test output:\n#{@output}"
end
end
end
end
def see_in_no_particular_order(*phrases)
phrases.each do |txt|
assert_not_nil @output.index(txt), "Didn't see '#{txt}' in test output:\n#{@output}"
end
end
def see_results(opts)
if @output =~ /(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors/
tests, assertions, failures, errors = [ $1, $2, $3, $4 ]
[:tests, :assertions, :failures, :errors].each do |key|
eval %{assert_equal(opts[:#{key}].to_s, #{key}, "Wrong number of #{key} in report") if opts[:#{key}]}
end
else
flunk "Didn't see the test results report line"
end
end
def see_no_results
if @output =~ /\d+ tests, \d+ assertions, \d+ failures, \d+ errors/
flunk "Should not have had a results line:\n#{@output}"
end
end
def lib_dir
File.expand_path(File.dirname(__FILE__) + "/../../lib")
end
def generate_test_code(opts={})
if opts[:with_failure] or opts[:raise_nasty_in_test]
test_method_code = generate_failing_test("test_something", opts)
else
test_method_code = generate_passing_test("test_something")
end
requires_for_ext = ''
if opts[:use_before_setup] or opts[:use_after_teardown]
requires_for_ext =<<-RFE
$: << "#{lib_dir}"
require 'test_unit_before_after'
RFE
end
before_setups = ''
if opts[:use_before_setup]
add_on_two = ""
if opts[:flunk_in_before_setup]
add_on_two = %{; test.flunk "Flunk in 2nd before_setup"}
elsif opts[:raise_in_before_setup]
add_on_two = %{; raise "Error in 2nd before_setup"}
end
before_setups =<<-BSTS
Test::Unit::TestCase.before_setup do |test|
puts "1st before_setup"
end
Test::Unit::TestCase.before_setup do |test|
puts "2nd before_setup" #{add_on_two}
end
Test::Unit::TestCase.before_setup do |test|
puts "3rd before_setup"
end
BSTS
end
setup_code =<<-SC
def setup
puts "THE SETUP"
end
SC
if opts[:omit_setup]
setup_code = ""
end
after_teardowns = ''
if opts[:use_after_teardown]
add_on_one = ""
add_on_two = ""
if opts[:flunk_in_after_teardown]
add_on_one = %{; test.flunk "Flunk in first after_teardown"}
add_on_two = %{; test.flunk "Flunk in second after_teardown"}
elsif opts[:raise_in_after_teardown]
add_on_one = %{; raise "Error in first after_teardown"}
add_on_two = %{; raise "Error in second after_teardown"}
end
after_teardowns =<<-ATDS
Test::Unit::TestCase.after_teardown do |test|
puts "1st after_teardown" #{add_on_one}
end
Test::Unit::TestCase.after_teardown do |test|
puts "2nd after_teardown" #{add_on_two}
end
ATDS
end
teardown_code =<<-TDC
def teardown
puts "THE TEARDOWN"
end
TDC
if opts[:flunk_in_teardown]
teardown_code =<<-TDC
def teardown
flunk "FLUNK IN TEARDOWN"
end
TDC
elsif opts[:raise_in_teardown]
teardown_code =<<-TDC
def teardown
raise "ERROR IN TEARDOWN"
end
TDC
end
if opts[:omit_teardown]
teardown_code = ""
end
str = <<-TCODE
require 'test/unit'
#{requires_for_ext}
#{before_setups} #{after_teardowns}
class MyExampleTest < Test::Unit::TestCase
#{setup_code}
#{teardown_code}
#{test_method_code}
end
TCODE
end
def generate_passing_test(tname)
str = <<-TMETH
def #{tname}
puts "A TEST"
end
TMETH
end
def generate_failing_test(tname, opts={})
str = "NOT DEFINED?"
if opts[:raise_nasty_in_test]
str = <<-TMETH
def #{tname}
raise NoMemoryError, "NASTY ERROR"
end
TMETH
elsif opts[:use_helpers]
str = <<-TMETH
def #{tname}
puts "A FAILING TEST"
my_helper
end
def my_helper
tripwire
end
def tripwire
assert false, "Instrumented failure"
end
TMETH
else
str = <<-TMETH
def #{tname}
puts "A FAILING TEST"
assert false, "Instrumented failure"
end
TMETH
end
return str
end
def is_modern_test_unit?
begin
Test::Unit::TestCase::PASSTHROUGH_EXCEPTIONS
return true
rescue NameError
return false
end
end
end
+62
View File
@@ -0,0 +1,62 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/method_cleanout'
require 'hardmock/trapper'
class TrapperTest < Test::Unit::TestCase
include Hardmock
def setup
@mock = Object.new
@mock_control = MyControl.new
@builder = ExpBuilder.new
@trapper = Trapper.new(@mock, @mock_control, @builder)
end
#
# HELPERS
#
class MyControl
attr_reader :added
def add_expectation(expectation)
@added ||= []
@added << expectation
end
end
class ExpBuilder
attr_reader :options
def build_expectation(options)
@options = options
"dummy expectation"
end
end
#
# TESTS
#
def test_method_missing
output = @trapper.change(:less)
assert_same @mock, @builder.options[:mock]
assert_equal :change, @builder.options[:method]
assert_equal [:less], @builder.options[:arguments]
assert_not_nil @builder.options[:block]
assert @builder.options[:suppress_arguments_to_block], ":suppress_arguments_to_block should be set"
assert_equal [ "dummy expectation" ], @mock_control.added,
"Wrong expectation added to control"
assert_equal "dummy expectation", output, "Expectation should have been returned"
# Examine the block. It should take one argument and simply return
# that argument. because of the 'suppress arguments to block'
# setting, the argument can only end up being a block, in practice.
trapper_block = @builder.options[:block]
assert_equal "the argument", trapper_block.call("the argument"),
"The block should merely return the passed argument"
end
end
+40
View File
@@ -0,0 +1,40 @@
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
require 'hardmock/method_cleanout'
require 'hardmock/mock_control'
require 'hardmock/errors'
require 'hardmock/expectation_builder'
require 'hardmock/expectation'
require 'hardmock/mock'
class VerifyErrorTest < Test::Unit::TestCase
include Hardmock
#
# TESTS
#
def test_formatted_list_of_unmet_expectations
mock1 = Mock.new('mock1')
mock2 = Mock.new('mock2')
exp1 = Expectation.new( :mock => mock1, :method => 'send_parts', :arguments => [1,2,:a] )
exp2 = Expectation.new( :mock => mock2, :method => 'grind_it', :arguments => [] )
exp_list = [ exp1, exp2 ]
err = VerifyError.new("This is the error", exp_list)
assert_equal "This is the error:\n * #{exp1.to_s}\n * #{exp2.to_s}", err.message
end
def test_empty_list_of_expectations
# this is not a normal case; not spending a lot of time to make this better
exp_list = []
err = VerifyError.new("This is the error:\n", exp_list)
end
def test_nil_expectation_list
# this is not a normal case; not spending a lot of time to make this better
exp_list = []
err = VerifyError.new("This is the error:\n", exp_list)
end
end