tst 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.md +190 -0
- data/Rakefile +45 -0
- data/bin/tst +69 -0
- data/lib/tst/reporters/plain.rb +87 -0
- data/lib/tst/reporters/pretty.rb +29 -0
- data/lib/tst/reporters.rb +25 -0
- data/lib/tst.rb +267 -0
- data/test/assertions.rb +61 -0
- data/test/fixtures/exception.rb +3 -0
- data/test/fixtures/failure.rb +3 -0
- data/test/fixtures/success.rb +3 -0
- data/test/plain_reporter.rb +56 -0
- data/test/test.rb +23 -0
- metadata +66 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Arun Srinivasan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
# Tst
|
2
|
+
|
3
|
+
A small testing library that tries not to do too much.
|
4
|
+
|
5
|
+
You get a top-level `tst` method, a few `assert` methods, and readable test
|
6
|
+
output.
|
7
|
+
|
8
|
+
# Installation
|
9
|
+
|
10
|
+
Standard issue:
|
11
|
+
|
12
|
+
gem install tst
|
13
|
+
|
14
|
+
# Usage
|
15
|
+
|
16
|
+
The `tst` method is available on the top-level, takes an optional name and
|
17
|
+
a required block, like so:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
tst "Array-indexing is not 1-based" do
|
21
|
+
arr = [1, 2, 3, 4, 5]
|
22
|
+
assert arr[1] != 1
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
If the block runs to completion without raising anything, it passes.
|
27
|
+
|
28
|
+
If one of assertion methods (see below) raises, it counts as a failure.
|
29
|
+
|
30
|
+
If the block raises for any other reason, it's an exception.
|
31
|
+
|
32
|
+
### Assertions
|
33
|
+
|
34
|
+
Within a `tst` block, you have access to 3 assert methods: `assert`,
|
35
|
+
`assert_equal`, and `assert_raises`. They do what you'd expect.
|
36
|
+
|
37
|
+
If those don't cover your needs, just write your own:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
module Tst
|
41
|
+
|
42
|
+
# The assertions live in the Assertions module and are available
|
43
|
+
# to the `tst` blocks.
|
44
|
+
module Assertions
|
45
|
+
|
46
|
+
# No naming or argument rules, it's just a method.
|
47
|
+
# Define it how you please.
|
48
|
+
def sums_to_42(*args)
|
49
|
+
actual = args.inject(0) { |sum, arg| sum + arg }
|
50
|
+
|
51
|
+
# return without raising to pass
|
52
|
+
return if actual == 42
|
53
|
+
|
54
|
+
# Tst::Failure is a subclass of StandardError that takes
|
55
|
+
# 3 arguments: Failure.new(message, expected, actual)
|
56
|
+
#
|
57
|
+
# message - a brief description of the failure
|
58
|
+
# expected - the expected value
|
59
|
+
# actual - the actual value
|
60
|
+
raise Tst::Failure.new("#{args} doesn't sum to 42", 42, actual)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Now just use your custom assertion in a test
|
66
|
+
tst "some array sums to 42" do
|
67
|
+
sums_to_42(40, 1, 1) # This will pass just fine
|
68
|
+
sums_to_42(40, 1, 2) # This will fail
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
Running that test will yield output like:
|
73
|
+
|
74
|
+
$ tst 'some_array.rb'
|
75
|
+
F
|
76
|
+
F1: [40, 1, 2] doesn't sum to 42 - "some array sums to 42"
|
77
|
+
exp: 42
|
78
|
+
act: 43
|
79
|
+
* some_array.rb:<lineno>:in `block in <top (required)>'
|
80
|
+
|
81
|
+
### Running tests
|
82
|
+
|
83
|
+
Tst comes with a command-line runner, named "tst".
|
84
|
+
|
85
|
+
$ tst
|
86
|
+
Usage: tst [options] files
|
87
|
+
|
88
|
+
-h, --help Displays this message
|
89
|
+
|
90
|
+
-I, --libdir A directory to add to the load path
|
91
|
+
(can be used multiple times: "-I lib -I test"
|
92
|
+
|
93
|
+
-r, --reporter Specify a reporter to use. "pretty" or "plain".
|
94
|
+
Defaults to "plain"
|
95
|
+
|
96
|
+
files Specifies which test files to run. Is passed through to
|
97
|
+
Dir.glob, so anything it accepts is valid. NOTE: use
|
98
|
+
quotes to make sure nothing gets swallowed.
|
99
|
+
|
100
|
+
You can also run tests programmatically. Here's a sample rake task, for
|
101
|
+
instance:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
require 'tst'
|
105
|
+
|
106
|
+
desc 'Run the tests'
|
107
|
+
task :test do
|
108
|
+
Tst.run 'test/*.rb',
|
109
|
+
:load_paths => ['.', 'lib'], # same as -I above
|
110
|
+
:reporter => Tst::Reporters::Pretty.new # same as -r above
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
# Meanderings
|
115
|
+
|
116
|
+
### Why aren't there setup and teardown methods?
|
117
|
+
|
118
|
+
Having setup and teardown methods makes it harder to look at one test and see
|
119
|
+
what's happening, as it's easy for relevant stuff to be scrolled off the
|
120
|
+
screen, or even hidden in a superclass. And the names are totally generic.
|
121
|
+
|
122
|
+
And we're in ruby! You can get what you need by just writing a method _with
|
123
|
+
a name_ that encapsulates what you need. Take a look at
|
124
|
+
"test/plain_reporter.rb" for an example.
|
125
|
+
|
126
|
+
### Lightness
|
127
|
+
|
128
|
+
Tst tries to avoid weight, in various forms:
|
129
|
+
|
130
|
+
**Weighty Structure**
|
131
|
+
|
132
|
+
Tst has no grouping - TestCases, Context blocks, etc, because we're in ruby,
|
133
|
+
where such ceremony is unnecessary. It is not required to make the testing
|
134
|
+
tools work, and I'm not sure it yields much benefit. We already put our test
|
135
|
+
code into files with names. Is further taxonomy really necessary?
|
136
|
+
|
137
|
+
I believe the lack of ceremony is easier to understand, learn, use, and is
|
138
|
+
nicer to look at.
|
139
|
+
|
140
|
+
**Conceptual weight**
|
141
|
+
|
142
|
+
This may just be a personal preference thing, but I don't understand the appeal
|
143
|
+
of the 'english-y' DSL's.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
# Test tool concepts
|
147
|
+
# ------------------
|
148
|
+
# Compare this...
|
149
|
+
tst "a new Thing is not empty" do # :tst
|
150
|
+
thing = Thing.new
|
151
|
+
assert !thing.empty? # :assert
|
152
|
+
end
|
153
|
+
|
154
|
+
# ... to this (RSpec):
|
155
|
+
describe Thing do # :describe
|
156
|
+
let(:thing) { Thing.new } # :let
|
157
|
+
|
158
|
+
it "should be empty" do # :it
|
159
|
+
thing.should be_empty # :should
|
160
|
+
end # magic :be_* method
|
161
|
+
# how :thing gets in scope
|
162
|
+
end
|
163
|
+
|
164
|
+
# or with even more magic:
|
165
|
+
describe Thing do # :describe
|
166
|
+
it { should be_empty } # :it
|
167
|
+
end # magic Thing initialization
|
168
|
+
# implicit :subject
|
169
|
+
# :should (w/ implicit :subject)
|
170
|
+
# :be_*
|
171
|
+
```
|
172
|
+
|
173
|
+
Some argue that the RSpec examples read well, but look at how much you need to
|
174
|
+
know to understand what's going on.
|
175
|
+
|
176
|
+
Don't get me wrong. RSpec is a pretty cool implementation of an interesting
|
177
|
+
idea, but I'm not convinced that it's worth the conceptual overhead when you
|
178
|
+
consider what you get.
|
179
|
+
|
180
|
+
**Code weight**
|
181
|
+
|
182
|
+
Just looking in the 'lib' directories:
|
183
|
+
|
184
|
+
- test/unit: 6313
|
185
|
+
- rspec: 5460
|
186
|
+
- minitest: 1210
|
187
|
+
- tst: 220
|
188
|
+
|
189
|
+
The core of tst's testing engine is really just 122 lines (see 'tst.rb'). The
|
190
|
+
other 98 is IO/reporting code.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
$:.unshift File.expand_path('lib')
|
2
|
+
require 'tst'
|
3
|
+
|
4
|
+
version = Tst::VERSION
|
5
|
+
|
6
|
+
desc 'Run the tests'
|
7
|
+
task :test do
|
8
|
+
Tst.run 'test/*.rb',
|
9
|
+
:load_paths => ['.', 'lib'],
|
10
|
+
:reporter => Tst::Reporters::Pretty.new
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :test do
|
14
|
+
desc 'For fun, run the fixtures'
|
15
|
+
task :fixtures do
|
16
|
+
Tst.run 'test/fixtures/*.rb',
|
17
|
+
:load_paths => ['.', 'lib'],
|
18
|
+
:reporter => Tst::Reporters::Pretty.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace :gem do
|
23
|
+
desc 'Clean up generated files'
|
24
|
+
task :clean do
|
25
|
+
sh 'rm -rf pkg'
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Build the gem"
|
29
|
+
task :build => :clean do
|
30
|
+
sh "mkdir pkg"
|
31
|
+
sh "gem build tst.gemspec"
|
32
|
+
sh "mv tst-#{version}.gem pkg/"
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Release v#{version}"
|
36
|
+
task :release => :build do
|
37
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
38
|
+
sh "git tag v#{version}"
|
39
|
+
sh "git push origin master"
|
40
|
+
sh "git push origin v#{version}"
|
41
|
+
sh "gem push pkg/#{name}-#{version}.gem"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
task :default => :test
|
data/bin/tst
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.expand_path('lib')
|
4
|
+
require 'getoptlong'
|
5
|
+
require 'tst'
|
6
|
+
|
7
|
+
Banner = <<-Banner
|
8
|
+
Usage: tst [options] files
|
9
|
+
|
10
|
+
-h, --help Displays this message
|
11
|
+
|
12
|
+
-I, --libdir A directory to add to the load path
|
13
|
+
(can be used multiple times: "-I lib -I test"
|
14
|
+
|
15
|
+
-r, --reporter Specify a reporter to use. "pretty" or "plain".
|
16
|
+
Defaults to "plain"
|
17
|
+
|
18
|
+
files Specifies which test files to run. Is passed through to
|
19
|
+
Dir.glob, so anything it accepts is valid. NOTE: use
|
20
|
+
quotes to make sure nothing gets swallowed.
|
21
|
+
Banner
|
22
|
+
|
23
|
+
def parse_options!(options)
|
24
|
+
GetoptLong.new(
|
25
|
+
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
26
|
+
['--libdir', '-I', GetoptLong::REQUIRED_ARGUMENT],
|
27
|
+
['--reporter', '-r', GetoptLong::REQUIRED_ARGUMENT]
|
28
|
+
).each do |switch, arg|
|
29
|
+
case switch
|
30
|
+
when '--help'
|
31
|
+
exit_with(Banner)
|
32
|
+
when '--reporter'
|
33
|
+
set_reporter(arg, options) || exit_with("Unknown reporter '#{arg}'.")
|
34
|
+
when '--libdir'
|
35
|
+
add_include_dir(arg, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
exit_with(Banner) unless glob = ARGV.shift
|
40
|
+
return glob, options
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_reporter(reporter, options)
|
44
|
+
case reporter
|
45
|
+
when "pretty"
|
46
|
+
options[:reporter] = Tst::Reporters::Pretty.new
|
47
|
+
true
|
48
|
+
when "plain"
|
49
|
+
true
|
50
|
+
else
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_include_dir(dir, options)
|
56
|
+
options[:load_paths] << dir
|
57
|
+
end
|
58
|
+
|
59
|
+
def exit_with(msg)
|
60
|
+
puts msg
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
|
64
|
+
glob, options = parse_options!(
|
65
|
+
:reporter => Tst::Reporters::Plain.new,
|
66
|
+
:load_paths => []
|
67
|
+
)
|
68
|
+
|
69
|
+
Tst.run(glob, options)
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Tst
|
2
|
+
module Reporters
|
3
|
+
class Plain
|
4
|
+
attr_reader :io, :results
|
5
|
+
|
6
|
+
def initialize(io = $stdout)
|
7
|
+
@io = io
|
8
|
+
end
|
9
|
+
|
10
|
+
def success(result) io.print('.') end
|
11
|
+
def failure(result) io.print('F') end
|
12
|
+
def exception(result) io.print('E') end
|
13
|
+
|
14
|
+
def summarize(results)
|
15
|
+
@results = results
|
16
|
+
summary = ["\n"] + failures + exceptions + conclusion
|
17
|
+
summary.each { |line| io.puts(line) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def failures
|
21
|
+
results.failures.each_with_index.flat_map do |failure, i|
|
22
|
+
failure_details(failure.name, failure.exception, i)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def failure_details(name, exception, i)
|
27
|
+
[ failure_header(name, exception, i),
|
28
|
+
expected_actual(exception),
|
29
|
+
backtrace(exception.backtrace),
|
30
|
+
"" ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure_header(name, exception, i)
|
34
|
+
"F#{i+1}: #{exception.message} - #{name.inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def expected_actual(exception)
|
38
|
+
[" exp: #{exception.expected.inspect}",
|
39
|
+
" got: #{exception.actual.inspect}"
|
40
|
+
].join("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
def exceptions
|
44
|
+
results.exceptions.each_with_index.flat_map do |exception, i|
|
45
|
+
exception_details(exception.name, exception.exception, i)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def exception_details(name, exception, i)
|
50
|
+
[ exception_header(name, exception, i),
|
51
|
+
backtrace(exception.backtrace),
|
52
|
+
"" ]
|
53
|
+
end
|
54
|
+
|
55
|
+
def exception_header(name, exception, i)
|
56
|
+
"E#{i+1}: #{exception.inspect} - #{name.inspect}"
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def backtrace(trace)
|
61
|
+
filter_backtrace(trace).map { |line| "* #{line}" }.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def conclusion
|
65
|
+
[ "#{elapsed}: #{passed}, #{failed}, #{raised}." ]
|
66
|
+
end
|
67
|
+
|
68
|
+
def elapsed
|
69
|
+
"Ran %d tests in %.6fs" % [results.count, results.elapsed]
|
70
|
+
end
|
71
|
+
def passed; "#{results.successes.count} passed" end
|
72
|
+
def failed; "#{results.failures.count} failed" end
|
73
|
+
def raised; "#{results.exceptions.count} raised" end
|
74
|
+
|
75
|
+
def filter_backtrace(backtrace)
|
76
|
+
backtrace.reject do |line|
|
77
|
+
line =~ BACKTRACE_FILTER
|
78
|
+
end.map do |line|
|
79
|
+
line.gsub("#{Dir.pwd}/", "")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
BACKTRACE_FILTER =
|
84
|
+
/lib\/tst\.rb|bin\/tst|\/(ruby|jruby|rbx)[-\/]([0-9\.])+|Rakefile/
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Tst
|
2
|
+
module Reporters
|
3
|
+
class Pretty < Plain
|
4
|
+
def color(n, s) "\e[#{n}m#{s}\e[0m" end
|
5
|
+
|
6
|
+
def black(s) color(30, s) end
|
7
|
+
def red(s) color(31, s) end
|
8
|
+
def green(s) color(32, s) end
|
9
|
+
def yellow(s) color(33, s) end
|
10
|
+
def blue(s) color(34, s) end
|
11
|
+
def magenta(s) color(35, s) end
|
12
|
+
def cyan(s) color(36, s) end
|
13
|
+
def white(s) color(37, s) end
|
14
|
+
|
15
|
+
def success(result) io.print green('.') end
|
16
|
+
def failure(result) io.print red('F') end
|
17
|
+
def exception(result) io.print yellow('E') end
|
18
|
+
|
19
|
+
def failure_header(*) red(super) end
|
20
|
+
def expected_actual(*) white(super) end
|
21
|
+
def backtrace(*) cyan(super) end
|
22
|
+
def exception_header(*) yellow(super) end
|
23
|
+
|
24
|
+
def passed; green(super) end
|
25
|
+
def failed; red(super) end
|
26
|
+
def raised; yellow(super) end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'tst/reporters/plain'
|
2
|
+
require 'tst/reporters/pretty'
|
3
|
+
|
4
|
+
# ### Reporters ###
|
5
|
+
#
|
6
|
+
# Reporters are actually very simple.
|
7
|
+
#
|
8
|
+
# A reporter is just any object that responds to the following
|
9
|
+
# methods: `success`, `failure`, `exception`, `summarize`.
|
10
|
+
#
|
11
|
+
# `success`, `failure`, and `exception` get called as the tests
|
12
|
+
# are running - for real-time reporting.
|
13
|
+
#
|
14
|
+
# When called, they receive a Result instance as an argument, so
|
15
|
+
# you could list the name of each test after it ran.
|
16
|
+
#
|
17
|
+
# After all the tests run, the `summarize` method gets called with
|
18
|
+
# a Results (note the plural) object, which has information about
|
19
|
+
# the the tests that were run.
|
20
|
+
#
|
21
|
+
# Take a look at 'lib/tst.rb' for details about Result and Results.
|
22
|
+
#
|
23
|
+
# And take a look at the "plain" and "pretty" reporters in the
|
24
|
+
# 'lib/tst/reporters/' directory to see a couple of actual
|
25
|
+
# implementations.
|
data/lib/tst.rb
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'tst/reporters'
|
2
|
+
|
3
|
+
# Tst
|
4
|
+
module Tst
|
5
|
+
VERSION = "0.0.1"
|
6
|
+
|
7
|
+
# ### Failure ###
|
8
|
+
#
|
9
|
+
# An error class that carries data about test failures.
|
10
|
+
class Failure < StandardError
|
11
|
+
attr_reader :message, :expected, :actual
|
12
|
+
|
13
|
+
# Takes the `message`, `expected`, and `actual` parameters
|
14
|
+
# and stuffs them into instance variables.
|
15
|
+
def initialize(message, expected, actual)
|
16
|
+
@message = message
|
17
|
+
@expected = expected
|
18
|
+
@actual = actual
|
19
|
+
super(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# ### Assertions ###
|
24
|
+
#
|
25
|
+
# Defines a few `assert` methods for use in tests.
|
26
|
+
#
|
27
|
+
# If you'd like to define your own assertion methods,
|
28
|
+
# open up the module and do so:
|
29
|
+
#
|
30
|
+
# module Tst::Assertions
|
31
|
+
# def assert_seven(actual)
|
32
|
+
# # return without raising to pass
|
33
|
+
# return if actual == 7
|
34
|
+
#
|
35
|
+
# # raise a Tst::Failure to fail
|
36
|
+
# raise Tst::Failure.new('Not 7', 7, actual)
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# Because `Assertions` is included into `Test`, your
|
41
|
+
# new methods will be available in `tst` blocks.
|
42
|
+
module Assertions
|
43
|
+
# *assert*, the lie detector.
|
44
|
+
#
|
45
|
+
# It fails if `value` is `false` or `nil`. Succeeds otherwise.
|
46
|
+
def assert(value)
|
47
|
+
return if value
|
48
|
+
raise Failure.new("Failure: Truthiness", "not false or nil", value)
|
49
|
+
end
|
50
|
+
|
51
|
+
# *assert_equal*, the egalitarian.
|
52
|
+
#
|
53
|
+
# Fails unless it's arguments are equal (with `==`).
|
54
|
+
def assert_equal(expected, actual)
|
55
|
+
return if expected == actual
|
56
|
+
raise Failure.new("Equality Failure", expected, actual)
|
57
|
+
end
|
58
|
+
|
59
|
+
# *assert_raises*, the pessimist.
|
60
|
+
#
|
61
|
+
# Succeeds if it catches an error AND that error is
|
62
|
+
# a `kind_of?` the `expected` error.
|
63
|
+
#
|
64
|
+
# Fails otherwise.
|
65
|
+
def assert_raises(expected=StandardError)
|
66
|
+
begin
|
67
|
+
yield
|
68
|
+
rescue => actual
|
69
|
+
return if actual.kind_of?(expected)
|
70
|
+
raise Failure.new("Failure: Unexpected Exception", expected, actual)
|
71
|
+
end
|
72
|
+
raise Failure.new("Failure: No Exception", expected, "Nothing raised.")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# ### Test ###
|
77
|
+
#
|
78
|
+
# An instance of test is basically only responsible for providing
|
79
|
+
# a scope within which to run a block.
|
80
|
+
#
|
81
|
+
# It includes the Assertions module so that the assertions
|
82
|
+
# methods are available to you inside your tests.
|
83
|
+
class Test
|
84
|
+
include Assertions
|
85
|
+
|
86
|
+
attr_reader :name
|
87
|
+
|
88
|
+
def initialize(name, &block)
|
89
|
+
@name = name
|
90
|
+
@block = block
|
91
|
+
end
|
92
|
+
|
93
|
+
def run
|
94
|
+
instance_eval &@block
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set up some constants to represent test outcomes.
|
99
|
+
SUCCEEDED = :success
|
100
|
+
FAILED = :failure
|
101
|
+
RAISED = :exception
|
102
|
+
|
103
|
+
# ### Result ###
|
104
|
+
#
|
105
|
+
# A tidy little place to store test result information.
|
106
|
+
# The values should be self-explanatory.
|
107
|
+
Result = Struct.new(:name, :status, :elapsed, :exception)
|
108
|
+
|
109
|
+
|
110
|
+
# ### Results ###
|
111
|
+
#
|
112
|
+
# A collection class that keeps track of test results.
|
113
|
+
class Results
|
114
|
+
attr_reader :count, :elapsed, :results
|
115
|
+
|
116
|
+
# It starts out with everything zeroed out.
|
117
|
+
def initialize
|
118
|
+
@count = 0
|
119
|
+
@elapsed = 0
|
120
|
+
@results = []
|
121
|
+
end
|
122
|
+
|
123
|
+
# Collects incoming `result`s and updates `count` and `elapsed`
|
124
|
+
# to reflect the new `result`.
|
125
|
+
def <<(result)
|
126
|
+
@count += 1
|
127
|
+
@elapsed += result.elapsed
|
128
|
+
results << result
|
129
|
+
end
|
130
|
+
|
131
|
+
# Accessors to pull out the subset of tests you need.
|
132
|
+
def successes; results.select { |r| r.status == SUCCEEDED } end
|
133
|
+
def failures; results.select { |r| r.status == FAILED } end
|
134
|
+
def exceptions; results.select { |r| r.status == RAISED } end
|
135
|
+
end
|
136
|
+
|
137
|
+
# ### Runner ###
|
138
|
+
#
|
139
|
+
# Collects and runs your tests.
|
140
|
+
class Runner
|
141
|
+
attr_reader :tests, :results, :reporter
|
142
|
+
|
143
|
+
# A new Runner accepts a Reporter instance, which it delegates
|
144
|
+
# to for displaying results. It defaults to the plain reporter.
|
145
|
+
#
|
146
|
+
# Look at 'lib/tst/reporters.rb' for more information on reporters.
|
147
|
+
def initialize(reporter=nil)
|
148
|
+
@tests = []
|
149
|
+
@results = Results.new
|
150
|
+
@reporter = reporter || Reporters::Plain.new
|
151
|
+
end
|
152
|
+
|
153
|
+
# Collects incoming tests.
|
154
|
+
def <<(test)
|
155
|
+
tests << test
|
156
|
+
end
|
157
|
+
|
158
|
+
# Runs all the tests it has and then hands control
|
159
|
+
# over to `reporter` to tell you about it.
|
160
|
+
def run!
|
161
|
+
tests.each { |test| run_test(test) }
|
162
|
+
reporter.summarize(results)
|
163
|
+
end
|
164
|
+
|
165
|
+
# The actual machinery for running a test.
|
166
|
+
def run_test(test)
|
167
|
+
start = Time.now
|
168
|
+
test.run
|
169
|
+
status = SUCCEEDED
|
170
|
+
rescue Failure => exception
|
171
|
+
status = FAILED
|
172
|
+
rescue StandardError => exception
|
173
|
+
status = RAISED
|
174
|
+
ensure
|
175
|
+
elapsed = Time.now - start
|
176
|
+
record(test.name, status, elapsed, exception)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Adds a result to `results` and calls the `success`,
|
180
|
+
# `failure`, or `exception` method on `reporter` so
|
181
|
+
# it can show you your results as they come in.
|
182
|
+
def record(name, status, elapsed, exception=nil)
|
183
|
+
result = Result.new(name, status, elapsed, exception)
|
184
|
+
results << result
|
185
|
+
reporter.send(status, result)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# ### FileRunner ###
|
190
|
+
#
|
191
|
+
# A little wrapper class (around a Runner) to allow
|
192
|
+
# for easily running tests that are in files.
|
193
|
+
class FileRunner
|
194
|
+
attr_reader :runner
|
195
|
+
|
196
|
+
# Runs the tests in the files represented by `glob`.
|
197
|
+
#
|
198
|
+
# `glob`: is any string that `Dir.glob` knows how to handle.
|
199
|
+
#
|
200
|
+
# Accepts the following options:
|
201
|
+
# `:reporter`: any object that responds_to the reporter
|
202
|
+
# interface
|
203
|
+
# (see 'lib/tst/reporters.rb' for details)
|
204
|
+
#
|
205
|
+
# `:load_paths`: an Array of directories to be added to
|
206
|
+
# the load path before running the tests.
|
207
|
+
#
|
208
|
+
# Note that `run` makes a new Runner each time it's called,
|
209
|
+
# allowing it to be called several times on the same
|
210
|
+
# FileRunner instance while still doing the right thing.
|
211
|
+
def run(glob, options={})
|
212
|
+
@runner = Runner.new options[:reporter]
|
213
|
+
load_files(glob, options[:load_paths] || [])
|
214
|
+
runner.run!
|
215
|
+
end
|
216
|
+
|
217
|
+
# Collects incoming tests and hands them off to the `runner`.
|
218
|
+
def <<(test)
|
219
|
+
runner << test
|
220
|
+
end
|
221
|
+
|
222
|
+
# Modifies the load path if necessary and loads all the
|
223
|
+
# test files.
|
224
|
+
def load_files(glob, load_paths)
|
225
|
+
load_paths.each { |path| $:.unshift(path) }
|
226
|
+
Dir[glob].each { |file| load(file, true) }
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Module-level accessor to a FileRunner instance.
|
231
|
+
def self.file_runner
|
232
|
+
@file_runner ||= FileRunner.new
|
233
|
+
end
|
234
|
+
|
235
|
+
# Hands a test off to `file_runner`. This is called by
|
236
|
+
# the top-level `tst` method (i.e., the one you use to
|
237
|
+
# write your tests with).
|
238
|
+
def self.add_test(name, &block)
|
239
|
+
file_runner << Test.new(name, &block)
|
240
|
+
end
|
241
|
+
|
242
|
+
# The module-level API entry point.
|
243
|
+
#
|
244
|
+
# An example from Tst's own Rakefile:
|
245
|
+
#
|
246
|
+
# task :test do
|
247
|
+
# Tst.run 'test/*.rb',
|
248
|
+
# :load_paths => ['.', 'lib'],
|
249
|
+
# :reporter => Tst::Reporters::Pretty.new
|
250
|
+
# end
|
251
|
+
def self.run(glob, options={})
|
252
|
+
file_runner.run(glob, options)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# The `tst` method.
|
257
|
+
#
|
258
|
+
# Acts as proxy to `Tst.add_test`. This is the method you
|
259
|
+
# actually call as a consumer of the library.
|
260
|
+
#
|
261
|
+
# tst "the answer is correct" do
|
262
|
+
# answer = Answer.new
|
263
|
+
# assert_equal 42, answer
|
264
|
+
# end
|
265
|
+
def tst(name=nil, &block)
|
266
|
+
Tst.add_test(name, &block)
|
267
|
+
end
|
data/test/assertions.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# :assert
|
2
|
+
|
3
|
+
tst "assert(nil) raises" do
|
4
|
+
assert_raises Tst::Failure do
|
5
|
+
assert nil
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
tst "assert(false) raises" do
|
10
|
+
assert_raises Tst::Failure do
|
11
|
+
assert false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
tst "assert(true) does NOT raise" do
|
16
|
+
assert true
|
17
|
+
end
|
18
|
+
|
19
|
+
tst "assert(0) does NOT raise" do
|
20
|
+
assert 0
|
21
|
+
end
|
22
|
+
|
23
|
+
tst "assert('') does NOT raise" do
|
24
|
+
assert 'asdf'
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# :assert_equal
|
29
|
+
|
30
|
+
tst "assert_equal raises if things are NOT ==" do
|
31
|
+
assert_raises Tst::Failure do
|
32
|
+
assert_equal "Arthur Dent", "Ford Prefect"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
tst "assert_equal does NOT raise if things are ==" do
|
37
|
+
assert_equal 42, 42
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# :assert_raises
|
42
|
+
|
43
|
+
tst "assert_raises raises if its block does NOT raise something" do
|
44
|
+
assert_raises Tst::Failure do
|
45
|
+
assert_raises { }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
tst "assert_raises raises if its block doesn't raise the right thing" do
|
50
|
+
assert_raises Tst::Failure do
|
51
|
+
assert_raises(NameError) { raise RuntimeError }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
tst "assert_raises does NOT raise if it gets the expected error" do
|
56
|
+
assert_raises(NameError) { raise NameError }
|
57
|
+
end
|
58
|
+
|
59
|
+
tst "assert_raises does NOT raise on expected error subclass" do
|
60
|
+
assert_raises(ArgumentError) { raise Class.new(ArgumentError) }
|
61
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
def tst_runner_output(glob, expected)
|
4
|
+
tst "running #{glob}" do
|
5
|
+
io = StringIO.new('')
|
6
|
+
reporter = Tst::Reporters::Plain.new(io)
|
7
|
+
Tst.run glob, :reporter => reporter
|
8
|
+
assert_equal expected, io.string.gsub(/0\.\d+/, 'X.XXX')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
tst_runner_output "test/fixtures/success.rb", <<-end
|
13
|
+
.
|
14
|
+
Ran 1 tests in X.XXXs: 1 passed, 0 failed, 0 raised.
|
15
|
+
end
|
16
|
+
|
17
|
+
tst_runner_output "test/fixtures/failure.rb", <<-end
|
18
|
+
F
|
19
|
+
F1: Failure: Truthiness - \"one failing test\"
|
20
|
+
exp: \"not false or nil\"
|
21
|
+
got: false
|
22
|
+
* test/fixtures/failure.rb:2:in `block in <top (required)>'
|
23
|
+
* test/plain_reporter.rb:7:in `block in tst_runner_output'
|
24
|
+
|
25
|
+
Ran 1 tests in X.XXXs: 0 passed, 1 failed, 0 raised.
|
26
|
+
end
|
27
|
+
|
28
|
+
tst_runner_output "test/fixtures/exception.rb", <<-end
|
29
|
+
E
|
30
|
+
E1: #<RuntimeError: I'm a teapot> - \"one 'exceptional' test\"
|
31
|
+
* test/fixtures/exception.rb:2:in `block in <top (required)>'
|
32
|
+
* test/plain_reporter.rb:7:in `block in tst_runner_output'
|
33
|
+
|
34
|
+
Ran 1 tests in X.XXXs: 0 passed, 0 failed, 1 raised.
|
35
|
+
end
|
36
|
+
|
37
|
+
tst_runner_output "test/fixtures/{exception,success,failure}.rb", <<-end
|
38
|
+
E.F
|
39
|
+
F1: Failure: Truthiness - \"one failing test\"
|
40
|
+
exp: \"not false or nil\"
|
41
|
+
got: false
|
42
|
+
* test/fixtures/failure.rb:2:in `block in <top (required)>'
|
43
|
+
* test/plain_reporter.rb:7:in `block in tst_runner_output'
|
44
|
+
|
45
|
+
E1: #<RuntimeError: I'm a teapot> - \"one 'exceptional' test\"
|
46
|
+
* test/fixtures/exception.rb:2:in `block in <top (required)>'
|
47
|
+
* test/plain_reporter.rb:7:in `block in tst_runner_output'
|
48
|
+
|
49
|
+
Ran 3 tests in X.XXXs: 1 passed, 1 failed, 1 raised.
|
50
|
+
end
|
51
|
+
|
52
|
+
tst_runner_output "test/fixtures/does_not_exist.rb", <<-end
|
53
|
+
|
54
|
+
Ran 0 tests in X.XXXs: 0 passed, 0 failed, 0 raised.
|
55
|
+
end
|
56
|
+
|
data/test/test.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# One nice thing is that each test gets a fresh scope,
|
2
|
+
# which helps mitigate state leakage...
|
3
|
+
tst "tests don't have access to top-level instance variables" do
|
4
|
+
@outside = "I'm outside"
|
5
|
+
|
6
|
+
Tst::Test.new "instance variable" do
|
7
|
+
@outside = "I'm inside, muahahaha!"
|
8
|
+
end.run
|
9
|
+
|
10
|
+
assert_equal "I'm outside", @outside
|
11
|
+
end
|
12
|
+
|
13
|
+
# ... but ruby block scope can be a bitch.
|
14
|
+
tst "for better or for worse, local variables are fair game" do
|
15
|
+
outside = "I'm outside"
|
16
|
+
|
17
|
+
Tst::Test.new "local variable" do
|
18
|
+
outside = "I'm inside, muahahaha!"
|
19
|
+
end.run
|
20
|
+
|
21
|
+
assert_equal "I'm inside, muahahaha!", outside
|
22
|
+
end
|
23
|
+
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tst
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Arun Srinivasan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-26 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: A small testing library that tries not to do too much.
|
15
|
+
email: satchmorun@gmail.com
|
16
|
+
executables:
|
17
|
+
- tst
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- README.md
|
22
|
+
- LICENSE
|
23
|
+
- Rakefile
|
24
|
+
- bin/tst
|
25
|
+
- lib/tst/reporters/plain.rb
|
26
|
+
- lib/tst/reporters/pretty.rb
|
27
|
+
- lib/tst/reporters.rb
|
28
|
+
- lib/tst.rb
|
29
|
+
- test/assertions.rb
|
30
|
+
- test/fixtures/exception.rb
|
31
|
+
- test/fixtures/failure.rb
|
32
|
+
- test/fixtures/success.rb
|
33
|
+
- test/plain_reporter.rb
|
34
|
+
- test/test.rb
|
35
|
+
homepage: http://github.com/satchmorun/tst
|
36
|
+
licenses:
|
37
|
+
- MIT
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.10
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: A small testing library that tries not to do too much.
|
60
|
+
test_files:
|
61
|
+
- test/assertions.rb
|
62
|
+
- test/fixtures/exception.rb
|
63
|
+
- test/fixtures/failure.rb
|
64
|
+
- test/fixtures/success.rb
|
65
|
+
- test/plain_reporter.rb
|
66
|
+
- test/test.rb
|