testicles 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|