speccify 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.markdown +269 -0
  2. data/lib/speccify.rb +201 -0
  3. 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
+