speccify 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +269 -0
- data/lib/speccify.rb +201 -0
- metadata +54 -0
data/README.markdown
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
# Speccify
|
2
|
+
|
3
|
+
## A lightweight alternative to RSpec.
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
## Features:
|
8
|
+
|
9
|
+
### Nested Contexts
|
10
|
+
|
11
|
+
### Custom matchers are no-brainers with def_matcher
|
12
|
+
|
13
|
+
### Blazing Fast
|
14
|
+
|
15
|
+
### 100% Ruby 1.9 and 1.8 compatible
|
16
|
+
|
17
|
+
### Rails out of the box!
|
18
|
+
|
19
|
+
### Sophisticated Expectations/Matchers System
|
20
|
+
|
21
|
+
### < 300 LOC
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
## Install Speccify:
|
26
|
+
|
27
|
+
> sudo gem install speccify
|
28
|
+
|
29
|
+
## Using Speccify
|
30
|
+
|
31
|
+
Create a testfile:
|
32
|
+
|
33
|
+
# test_object.rb
|
34
|
+
|
35
|
+
require "rubygems"
|
36
|
+
require "speccify"
|
37
|
+
|
38
|
+
describe Object do
|
39
|
+
before do
|
40
|
+
@obj = Object.new
|
41
|
+
end
|
42
|
+
|
43
|
+
it "is not nil" do
|
44
|
+
@obj.should_not be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can be frozen" do
|
48
|
+
@obj.freeze
|
49
|
+
@obj.should be_frozen
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Run it with the ruby command:
|
54
|
+
|
55
|
+
> $ ruby test_object.rb
|
56
|
+
>Loaded suite -
|
57
|
+
>Started
|
58
|
+
>..
|
59
|
+
>Finished in 0.001820 seconds.
|
60
|
+
>
|
61
|
+
>2 tests, 2 assertions, 0 failures, 0 errors
|
62
|
+
|
63
|
+
|
64
|
+
## Using Speccify with autotest
|
65
|
+
|
66
|
+
1. Name your testfiles test_whatever.rb
|
67
|
+
2. Start autotest
|
68
|
+
3. There is no step three ...
|
69
|
+
|
70
|
+
## Using Speccify with Rails
|
71
|
+
|
72
|
+
Use the default rails test directory/structure, testhelpers, assertions and infrastructure.
|
73
|
+
|
74
|
+
1. sudo use_minitest yes
|
75
|
+
2. require 'speccify' in test_helper.rb
|
76
|
+
3. There is no step three ...
|
77
|
+
|
78
|
+
Tell Speccify what kind of test he is, by passing one of the following options to the describe method:
|
79
|
+
|
80
|
+
* :type => ActiveSupport::TestCase
|
81
|
+
* :type => ActionController::TestCase
|
82
|
+
* :type => ActionMailer::TestCase
|
83
|
+
* :type => ActionView::TestCase
|
84
|
+
|
85
|
+
### Example, Functional Test:
|
86
|
+
|
87
|
+
describe HorstsController, :type => ActionController::TestCase do
|
88
|
+
it "should get index" do
|
89
|
+
get :index
|
90
|
+
@response.should be_success
|
91
|
+
assigns(:horsts).should_not be_nil
|
92
|
+
end
|
93
|
+
describe "Whatever" do
|
94
|
+
# I'm still a ActionController::TestCase
|
95
|
+
it "should get index" do
|
96
|
+
get :index
|
97
|
+
@response.should be_success
|
98
|
+
assigns(:horsts).should_not be_nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
I'm not sure if I should wrap all the rails assertions in matchers.
|
104
|
+
Using the default rails assertions works fine.
|
105
|
+
|
106
|
+
|
107
|
+
## Built in matchers
|
108
|
+
|
109
|
+
* be_something, for any arbitrary something:
|
110
|
+
|
111
|
+
@obj.should be_something
|
112
|
+
# passes if @obj.something? is true
|
113
|
+
|
114
|
+
|
115
|
+
* have(n).somethings, for any arbitrary something:
|
116
|
+
|
117
|
+
@obj.should have(3).somethings
|
118
|
+
# passes if @obj.somethings.length == 3
|
119
|
+
|
120
|
+
* change {something}
|
121
|
+
|
122
|
+
lambda {@var+=1}.should change {@var}
|
123
|
+
# passes
|
124
|
+
lambda { }.should change {@var}
|
125
|
+
# fails
|
126
|
+
@var = 1
|
127
|
+
lambda {@var+=1}.should change {@var}.from(1).to(2)
|
128
|
+
# passes
|
129
|
+
|
130
|
+
* more
|
131
|
+
|
132
|
+
## DEF_MATCHER
|
133
|
+
|
134
|
+
### A simple example first:
|
135
|
+
|
136
|
+
def_matcher :be_nil do |given, matcher, args|
|
137
|
+
given.nil?
|
138
|
+
end
|
139
|
+
nil.should be_nil
|
140
|
+
|
141
|
+
|
142
|
+
The `def_matcher` method is really simple to use.
|
143
|
+
You just provide it the name of your matcher and attach a block that defines it's behavior.
|
144
|
+
The return value of the block is a boolean that
|
145
|
+
actually will be expected (should) or not expected (`should_not`).
|
146
|
+
|
147
|
+
There are three arguments available inside the matcher block:
|
148
|
+
|
149
|
+
### given
|
150
|
+
|
151
|
+
This is the object that has received the should or `should_not`.
|
152
|
+
|
153
|
+
### matcher
|
154
|
+
|
155
|
+
This is the matcher object. You can set the failure messages as attributes on this object:
|
156
|
+
|
157
|
+
def_matcher :matcher_name do |given, matcher, args|
|
158
|
+
matcher.positive_msg = "You can see me if I am applied to should and I return a false value"
|
159
|
+
matcher.negative_msg = "You can see me if I am applied to should_not and I return a true value"
|
160
|
+
end
|
161
|
+
|
162
|
+
It holds a list of all methods that have been called on the matcher (for chaining):
|
163
|
+
|
164
|
+
obj.should matcher_name.some_method(4,5,6) {"and a block"}.second
|
165
|
+
def_matcher :matcher_name do |given, matcher, args|
|
166
|
+
# this is an ostruct that holds all information about the first method 'some_method'
|
167
|
+
matcher.msgs[0]
|
168
|
+
# this is an ostruct that holds all information about the second method 'second'
|
169
|
+
matcher.msgs[1]
|
170
|
+
# this is the name of the first method:
|
171
|
+
matcher.msgs[0].name #=> :some_method
|
172
|
+
# this is a list of arguments that have been passed to the first method:
|
173
|
+
matcher.msgs[0].args #=> [4,5,6]
|
174
|
+
# this is the block that was attached:
|
175
|
+
matcher.msgs[0].block #=> proc {"and a block"}
|
176
|
+
end
|
177
|
+
|
178
|
+
If there is a failure, it knows where:
|
179
|
+
|
180
|
+
def_matcher :matcher_name do |given, matcher, args|
|
181
|
+
matcher.loc #=> "./some/where.rb:55 ... "
|
182
|
+
end
|
183
|
+
|
184
|
+
### args
|
185
|
+
This is a list of all arguments that have been applied to the matcher. Like the 6 in:
|
186
|
+
|
187
|
+
(3*3).should_not be(6)
|
188
|
+
|
189
|
+
## More def_matcher examples
|
190
|
+
|
191
|
+
### A little more complex:
|
192
|
+
|
193
|
+
def_matcher :be_in_range do |given, matcher, args|
|
194
|
+
range = args[1] ? (args[0]..args[1]) : args[0]
|
195
|
+
matcher.positive_msg = "expected #{given} to be in range (#{range})"
|
196
|
+
matcher.negative_msg = "expected #{given} not to be in range (#{range})"
|
197
|
+
range.include?(given)
|
198
|
+
end
|
199
|
+
2.should be_in_range(1,3)
|
200
|
+
"m".should be_in_range("a".."z")
|
201
|
+
|
202
|
+
### Matchers may receive messages:
|
203
|
+
|
204
|
+
def_matcher :have do |given, matcher, args|
|
205
|
+
number = args[0]
|
206
|
+
actual = given.send(matcher.msgs[0].name).length
|
207
|
+
matcher.positive_msg = "Expected #{given} to have #{actual}, but found #{number} "
|
208
|
+
actual == number
|
209
|
+
end
|
210
|
+
class Thing
|
211
|
+
def widgets
|
212
|
+
@widgets ||= []
|
213
|
+
end
|
214
|
+
end
|
215
|
+
@thing.should have(3).widgets
|
216
|
+
|
217
|
+
|
218
|
+
## Speccify vs. RSpec
|
219
|
+
|
220
|
+
### Speccify provides the parts of RSpec that actually matter (to me):
|
221
|
+
|
222
|
+
* **The familiar describe/it syntax**
|
223
|
+
* **nested contexts**
|
224
|
+
* **should/should_not**
|
225
|
+
* **matchers and custom matchers**
|
226
|
+
|
227
|
+
### Things that actually doesn't matter (to me):
|
228
|
+
|
229
|
+
* **/spec directory instead of /test**
|
230
|
+
* **`whatever_spec.rb` instead of `test_whatever.rb`**
|
231
|
+
* **custom formatters**
|
232
|
+
* **custom runners**
|
233
|
+
* **html output**
|
234
|
+
* **special commandline tools**
|
235
|
+
* **shared example groups**
|
236
|
+
* **pending examples**
|
237
|
+
* **`before_all, after_all, before_suite, after_suite`**
|
238
|
+
|
239
|
+
|
240
|
+
## Contribution
|
241
|
+
|
242
|
+
* Idea?, Feature Request?, Bug? -> [Lighthouse](http://300.lighthouseapp.com/projects/24443/home)
|
243
|
+
* source -> [GitHub](http://github.com/mhennemeyer/speccify)
|
244
|
+
* talk? -> [GoogleGroup](http://groups.google.de/group/speccify)
|
245
|
+
|
246
|
+
## License
|
247
|
+
|
248
|
+
(The MIT License)
|
249
|
+
|
250
|
+
Copyright (c) Matthias Hennemeyer
|
251
|
+
|
252
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
253
|
+
a copy of this software and associated documentation files (the
|
254
|
+
'Software'), to deal in the Software without restriction, including
|
255
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
256
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
257
|
+
permit persons to whom the Software is furnished to do so, subject to
|
258
|
+
the following conditions:
|
259
|
+
|
260
|
+
The above copyright notice and this permission notice shall be
|
261
|
+
included in all copies or substantial portions of the Software.
|
262
|
+
|
263
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
264
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
265
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
266
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
267
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
268
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
269
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/speccify.rb
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
|
4
|
+
unless defined?(MiniTest)
|
5
|
+
Test::Unit::TestCase.class_eval do
|
6
|
+
def default_test
|
7
|
+
instance_eval { @_result.instance_eval { @run_count ||= 0; @run_count -= 1} if defined?(@_result)}
|
8
|
+
end
|
9
|
+
end # Test::Unit::TestCase
|
10
|
+
end
|
11
|
+
|
12
|
+
module Speccify
|
13
|
+
module ExampleGroupClassMethods
|
14
|
+
attr_accessor :desc, :setup_chained, :teardown_chained
|
15
|
+
@desc ||= ""
|
16
|
+
|
17
|
+
def setup_chained
|
18
|
+
@setup_chained || lambda {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def teardown_chained
|
22
|
+
@teardown_chained || lambda {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def before(type = :each, &block)
|
26
|
+
raise "unsupported before type: #{type}" unless type == :each
|
27
|
+
passed_through_setup = self.setup_chained
|
28
|
+
self.setup_chained = lambda { instance_eval(&passed_through_setup);instance_eval(&block) }
|
29
|
+
define_method :setup, &self.setup_chained
|
30
|
+
end
|
31
|
+
|
32
|
+
def after(type = :each, &block)
|
33
|
+
raise "unsupported after type: #{type}" unless type == :each
|
34
|
+
passed_through_teardown = self.teardown_chained
|
35
|
+
self.teardown_chained = lambda {instance_eval(&block);instance_eval(&passed_through_teardown) }
|
36
|
+
define_method :teardown, &self.teardown_chained
|
37
|
+
end
|
38
|
+
|
39
|
+
def describe desc, &block
|
40
|
+
cls = Class.new(self.superclass)
|
41
|
+
Object.const_set self.name + desc.to_s.split(/\W+/).map { |s| s.capitalize }.join, cls
|
42
|
+
cls.setup_chained = self.setup_chained
|
43
|
+
cls.teardown_chained = self.teardown_chained
|
44
|
+
cls.desc = self.desc + " " + desc
|
45
|
+
cls.tests($1.constantize) if defined?(Rails) && self.name =~ /^(.*Controller)Test/
|
46
|
+
cls.class_eval(&block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def it desc, &block
|
50
|
+
self.before {}
|
51
|
+
define_method "test_#{desc.gsub(/\W+/, '_').downcase}", &lambda {$current_spec = self; instance_eval(&block)} if block_given?
|
52
|
+
self.after {}
|
53
|
+
end
|
54
|
+
end # ExampleGroupClassMethods
|
55
|
+
|
56
|
+
module ExampleGroupMethods
|
57
|
+
def method_missing(name, *args, &block)
|
58
|
+
if (name.to_s =~ /^be_(.+)/)
|
59
|
+
Speccify::Functions::build_matcher(name, args) do |given, matcher, args|
|
60
|
+
given.send(($1 + "?").to_sym)
|
61
|
+
end
|
62
|
+
else
|
63
|
+
raise NoMethodError.new(name.to_s)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end # ExampleGroupMethods
|
67
|
+
|
68
|
+
module Functions
|
69
|
+
def self.determine_class_name(name)
|
70
|
+
name.to_s.split(/\W+/).map { |s| s[0..0].upcase + s[1..-1] }.join
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.build_matcher(matcher_name, args, &block)
|
74
|
+
match_block = lambda do |actual, matcher|
|
75
|
+
block.call(actual, matcher, args)
|
76
|
+
end
|
77
|
+
body = lambda do |klass|
|
78
|
+
@matcher_name = matcher_name.to_s
|
79
|
+
def self.matcher_name
|
80
|
+
@matcher_name
|
81
|
+
end
|
82
|
+
|
83
|
+
attr_accessor :positive_msg, :negative_msg, :msgs, :loc
|
84
|
+
def initialize match_block
|
85
|
+
@match_block = match_block
|
86
|
+
end
|
87
|
+
|
88
|
+
def method_missing id, *args, &block
|
89
|
+
require 'ostruct'
|
90
|
+
(self.msgs ||= []) << OpenStruct.new( "name" => id, "args" => args, "block" => block )
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def matches? given
|
95
|
+
@positive_msg ||= Speccify::Functions::message("#{given} should #{self.class.matcher_name}", "no match", self.class.matcher_name , self.loc || "")
|
96
|
+
@negative_msg ||= Speccify::Functions::message("#{given} should not #{self.class.matcher_name}", "match", self.class.matcher_name , self.loc || "")
|
97
|
+
@match_block.call(given, self)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
Class.new(&body).new(match_block)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.message(expected, actual, op, location)
|
104
|
+
"""
|
105
|
+
Expected: #{expected.to_s}
|
106
|
+
got: #{actual.to_s}
|
107
|
+
comparison with: #{op.to_s}
|
108
|
+
location: (#{location})
|
109
|
+
"""
|
110
|
+
end
|
111
|
+
end # Functions
|
112
|
+
|
113
|
+
class OperatorMatcherProxy
|
114
|
+
def self.create given, loc, type = true
|
115
|
+
body = lambda do |klass|
|
116
|
+
define_method(:initialize) do |given|
|
117
|
+
@given = given
|
118
|
+
end
|
119
|
+
|
120
|
+
['==', '===', '=~', '>', '>=', '<', '<='].each do |operator|
|
121
|
+
define_method(operator) do |actual|
|
122
|
+
print_given = (@given == nil) ? "nil" : @given
|
123
|
+
print_actual = (type ? "" : "not ") + (actual.nil? ? "nil" : actual.to_s)
|
124
|
+
msg = Speccify::Functions::message(print_actual, print_given, operator, loc)
|
125
|
+
$current_spec.assert(type == @given.send(operator,actual), msg)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
return Class.new(&body).new(given)
|
131
|
+
end
|
132
|
+
end # OperatorMatcherProxy
|
133
|
+
|
134
|
+
Object.class_eval do
|
135
|
+
def should matcher = nil
|
136
|
+
if matcher
|
137
|
+
matcher.loc = caller[0]
|
138
|
+
$current_spec.assert(matcher.matches?(self), matcher.positive_msg)
|
139
|
+
else
|
140
|
+
OperatorMatcherProxy.create(self,caller[0])
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def should_not matcher = nil
|
145
|
+
if matcher
|
146
|
+
matcher.loc = caller[0]
|
147
|
+
$current_spec.assert(!matcher.matches?(self), matcher.negative_msg)
|
148
|
+
else
|
149
|
+
OperatorMatcherProxy.create(self,caller[0], false)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end # Object
|
153
|
+
|
154
|
+
module Extension
|
155
|
+
def def_matcher(matcher_name, &block)
|
156
|
+
self.class.send :define_method, matcher_name do |*args|
|
157
|
+
Speccify::Functions::build_matcher(matcher_name, args, &block)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end # Extension
|
161
|
+
|
162
|
+
module ::Kernel
|
163
|
+
def describe *args, &block
|
164
|
+
super_class = (Hash === args.last && (args.last[:type] || args.last[:testcase])) || Test::Unit::TestCase
|
165
|
+
super_class.class_eval {extend ExampleGroupClassMethods; include ExampleGroupMethods}
|
166
|
+
cls = Class.new(super_class)
|
167
|
+
cnst, desc = args
|
168
|
+
Object.const_set Speccify::Functions::determine_class_name(cnst.to_s + "Test"), cls
|
169
|
+
cls.desc = String === desc ? desc : cnst.to_s
|
170
|
+
cls.class_eval(&block)
|
171
|
+
end
|
172
|
+
private :describe
|
173
|
+
end # Kernel
|
174
|
+
end # Speccify
|
175
|
+
include Speccify::Extension
|
176
|
+
|
177
|
+
# A few matchers:
|
178
|
+
def_matcher :be do |given, matcher, args|
|
179
|
+
given == args[0]
|
180
|
+
end
|
181
|
+
|
182
|
+
def change(&block)
|
183
|
+
Speccify::Functions::build_matcher(:change, []) do |given, matcher, args|
|
184
|
+
before = block.call
|
185
|
+
given.call
|
186
|
+
after = block.call
|
187
|
+
comparison = after != before
|
188
|
+
if list = matcher.msgs
|
189
|
+
comparison = case list[0].name
|
190
|
+
# todo provide meaningful messages
|
191
|
+
when :by then (after == before + list[0].args[0] || after == before - list[0].args[0])
|
192
|
+
when :by_at_least then (after >= before + list[0].args[0] || after <= before - list[0].args[0])
|
193
|
+
when :by_at_most then (after <= before + list[0].args[0] && after >= before - list[0].args[0])
|
194
|
+
when :from then (before == list[0].args[0]) && (after == list[1].args[0])
|
195
|
+
end
|
196
|
+
end
|
197
|
+
matcher.positive_msg = "given block didn't alter the block attached to change, #{matcher.loc}"
|
198
|
+
matcher.negative_msg = "given block did alter the block attached to change, #{matcher.loc}"
|
199
|
+
comparison
|
200
|
+
end
|
201
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: speccify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthias Hennemeyer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-01 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Speccify is a lightweight alternative to RSpec.
|
17
|
+
email: mhennemeyer@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- README.markdown
|
26
|
+
- lib/speccify.rb
|
27
|
+
has_rdoc: false
|
28
|
+
homepage: http://github.com/mhennemeyer/speccify
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: "0"
|
39
|
+
version:
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
requirements: []
|
47
|
+
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.3.1
|
50
|
+
signing_key:
|
51
|
+
specification_version: 2
|
52
|
+
summary: Speccify is a lightweight alternative to RSpec.
|
53
|
+
test_files: []
|
54
|
+
|