tst 0.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/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
|