testicles 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/LICENSE +22 -0
- data/README.rdoc +139 -0
- data/Rakefile +18 -0
- data/lib/testicles.rb +80 -0
- data/lib/testicles/report.rb +173 -0
- data/lib/testicles/reports.rb +2 -0
- data/lib/testicles/reports/progress.rb +111 -0
- data/lib/testicles/runner.rb +24 -0
- data/lib/testicles/test_case.rb +185 -0
- data/testicles.gemspec +31 -0
- metadata +65 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
doc
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2009 Nicolas Sanguinetti
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
= Testicles, the ballsy test framework
|
2
|
+
|
3
|
+
require "testicles"
|
4
|
+
|
5
|
+
class UserTest < Testicles::TestCase
|
6
|
+
setup do
|
7
|
+
@user = User.new(:name => "John Doe", :email => "john@example.org")
|
8
|
+
end
|
9
|
+
|
10
|
+
test "has a name" do
|
11
|
+
assert @user.name == "John Doe"
|
12
|
+
end
|
13
|
+
|
14
|
+
test "has an email" do
|
15
|
+
assert @user.email == "john@example.org"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Testicles is a small, simple, and easy-to-extend testing framework for ruby. It
|
20
|
+
was written as a replacement for Test::Unit, given how awful its code is, and
|
21
|
+
how difficult it is to extend in order to add new features.
|
22
|
+
|
23
|
+
I believe in minimalistic software, which is easily understood, easy to test,
|
24
|
+
and specially, easy to extend for third parties. That's where I'm aiming with
|
25
|
+
Testicles.
|
26
|
+
|
27
|
+
== Assertions
|
28
|
+
|
29
|
+
You probably wonder why the example doesn't have any assertion other than
|
30
|
+
+assert+. Why not +assert_equal+, right? Well, the idea is to keep it slim. If
|
31
|
+
you want to use more assertions, you're free to define your own. Also, since it
|
32
|
+
uses the same assertion API as Test::Unit, you can just require its assertions:
|
33
|
+
|
34
|
+
require "testicles"
|
35
|
+
require "test/unit/assertions"
|
36
|
+
|
37
|
+
class UserTest < Testicles::TestCase
|
38
|
+
include Test::Unit::Assertions
|
39
|
+
|
40
|
+
# now you can use all of Test::Unit assertions. For free.
|
41
|
+
end
|
42
|
+
|
43
|
+
You can even define rspec-like matchers if you want.
|
44
|
+
|
45
|
+
== Setup and teardown
|
46
|
+
|
47
|
+
If you need to run code before or after each test, declare a +setup+ or
|
48
|
+
+teardown+ block (respectively.)
|
49
|
+
|
50
|
+
class UserTest < Testicles::TestCase
|
51
|
+
setup do # this runs before each test
|
52
|
+
@user = User.create(:name => "John")
|
53
|
+
end
|
54
|
+
|
55
|
+
teardown do # this runs after each test
|
56
|
+
@user.destroy
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
+setup+ and +teardown+ blocks are evaluated in the same context as your test,
|
61
|
+
which means any instance variables defined in any of them are available in the
|
62
|
+
rest.
|
63
|
+
|
64
|
+
You can also use +global_setup+ and +global_teardown+ to run code only once per
|
65
|
+
test case. +global_setup+ blocks will run once before the first test is run, and
|
66
|
+
+global_teardown+ will run after all the tests have been run.
|
67
|
+
|
68
|
+
These methods, however, are dangerous, and should be used with caution, as
|
69
|
+
they might introduce dependencies between your tests if you don't write
|
70
|
+
your tests properly. Make sure that any state modified by code run in a
|
71
|
+
+global_setup+ or +global_teardown+ isn't changed in any of your tests.
|
72
|
+
|
73
|
+
Also, you should be aware that the code of +global_setup+ and +global_teardown+
|
74
|
+
blocks isn't evaluated in the same context as your tests and normal
|
75
|
+
+setup+/+teardown+ blocks are, so you can't share instance variables between
|
76
|
+
them.
|
77
|
+
|
78
|
+
== Nested contexts
|
79
|
+
|
80
|
+
Break down your test into logical chunks with nested contexts:
|
81
|
+
|
82
|
+
class UserTest < Testicles::TestCase
|
83
|
+
setup do
|
84
|
+
@user = User.make
|
85
|
+
end
|
86
|
+
|
87
|
+
context "validations" do
|
88
|
+
test "validates name" do
|
89
|
+
@user.name = nil
|
90
|
+
assert !@user.valid?
|
91
|
+
end
|
92
|
+
|
93
|
+
# etc, etc
|
94
|
+
end
|
95
|
+
|
96
|
+
context "something else" do
|
97
|
+
# your get the idea
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Any +setup+ or +teardown+ blocks you defined in a context will run in that
|
102
|
+
context and in _any_ other context nested in it.
|
103
|
+
|
104
|
+
== Pending tests
|
105
|
+
|
106
|
+
There are two ways of marking a test as pending. You can declare a test with no
|
107
|
+
body:
|
108
|
+
|
109
|
+
class SomeTest < Testicles::TestCase
|
110
|
+
test "this test will be marked as pending"
|
111
|
+
|
112
|
+
test "this tests is also pending"
|
113
|
+
|
114
|
+
test "this test isn't pending" do
|
115
|
+
assert true
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
Or you can call the +pending+ method from inside your test.
|
120
|
+
|
121
|
+
class SomeTest < Testicles::TestCase
|
122
|
+
test "this test is pending" do
|
123
|
+
pending "oops, this doesn't work"
|
124
|
+
assert false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
== (Best|Worst) name ever
|
129
|
+
|
130
|
+
I personally believe the name is full of win. But I understand it's not the
|
131
|
+
most marketable name :)
|
132
|
+
|
133
|
+
If you hate it, suggest a better name. Or just fork it and name your version
|
134
|
+
different, it's open source, after all.
|
135
|
+
|
136
|
+
== Legal
|
137
|
+
|
138
|
+
Author:: Nicolás Sanguinetti — http://nicolassanguinetti.info
|
139
|
+
License:: MIT (see bundled LICENSE file for more info)
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require "hanna/rdoctask"
|
3
|
+
rescue LoadError
|
4
|
+
require "rake/rdoctask"
|
5
|
+
end
|
6
|
+
|
7
|
+
Rake::RDocTask.new do |rd|
|
8
|
+
rd.main = "README.rdoc"
|
9
|
+
rd.title = "API Documentation for Testicles"
|
10
|
+
rd.rdoc_files.include("README.rdoc", "LICENSE", "lib/**/*.rb")
|
11
|
+
rd.rdoc_dir = "doc"
|
12
|
+
end
|
13
|
+
|
14
|
+
begin
|
15
|
+
require "mg"
|
16
|
+
MG.new("testicles.gemspec")
|
17
|
+
rescue LoadError
|
18
|
+
end
|
data/lib/testicles.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Testicles
|
2
|
+
# Exception raised when an assertion fails. See TestCase#assert
|
3
|
+
class AssertionFailed < StandardError; end
|
4
|
+
|
5
|
+
# Exception raised to mark a test as pending. See TestCase#pending
|
6
|
+
class Pending < StandardError; end
|
7
|
+
|
8
|
+
# Register a new Report. This will make your report available to Testicles,
|
9
|
+
# allowing you to run your tests through this report. For example
|
10
|
+
#
|
11
|
+
# module Testicles
|
12
|
+
# class Reports::MyAwesomeReport < Report
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# add_report :awesomesauce, MyAwesomeReport
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# See Testicles.report_with to see how to select which report will be used.
|
19
|
+
def self.add_report(name, report)
|
20
|
+
available_reports[name] = report
|
21
|
+
end
|
22
|
+
|
23
|
+
# Register a test case to be run with Testicles. This is done automatically
|
24
|
+
# whenever you subclass Testicles::TestCase, so you probably shouldn't pay
|
25
|
+
# much attention to this method.
|
26
|
+
def self.add_test_case(test_case)
|
27
|
+
available_test_cases << test_case
|
28
|
+
end
|
29
|
+
|
30
|
+
# Set to +false+ to avoid running tests +at_exit+. Default is +true+.
|
31
|
+
def self.autorun=(flag)
|
32
|
+
@autorun = flag
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks to see if tests should be run +at_exit+ or not. Default is +true+.
|
36
|
+
# See Testicles.autorun=
|
37
|
+
def self.autorun?
|
38
|
+
!!@autorun
|
39
|
+
end
|
40
|
+
|
41
|
+
# Run all registered test cases through the selected report. You can pass
|
42
|
+
# arguments to the Report constructor here.
|
43
|
+
#
|
44
|
+
# See Testicles.add_test_case and Testicles.report_with
|
45
|
+
def self.run_all_tests!(*report_args)
|
46
|
+
report = available_reports.fetch(@report).new(*report_args)
|
47
|
+
Runner.new(report).run(*available_test_cases)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Select the name of the Report to use when running tests. See
|
51
|
+
# Testicles.add_report for more information on registering a report.
|
52
|
+
#
|
53
|
+
# The default report is Testicles::Reports::Progress
|
54
|
+
def self.report_with(name)
|
55
|
+
@report = name
|
56
|
+
end
|
57
|
+
|
58
|
+
self.autorun = true
|
59
|
+
self.report_with(:progress)
|
60
|
+
|
61
|
+
def self.available_test_cases
|
62
|
+
@test_cases ||= []
|
63
|
+
end
|
64
|
+
private_class_method :available_test_cases
|
65
|
+
|
66
|
+
def self.available_reports
|
67
|
+
@available_reports ||= {}
|
68
|
+
end
|
69
|
+
private_class_method :available_reports
|
70
|
+
end
|
71
|
+
|
72
|
+
require "testicles/test_case"
|
73
|
+
require "testicles/runner"
|
74
|
+
require "testicles/report"
|
75
|
+
require "testicles/reports"
|
76
|
+
require "testicles/reports/progress"
|
77
|
+
|
78
|
+
at_exit do
|
79
|
+
Testicles.run_all_tests! if Testicles.autorun?
|
80
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Testicles
|
2
|
+
class Report
|
3
|
+
# Seconds taken to run the test suite.
|
4
|
+
#
|
5
|
+
# TODO: this doesn't belong here, it's being set from within the test
|
6
|
+
# runner. But we need the value here.
|
7
|
+
attr_accessor :time_elapsed
|
8
|
+
|
9
|
+
# Define an event handler for your report. The different events fired in a
|
10
|
+
# report's life cycle are:
|
11
|
+
#
|
12
|
+
# :start:: Fired by the runner when starting the whole test suite.
|
13
|
+
# :enter:: Fired by the runner when starting a particular test case. It
|
14
|
+
# will get the test case as an argument.
|
15
|
+
# :assertion:: Fired by a test each time an assertion is run.
|
16
|
+
# :pass:: Fired by a test after it runs successfully without errors.
|
17
|
+
# It will get an instance of PassedTest as an argument.
|
18
|
+
# :pending:: Fired by a test which doesn't provide a test block or which
|
19
|
+
# calls TestCase#pending. It will get an instance of
|
20
|
+
# PendingTest as an argument.
|
21
|
+
# :failure:: Fired by a test in which an assertion failed. It will get an
|
22
|
+
# instance of FailedTest as an argument.
|
23
|
+
# :error:: Fired by a test where an uncaught exception was found. It
|
24
|
+
# will get an instance of ErroredTest as an argument.
|
25
|
+
# :exit:: Fired by the runner each time a test case finishes. It will
|
26
|
+
# take the test case as an argument.
|
27
|
+
# :end:: Fired by the runner at the end of the whole test suite.
|
28
|
+
#
|
29
|
+
# The event handler will receive the report as a first argument, plus any
|
30
|
+
# arguments documented above (depending on the event). It will also ensure
|
31
|
+
# that any handler for the same event declared on an ancestor class is run.
|
32
|
+
def self.on(event, &block)
|
33
|
+
define_method(:"on_#{event}") do |*args|
|
34
|
+
begin
|
35
|
+
super(*args)
|
36
|
+
rescue NoMethodError
|
37
|
+
end
|
38
|
+
|
39
|
+
block.call(self, *args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
on :pass do |report, passed_test|
|
44
|
+
report.passes << passed_test
|
45
|
+
end
|
46
|
+
|
47
|
+
on :pending do |report, pending_test|
|
48
|
+
report.pendings << pending_test
|
49
|
+
end
|
50
|
+
|
51
|
+
on :failure do |report, failed_test|
|
52
|
+
report.failures << failed_test
|
53
|
+
report.failures_and_errors << failed_test
|
54
|
+
end
|
55
|
+
|
56
|
+
on :error do |report, errored_test|
|
57
|
+
report.errors << errored_test
|
58
|
+
report.failures_and_errors << errored_test
|
59
|
+
end
|
60
|
+
|
61
|
+
on :assertion do |report|
|
62
|
+
report.add_assertion
|
63
|
+
end
|
64
|
+
|
65
|
+
# Run a test and report if it passes, fails, or is pending. Takes the name
|
66
|
+
# of the test as an argument. You can avoid reporting a passed test by
|
67
|
+
# passing +false+ as a second argument.
|
68
|
+
def report(name, report_success=true)
|
69
|
+
yield
|
70
|
+
on_pass(PassedTest.new(name)) if report_success
|
71
|
+
rescue Pending => e
|
72
|
+
on_pending(PendingTest.new(name, e))
|
73
|
+
rescue AssertionFailed => e
|
74
|
+
on_failure(FailedTest.new(name, e))
|
75
|
+
rescue Exception => e
|
76
|
+
on_error(ErroredTest.new(name, e))
|
77
|
+
end
|
78
|
+
|
79
|
+
# List all the tests (as PendingTest instances) that were pending.
|
80
|
+
def pendings
|
81
|
+
@pendings ||= []
|
82
|
+
end
|
83
|
+
|
84
|
+
# List all the tests (as PassedTest instances) that passed.
|
85
|
+
def passes
|
86
|
+
@passes ||= []
|
87
|
+
end
|
88
|
+
|
89
|
+
# List all the tests (as FailedTest instances) that failed an assertion.
|
90
|
+
def failures
|
91
|
+
@failures ||= []
|
92
|
+
end
|
93
|
+
|
94
|
+
# List all the tests (as ErroredTest instances) that raised an unrescued
|
95
|
+
# exception.
|
96
|
+
def errors
|
97
|
+
@errors ||= []
|
98
|
+
end
|
99
|
+
|
100
|
+
# Aggregated and ordered list of tests that either failed an assertion or
|
101
|
+
# raised an unrescued exception. Useful for displaying back to the user.
|
102
|
+
def failures_and_errors
|
103
|
+
@failures_and_errors ||= []
|
104
|
+
end
|
105
|
+
|
106
|
+
# Log an assertion was run (whether it succeeded or failed.)
|
107
|
+
def add_assertion
|
108
|
+
@assertions ||= 0
|
109
|
+
@assertions += 1
|
110
|
+
end
|
111
|
+
|
112
|
+
# Number of assertions run during the report.
|
113
|
+
def assertions
|
114
|
+
@assertions || 0
|
115
|
+
end
|
116
|
+
|
117
|
+
# Amount ot tests run (whether passed, pending, failed, or errored.)
|
118
|
+
def total_tests
|
119
|
+
passes.size + failures.size + errors.size + pendings.size
|
120
|
+
end
|
121
|
+
|
122
|
+
# Encapsulate the relevant information for a test that passed.
|
123
|
+
class PassedTest
|
124
|
+
# Name of the test that passed. Useful for certain reports.
|
125
|
+
attr_reader :test_name
|
126
|
+
|
127
|
+
def initialize(test_name) #:nodoc:
|
128
|
+
@test_name = test_name
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Encapsulates the relevant information for a test which failed an
|
133
|
+
# assertion.
|
134
|
+
class FailedTest < PassedTest
|
135
|
+
def initialize(test_name, error) #:nodoc:
|
136
|
+
super(test_name)
|
137
|
+
@error = error
|
138
|
+
end
|
139
|
+
|
140
|
+
# Message with which it failed the assertion
|
141
|
+
def error_message
|
142
|
+
@error.message
|
143
|
+
end
|
144
|
+
|
145
|
+
# Line of the file where the assertion failed
|
146
|
+
def line
|
147
|
+
backtrace.first.split(":")[1]
|
148
|
+
end
|
149
|
+
|
150
|
+
# File where the assertion failed
|
151
|
+
def file
|
152
|
+
backtrace.first.split(":")[0]
|
153
|
+
end
|
154
|
+
|
155
|
+
# Backtrace of the assertion
|
156
|
+
def backtrace
|
157
|
+
@error.backtrace
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Encapsulates the relevant information for a test which raised an
|
162
|
+
# unrescued exception.
|
163
|
+
class ErroredTest < FailedTest
|
164
|
+
end
|
165
|
+
|
166
|
+
# Encapsulates the relevant information for a test that the user marked as
|
167
|
+
# pending.
|
168
|
+
class PendingTest < FailedTest
|
169
|
+
# Message passed to TestCase#pending, if any.
|
170
|
+
alias_method :pending_message, :error_message
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Testicles
|
2
|
+
# The +:progress+ report will output a +.+ for each passed test in the suite,
|
3
|
+
# a +P+ for each pending test, an +F+ for each test that failed an assertion,
|
4
|
+
# and an +E+ for each test that raised an unrescued exception.
|
5
|
+
#
|
6
|
+
# At the end of the suite it will output a list of all pending tests, with
|
7
|
+
# files and line numbers, and after that a list of all failures and errors,
|
8
|
+
# which also contains the first 3 lines of the backtrace for each.
|
9
|
+
class Reports::Progress < Report
|
10
|
+
# Set the stream where the report will be written to. STDOUT by default.
|
11
|
+
def initialize(stream=STDOUT)
|
12
|
+
@stream = stream
|
13
|
+
end
|
14
|
+
|
15
|
+
on :end do |report|
|
16
|
+
report.instance_eval do
|
17
|
+
puts
|
18
|
+
puts
|
19
|
+
report_pending_tests unless pendings.empty?
|
20
|
+
report_errors unless errors.empty?
|
21
|
+
puts summary
|
22
|
+
puts running_time
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
on :pass do |report, pass|
|
27
|
+
report.send(:print, ".")
|
28
|
+
end
|
29
|
+
|
30
|
+
on :pending do |report, pending|
|
31
|
+
report.send(:print, "P")
|
32
|
+
end
|
33
|
+
|
34
|
+
on :failure do |report, failure|
|
35
|
+
report.send(:print, "F")
|
36
|
+
end
|
37
|
+
|
38
|
+
on :error do |report, error|
|
39
|
+
report.send(:print, "E")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def running_time
|
45
|
+
"Ran in #{time_elapsed} seconds"
|
46
|
+
end
|
47
|
+
|
48
|
+
def report_pending_tests
|
49
|
+
puts "Pending tests:"
|
50
|
+
puts
|
51
|
+
|
52
|
+
pad_indexes = pendings.size.to_s.size
|
53
|
+
pendings.each_with_index do |pending, index|
|
54
|
+
puts " #{pad(index+1, pad_indexes)}) #{pending.test_name} (#{pending.pending_message})"
|
55
|
+
puts indent("On line #{pending.line} of `#{pending.file}'", 6 + pad_indexes)
|
56
|
+
puts
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def report_errors
|
61
|
+
puts "Failures:"
|
62
|
+
puts
|
63
|
+
|
64
|
+
pad_indexes = failures_and_errors.size.to_s.size
|
65
|
+
failures_and_errors.each_with_index do |error, index|
|
66
|
+
puts " #{pad(index+1, pad_indexes)}) #{test_type(error)}: `#{error.test_name}' (on line #{error.line} of `#{error.file}')"
|
67
|
+
puts indent("With `#{error.error_message}'", 6 + pad_indexes)
|
68
|
+
puts indent(error.backtrace[0..2].join("\n"), 6 + pad_indexes)
|
69
|
+
puts
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def summary
|
74
|
+
"%d test%s, %d assertion%s (%d passed, %d pending, %d failed, %d errored)" % [total_tests,
|
75
|
+
total_tests == 1 ? "" : "s",
|
76
|
+
assertions,
|
77
|
+
assertions == 1 ? "" : "s",
|
78
|
+
passes.size,
|
79
|
+
pendings.size,
|
80
|
+
failures.size,
|
81
|
+
errors.size]
|
82
|
+
end
|
83
|
+
|
84
|
+
def indent(strings, size=2, indent_with=" ")
|
85
|
+
Array(strings).map do |str|
|
86
|
+
str.to_s.split("\n").map {|s| indent_with * size + s }.join("\n")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def pad(str, amount)
|
91
|
+
" " * (amount - str.to_s.size) + str.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_type(test)
|
95
|
+
case test # order is important since ErroredTest < FailedTest
|
96
|
+
when ErroredTest; "Error"
|
97
|
+
when FailedTest; "Failure"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def print(*args)
|
102
|
+
@stream.print(*args)
|
103
|
+
end
|
104
|
+
|
105
|
+
def puts(*args)
|
106
|
+
@stream.puts(*args)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
add_report :progress, Reports::Progress
|
111
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Testicles
|
2
|
+
class Runner
|
3
|
+
# Set up the test runner. Takes in a Report that will be passed to test
|
4
|
+
# cases for reporting.
|
5
|
+
def initialize(report)
|
6
|
+
@report = report
|
7
|
+
end
|
8
|
+
|
9
|
+
# Run a set of test cases, provided as arguments. This will fire relevant
|
10
|
+
# events on the runner's report, at the +start+ and +end+ of the test run,
|
11
|
+
# and before and after each test case (+enter+ and +exit+.)
|
12
|
+
def run(*test_cases)
|
13
|
+
@report.on_start if @report.respond_to?(:on_start)
|
14
|
+
started_at = Time.now
|
15
|
+
test_cases.each do |test_case|
|
16
|
+
@report.on_enter(test_case) if @report.respond_to?(:on_enter)
|
17
|
+
test_case.run(@report)
|
18
|
+
@report.on_exit(test_case) if @report.respond_to?(:on_exit)
|
19
|
+
end
|
20
|
+
@report.time_elapsed = Time.now - started_at
|
21
|
+
@report.on_end if @report.respond_to?(:on_end)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Testicles
|
2
|
+
class TestCase
|
3
|
+
# Run all tests in this context. Takes a Report instance in order to
|
4
|
+
# provide output.
|
5
|
+
def self.run(result)
|
6
|
+
result.report("#{description} global setup", false) { do_global_setup }
|
7
|
+
tests.each {|test| test.run(result) }
|
8
|
+
result.report("#{description} global teardown", false) { do_global_teardown }
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a test to be run in this context. This method is aliased as +it+ and
|
12
|
+
# +should+ for your comfort.
|
13
|
+
def self.test(name, &block)
|
14
|
+
tests << new(name, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add a setup block to be run before each test in this context. This method
|
18
|
+
# is aliased as +before+ for your comfort.
|
19
|
+
def self.setup(&block)
|
20
|
+
define_method :setup do
|
21
|
+
super
|
22
|
+
instance_eval(&block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add a +setup+ block that will be run *once* for the entire test case,
|
27
|
+
# before the first test is run.
|
28
|
+
#
|
29
|
+
# Keep in mind that while +setup+ blocks are evaluated on the context of the
|
30
|
+
# test, and thus you can share state between them, your tests will not be
|
31
|
+
# able to access instance variables set in a +global_setup+ block.
|
32
|
+
#
|
33
|
+
# This is usually not needed (and generally using it is a code smell, since
|
34
|
+
# you could make a test dependent on the state of other tests, which is a
|
35
|
+
# huge problem), but it comes in handy when you need to do expensive
|
36
|
+
# operations in your test setup/teardown and the tests won't modify the
|
37
|
+
# state set on this operations. For example, creating large amount of
|
38
|
+
# records in a database or filesystem, when your tests will only read these
|
39
|
+
# records.
|
40
|
+
def self.global_setup(&block)
|
41
|
+
(class << self; self; end).class_eval do
|
42
|
+
define_method :do_global_setup do
|
43
|
+
super
|
44
|
+
instance_eval(&block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Add a teardown block to be run after each test in this context. This
|
50
|
+
# method is aliased as +after+ for your comfort.
|
51
|
+
def self.teardown(&block)
|
52
|
+
define_method :teardown do
|
53
|
+
instance_eval(&block)
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add a +teardown+ block that will be run *once* for the entire test case,
|
59
|
+
# after the last test is run.
|
60
|
+
#
|
61
|
+
# Keep in mind that while +teardown+ blocks are evaluated on the context of
|
62
|
+
# the test, and thus you can share state between the tests and the
|
63
|
+
# teardown blocks, you will not be able to access instance variables set in
|
64
|
+
# a test from your +global_teardown+ block.
|
65
|
+
#
|
66
|
+
# See TestCase.global_setup for a discussion on why these methods are best
|
67
|
+
# avoided unless you really need them and use them carefully.
|
68
|
+
def self.global_teardown(&block)
|
69
|
+
(class << self; self; end).class_eval do
|
70
|
+
define_method :do_global_teardown do
|
71
|
+
instance_eval(&block)
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Define a new test context nested under the current one. All +setup+ and
|
79
|
+
# +teardown+ blocks defined on the current context will be inherited by the
|
80
|
+
# new context. This method is aliased as +describe+ for your comfort.
|
81
|
+
def self.context(description, &block)
|
82
|
+
subclass = Class.new(self)
|
83
|
+
subclass.class_eval(&block) if block
|
84
|
+
subclass.description = "#{self.description} #{description}".strip
|
85
|
+
const_set(sanitize_description(description), subclass)
|
86
|
+
end
|
87
|
+
|
88
|
+
class << self
|
89
|
+
# Fancy name for your test case, reports can use this to give nice,
|
90
|
+
# descriptive output when running your tests.
|
91
|
+
attr_accessor :description
|
92
|
+
|
93
|
+
alias_method :describe, :context
|
94
|
+
alias_method :story, :context
|
95
|
+
|
96
|
+
alias_method :before, :setup
|
97
|
+
alias_method :after, :teardown
|
98
|
+
|
99
|
+
alias_method :before_all, :global_setup
|
100
|
+
alias_method :after_all, :global_setup
|
101
|
+
|
102
|
+
alias_method :it, :test
|
103
|
+
alias_method :should, :test
|
104
|
+
alias_method :scenario, :test
|
105
|
+
end
|
106
|
+
|
107
|
+
# Initialize a new instance of a single test. This test can be run in
|
108
|
+
# isolation by calling TestCase#run.
|
109
|
+
def initialize(name, &block)
|
110
|
+
@test = block
|
111
|
+
@name = name
|
112
|
+
end
|
113
|
+
|
114
|
+
# Run a test in isolation. Any +setup+ and +teardown+ blocks defined for
|
115
|
+
# this test case will be run as expected.
|
116
|
+
#
|
117
|
+
# You need to provide a Report instance to handle errors/pending tests/etc.
|
118
|
+
#
|
119
|
+
# If the test's block is nil, then the test will be marked as pending and
|
120
|
+
# nothing will be run.
|
121
|
+
def run(result)
|
122
|
+
@result = result
|
123
|
+
|
124
|
+
result.report(name) do
|
125
|
+
pending if test.nil?
|
126
|
+
|
127
|
+
setup
|
128
|
+
instance_eval(&test)
|
129
|
+
teardown
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Ensure a condition is met. This will raise AssertionFailed if the
|
134
|
+
# condition isn't met. You can override the default failure message
|
135
|
+
# by passing it as an argument.
|
136
|
+
def assert(condition, message="Expected condition to be satisfied")
|
137
|
+
@result.on_assertion
|
138
|
+
raise AssertionFailed, message unless condition
|
139
|
+
end
|
140
|
+
|
141
|
+
# Make the test be ignored as pending. You can override the default message
|
142
|
+
# that will be sent to the report by passing it as an argument.
|
143
|
+
def pending(message="Not Yet Implemented")
|
144
|
+
raise Pending, message
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def setup #:nodoc:
|
150
|
+
end
|
151
|
+
|
152
|
+
def teardown #:nodoc:
|
153
|
+
end
|
154
|
+
|
155
|
+
def test
|
156
|
+
@test
|
157
|
+
end
|
158
|
+
|
159
|
+
def name
|
160
|
+
@name
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.tests
|
164
|
+
@tests ||= []
|
165
|
+
end
|
166
|
+
private_class_method :tests
|
167
|
+
|
168
|
+
def self.sanitize_description(description)
|
169
|
+
"Test#{description.gsub(/\W+/, ' ').strip.gsub(/(^| )(\w)/) { $2.upcase }}".to_sym
|
170
|
+
end
|
171
|
+
private_class_method :sanitize_description
|
172
|
+
|
173
|
+
def self.do_global_setup
|
174
|
+
end
|
175
|
+
private_class_method :do_global_setup
|
176
|
+
|
177
|
+
def self.do_global_teardown
|
178
|
+
end
|
179
|
+
private_class_method :do_global_teardown
|
180
|
+
|
181
|
+
def self.inherited(child)
|
182
|
+
Testicles.add_test_case(child)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
data/testicles.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "testicles"
|
3
|
+
s.version = "0.1"
|
4
|
+
s.date = "2009-09-11"
|
5
|
+
|
6
|
+
s.description = "Testicles is a tiny, simple, and easy-to-extend test framework"
|
7
|
+
s.summary = s.description
|
8
|
+
s.homepage = "http://github.com/foca/testicles"
|
9
|
+
|
10
|
+
s.authors = ["Nicolás Sanguinetti"]
|
11
|
+
s.email = "contacto@nicolassanguinetti.info"
|
12
|
+
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
s.rubyforge_project = "testicles"
|
15
|
+
s.has_rdoc = true
|
16
|
+
s.rubygems_version = "1.3.1"
|
17
|
+
|
18
|
+
s.files = %w[
|
19
|
+
.gitignore
|
20
|
+
LICENSE
|
21
|
+
README.rdoc
|
22
|
+
Rakefile
|
23
|
+
testicles.gemspec
|
24
|
+
lib/testicles.rb
|
25
|
+
lib/testicles/test_case.rb
|
26
|
+
lib/testicles/runner.rb
|
27
|
+
lib/testicles/report.rb
|
28
|
+
lib/testicles/reports.rb
|
29
|
+
lib/testicles/reports/progress.rb
|
30
|
+
]
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: testicles
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Nicol\xC3\xA1s Sanguinetti"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-11 00:00:00 -03:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Testicles is a tiny, simple, and easy-to-extend test framework
|
17
|
+
email: contacto@nicolassanguinetti.info
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- LICENSE
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- testicles.gemspec
|
30
|
+
- lib/testicles.rb
|
31
|
+
- lib/testicles/test_case.rb
|
32
|
+
- lib/testicles/runner.rb
|
33
|
+
- lib/testicles/report.rb
|
34
|
+
- lib/testicles/reports.rb
|
35
|
+
- lib/testicles/reports/progress.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://github.com/foca/testicles
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project: testicles
|
60
|
+
rubygems_version: 1.3.2
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: Testicles is a tiny, simple, and easy-to-extend test framework
|
64
|
+
test_files: []
|
65
|
+
|