kintama 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,247 @@
1
+ Hello
2
+ =====
3
+
4
+ This is a tool for testing code. Or maybe it's a tool for exploring ways to test code. See below.
5
+
6
+ Huh? Really? Another one?
7
+ ====
8
+
9
+ ... Yeah, I know. To be honest, I'm not 100% sure why I'm doing this. Here are some guesses though:
10
+
11
+ My testing tools of choice, at the moment, are [Test::Unit][] with [shoulda][] to provide nested contexts, but not really it's macros.
12
+
13
+ I'm not a huge fan of [Test::Unit][]. Whenever I've tried to extend its behaviour I've hit snags, and found its code difficult to understand (particularly as lots of it don't seem to be regularly used - I'm looking at you, [TkRunner][] and friends). I also don't really love [RSpec][], but I think that's just a personal preference (I learned with test/unit, and I didn't want to relearn all of the matcher stuff).
14
+
15
+ I like [shoulda][], because like [RSpec][], it lets me nest groups of tests in ways that help remove duplication in setups. However, [I don't have a lot of confidence that shoulda is going to stick around in its current, useful-for-stuff-that-isnt-RSpec form](http://robots.thoughtbot.com/post/701863189/shoulda-rails3-and-beyond).
16
+
17
+ I like some of the more verbose output that [Cucumber][] and [RSpec][] produce, but as I mentioned above, I don't care for [RSpec][]'s matcher-heavy syntax. It's basically impossible to reproduce that output on anything that uses [Test::Unit][] as a base (see [MonkeySpecDoc][] for an example, which fails because it cannot support any more than one level of nesting)
18
+
19
+ I also like things like [`before(:all)`][before_all], and [`fast_context`][fast_context], but don't like having to hack around inside [Test::Unit][] to implement them (I already have with [`test_startup`][test_startup]; it works but who knows for how long).
20
+
21
+
22
+ Related work
23
+ ------------
24
+
25
+ In the spirit of [shoulda][], a small library called [context][] adds the simple nested context structures to [Test::Unit][], but that's the problem - we can't build anything on top of [Test::Unit][].
26
+
27
+ Ditto for [contest][].
28
+
29
+ Probably the closest thing I've seen is [baretest][]. If you look around the code, some of the implementation details are quite similar to those that have evolved in this code (context-ish objects with parents). However, in many ways baretest is more complex, and the final API that it provides is quite foreign compared to [shoulda][].
30
+
31
+ Another alternative test framework is [riot][], which claims to be fast, but also appears to constrain the way that tests are written by avoiding instance variables in setups, for example.
32
+
33
+ [Testy][] is interesting - it looks like its output is YAML!. [Tryouts][] is thinking outside the box, using comment examples.
34
+
35
+ [Zebra][] addresses the apparent duplication of the test name and the test body, but does it by introducing an [RSpec][]-esque method on every object. Wild. Also, it's an extension of [Test::Unit][], so that's strike two for me, personally.
36
+
37
+ I have no idea what to make of [Shindo][].
38
+
39
+ [Exemplor][]... oh my god why am I contributing to this mess.
40
+
41
+ Erm.
42
+
43
+ Exploring future testing
44
+ ------------------------
45
+
46
+ I wanted to explore how easy it would be to reproduce a test framework with a modern, [shoulda][]/RSpec-esque syntax, but that was simple enough to be understandable when anyone needed to change it.
47
+
48
+ I also wanted to be able to start exploring different ways of expressing test behaviour, outside of the classic `setup -> test -> teardown` cycle, but didn't feel that I could use test/unit as a basis for this kind of speculative work without entering a world of pain.
49
+
50
+ Hence... _this_.
51
+
52
+
53
+ Examples
54
+ ========
55
+
56
+ These will all be very familiar to most people who are already users of [shoulda][]:
57
+
58
+ require 'kintama'
59
+
60
+ context "A thing" do
61
+ setup do
62
+ @thing = Thing.new
63
+ end
64
+ should "act like a thing" do
65
+ assert_equal "thingish", @thing.nature
66
+ end
67
+ end
68
+
69
+ Simple, right? Note that we don't need an outer subclass of `Test::Unit::TestCase`; it's nice to lose that noise, but otherwise so far so same-old-same-old. That's kind-of the point. Anyway, here's what you get when you run this:
70
+
71
+ A thing
72
+ should act like a thing: F
73
+
74
+ 1 tests, 1 failures
75
+
76
+ 1) A thing should act like a thing:
77
+ uninitialized constant Thing (at ./examples/simple.rb:6)
78
+
79
+ Firstly, it's formatted nicely. There are no cryptic line numbers or `bind` references like [shoulda][]. If you run it from a terminal, you'll get colour output too. That's nice.
80
+
81
+
82
+ Aliases
83
+ ----
84
+
85
+ There are a bunch of aliases you can use in various ways. If you don't like:
86
+
87
+ context "A thing" do
88
+
89
+ you could also write:
90
+
91
+ describe Thing do # like RSpec! ...
92
+ given "a thing" do # ...
93
+ testcase "a thing" do # ...
94
+
95
+ It's trivial to define other aliases that might make your tests more readable. Similarly for defining the tests themselves, instead of:
96
+
97
+ should "act like a thing" do
98
+
99
+ you might prefer:
100
+
101
+ it "should act like a thing" do # ...
102
+ test "acts like a thing" do # ...
103
+
104
+ Sometimes just having that flexibility makes all the difference.
105
+
106
+
107
+ Setup, teardown, nested contexts
108
+ --------------
109
+
110
+ These work as you'd expect based on shoulda or RSpec:
111
+
112
+ given "a Thing" do
113
+ setup do
114
+ @thing = Thing.new
115
+ end
116
+
117
+ it "should be happy" do
118
+ assert @thing.happy?
119
+ end
120
+
121
+ context "that is prodded" do
122
+ setup do
123
+ @thing.prod!
124
+ end
125
+
126
+ should "not be happy" do
127
+ assert_false @thing.happy?
128
+ end
129
+ end
130
+
131
+ teardown do
132
+ @thing.cleanup_or_something
133
+ end
134
+ end
135
+
136
+ You can also add (several) global `setup` and `teardown` blocks, which will be run before (or after) every test. For example:
137
+
138
+ Kintama.setup do
139
+ @app = ThingApp.new
140
+ end
141
+
142
+ given "a request" do
143
+ it "should work" do
144
+ assert_equal 200, @app.response.status
145
+ end
146
+ end
147
+
148
+
149
+ Helpers
150
+ -------
151
+
152
+ If you want to make methods available in your tests, you have a few options. You can define them inline:
153
+
154
+ context "my face" do
155
+ should "be awesome" do
156
+ assert_equal "awesome", create_face.status
157
+ end
158
+
159
+ helpers do
160
+ def create_face
161
+ Face.new(:name => "james", :eyes => "blue", :something => "something else")
162
+ end
163
+ end
164
+ end
165
+
166
+ Ideally I would've liked to make this syntatically similar to defining a private method in a class, but for various reasons that was not possible. Anyway, your other options are including a module:
167
+
168
+ module FaceHelper
169
+ def create_face
170
+ # etc ...
171
+ end
172
+ end
173
+
174
+ context "my face" do
175
+ include FaceHelper
176
+ should "be awesome" do
177
+ assert_equal "awesome", create_face.status
178
+ end
179
+ end
180
+
181
+ Or, if you're going to use the method in all your tests, you can add the module globally:
182
+
183
+ Kintama.add FaceHelper
184
+
185
+ or just define the method globally:
186
+
187
+ Kintama.add do
188
+ def create_face
189
+ # etc ...
190
+ end
191
+ end
192
+
193
+ ### Aside: what happens if you do define a method in the context?
194
+
195
+ It becomes available within context (and subcontext) definitions. Here's an example:
196
+
197
+ context "blah" do
198
+ def generate_tests_for(thing)
199
+ it "should work with #{thing}" do
200
+ assert thing.works
201
+ end
202
+ end
203
+
204
+ [Monkey.new, Tiger.new].each do |t|
205
+ generate_tests_for(t)
206
+ end
207
+ end
208
+
209
+ This is a bit like defining a 'class method' in a `TestCase` and then being able to call it to generate contexts or tests within that `TestCase`. It's not that tricky once you get used to it.
210
+
211
+
212
+ And now, the more experimental stuff
213
+ ====================================
214
+
215
+ Wouldn't it be nice to be able to introspect a failed test without having to re-run it? Well, you can. Lets imagine this test:
216
+
217
+ context "A thing" do
218
+ setup do
219
+ @thing = Thing.new
220
+ end
221
+ should "act like a thing" do
222
+ assert_equal "thingish", @thing.nature
223
+ end
224
+ end
225
+
226
+ Well... TO BE CONTINUED.
227
+
228
+
229
+
230
+ [Test::Unit]: http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/
231
+ [TkRunner]: http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit/UI/Tk/TestRunner.html
232
+ [RSpec]: http://rspec.info
233
+ [Cucumber]: http://cukes.info
234
+ [MonkeySpecDoc]: http://jgre.org/2008/09/03/monkeyspecdoc/
235
+ [before_all]: http://rspec.info/documentation/
236
+ [fast_context]: https://github.com/lifo/fast_context
237
+ [test_startup]: https://github.com/freerange/test_startup
238
+ [shoulda]: https://github.com/thoughtbot/shoulda
239
+ [baretest]: https://github.com/apeiros/baretest
240
+ [riot]: https://github.com/thumblemonks/riot
241
+ [context]: https://github.com/jm/context
242
+ [contest]: https://github.com/citrusbyte/contest
243
+ [Testy]: https://github.com/ahoward/testy
244
+ [Tryouts]: https://github.com/delano/tryouts
245
+ [Zebra]: https://github.com/jamesgolick/zebra
246
+ [Shindo]: https://github.com/geemus/shindo
247
+ [Exemplor]: https://github.com/quackingduck/exemplor
@@ -0,0 +1,100 @@
1
+ module Kintama
2
+ autoload :Runnable, 'kintama/runnable'
3
+ autoload :Context, 'kintama/context'
4
+ autoload :Test, 'kintama/test'
5
+ autoload :TestFailure, 'kintama/test'
6
+ autoload :Runner, 'kintama/runner'
7
+ autoload :Assertions, 'kintama/assertions'
8
+
9
+ class << self
10
+ def reset
11
+ @default_context = Class.new(Runnable)
12
+ @default_context.send(:include, Kintama::Context)
13
+ end
14
+
15
+ def default_context
16
+ reset unless @default_context
17
+ @default_context
18
+ end
19
+
20
+ # Create a new context. Aliases are 'testcase' and 'describe'
21
+ def context(name, parent=default_context, &block)
22
+ default_context.context(name, parent, &block)
23
+ end
24
+ alias_method :testcase, :context
25
+ alias_method :describe, :context
26
+
27
+ # Create a new context starting with "given "
28
+ def given(name, parent=default_context, &block)
29
+ default_context.given(name, parent, &block)
30
+ end
31
+
32
+ # Add a setup which will run at the start of every test.
33
+ def setup(&block)
34
+ default_context.setup(&block)
35
+ end
36
+
37
+ # Add a teardown which will be run at the end of every test.
38
+ def teardown(&block)
39
+ default_context.teardown(&block)
40
+ end
41
+
42
+ # Makes behaviour available within tests:
43
+ #
44
+ # module SomeModule
45
+ # def blah
46
+ # end
47
+ # end
48
+ # Kintama.include SomeModule
49
+ #
50
+ # Any methods will then be available within setup, teardown or tests.
51
+ def include(mod)
52
+ default_context.send(:include, mod)
53
+ end
54
+
55
+ # Make new testing behaviour available for the definition of tests.
56
+ # Methods included in this way are available during the definition of tests.
57
+ def extend(mod)
58
+ default_context.extend(mod)
59
+ end
60
+
61
+ # Adds the hook to automatically run all known tests using #run when
62
+ # ruby exits; this is most useful when running a test file from the command
63
+ # line or from within an editor
64
+ def add_exit_hook
65
+ return if @__added_exit_hook
66
+ at_exit { exit(Runner.default.run ? 0 : 1) }
67
+ @__added_exit_hook = true
68
+ end
69
+
70
+ # Tries to determine whether or not this is a sensible situation to automatically
71
+ # run all tests when ruby exits. At the moment, this is true when either:
72
+ # - the test was run via rake
73
+ # - the test file was run as the top-level ruby script
74
+ #
75
+ # This method will always return false if the environment variable
76
+ # KINTAMA_EXPLICITLY_DONT_RUN is not nil.
77
+ def should_run_on_exit?
78
+ return false if ENV["KINTAMA_EXPLICITLY_DONT_RUN"]
79
+ return test_file_was_run? || run_via_rake?
80
+ end
81
+
82
+ private
83
+
84
+ def test_file_was_run?
85
+ caller.last.split(":").first == $0
86
+ end
87
+
88
+ def run_via_rake?
89
+ caller.find { |line| File.basename(line.split(":").first) == "rake_test_loader.rb" } != nil
90
+ end
91
+ end
92
+ end
93
+
94
+ [:context, :given, :describe, :testcase].each do |method|
95
+ unless self.respond_to?(method)
96
+ eval %|def #{method}(*args, &block); Kintama.#{method}(*args, &block); end|
97
+ end
98
+ end
99
+
100
+ Kintama.add_exit_hook if Kintama.should_run_on_exit?
@@ -0,0 +1,38 @@
1
+ module Kintama
2
+ module Assertions
3
+ def assert(expression, message="failed")
4
+ raise Kintama::TestFailure, message unless expression
5
+ end
6
+
7
+ def flunk
8
+ assert false, "flunked."
9
+ end
10
+
11
+ def assert_equal(expected, actual, message="Expected #{expected.inspect} but got #{actual.inspect}")
12
+ assert actual == expected, message
13
+ end
14
+
15
+ def assert_not_equal(expected, actual, message)
16
+ assert actual != expected, message
17
+ end
18
+
19
+ def assert_nil(object, message="#{object.inspect} was not nil")
20
+ assert_equal nil, object, message
21
+ end
22
+
23
+ def assert_not_nil(object, message="should not be nil")
24
+ assert_not_equal nil, object, message
25
+ end
26
+
27
+ def assert_kind_of(klass, thing, message="should be a kind of #{klass}")
28
+ assert thing.is_a?(klass)
29
+ end
30
+
31
+ def assert_raises(message="should raise an exception", &block)
32
+ yield
33
+ raise Kintama::TestFailure, message
34
+ rescue
35
+ # do nothing, we expected this, but now no TestFailure was raised.
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,187 @@
1
+ module Kintama
2
+ module Context
3
+ def setup # noop
4
+ end
5
+
6
+ def teardown # noop
7
+ end
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ # Create a new context. If this is called within a context, a new subcontext
16
+ # will be created. Aliases are 'testcase' and 'describe'
17
+ def context(name, parent=self, &block)
18
+ c = Class.new(parent)
19
+ c.send(:include, Kintama::Context)
20
+ c.name = name.to_s
21
+ c.class_eval(&block)
22
+ c
23
+ end
24
+ alias_method :testcase, :context
25
+ alias_method :describe, :context
26
+
27
+ # Create a new context starting with "given "
28
+ def given(name, parent=self, &block)
29
+ context("given " + name, parent, &block)
30
+ end
31
+
32
+ def setup_blocks
33
+ @setup_blocks ||= []
34
+ end
35
+
36
+ def teardown_blocks
37
+ @teardown_blocks ||= []
38
+ end
39
+
40
+ # Define the setup for this context.
41
+ # It will also be run for any subcontexts, before their own setup blocks
42
+ def setup(&block)
43
+ self.setup_blocks << block
44
+
45
+ # redefine setup for the current set of blocks
46
+ blocks = self.setup_blocks
47
+ define_method(:setup) do
48
+ super
49
+ blocks.each { |b| instance_eval(&b) }
50
+ end
51
+ end
52
+
53
+ # Define the teardown for this context.
54
+ # It will also be run for any subcontexts, after their own teardown blocks
55
+ def teardown(&block)
56
+ self.teardown_blocks << block
57
+
58
+ # redefine teardown for the current set of blocks
59
+ blocks = self.teardown_blocks
60
+ define_method(:teardown) do
61
+ blocks.each { |b| instance_eval(&b) }
62
+ super
63
+ end
64
+ end
65
+
66
+ # Defines the subject of any matcher-based tests.
67
+ def subject(&block)
68
+ define_method(:subject, &block)
69
+ end
70
+
71
+ # Define a test to run in this context.
72
+ def test(name, &block)
73
+ c = Class.new(self)
74
+ c.send(:include, Kintama::Test)
75
+ c.name = name
76
+ c.block = block if block_given?
77
+ end
78
+
79
+ # Define a test to run in this context. The test name will start with "should "
80
+ # You can either supply a name and block, or a matcher. In the latter case, a test
81
+ # will be generated using that matcher.
82
+ def should(name_or_matcher, &block)
83
+ if name_or_matcher.respond_to?(:matches?)
84
+ test("should " + name_or_matcher.description) do
85
+ assert name_or_matcher.matches?(subject), name_or_matcher.failure_message
86
+ end
87
+ else
88
+ test("should " + name_or_matcher, &block)
89
+ end
90
+ end
91
+
92
+ # Define a test using a negated matcher, e.g.
93
+ #
94
+ # subject { 'a' }
95
+ # should_not equal('b')
96
+ #
97
+ def should_not(matcher)
98
+ test("should not " + matcher.description) do
99
+ assert !matcher.matches?(subject), matcher.negative_failure_message
100
+ end
101
+ end
102
+
103
+ # Define a test to run in this context. The test name will start with "it "
104
+ def it(name, &block)
105
+ test("it " + name, &block)
106
+ end
107
+
108
+ def inherited(child)
109
+ children << child
110
+ end
111
+
112
+ def children
113
+ @children ||= []
114
+ end
115
+
116
+ def tests
117
+ children.select { |c| c.is_a_test? }.sort_by { |t| t.name }
118
+ end
119
+
120
+ def subcontexts
121
+ children.select { |c| c.is_a_context? }.sort_by { |s| s.name }
122
+ end
123
+
124
+ # Returns true if this context has no known failed tests.
125
+ def passed?
126
+ failures.empty?
127
+ end
128
+
129
+ # Returns an array of tests in this and all subcontexts which failed in
130
+ # the previous run
131
+ def failures
132
+ ran_tests.select { |t| !t.passed? } + subcontexts.map { |s| s.failures }.flatten
133
+ end
134
+
135
+ def pending
136
+ tests.select { |t| t.pending? } + subcontexts.map { |s| s.pending }.flatten
137
+ end
138
+
139
+ def [](name)
140
+ subcontexts.find { |s| s.name == name } || tests.find { |t| t.name == name }
141
+ end
142
+
143
+ def method_missing(name, *args, &block)
144
+ if self[de_methodize(name)]
145
+ self[de_methodize(name)]
146
+ else
147
+ begin
148
+ super
149
+ rescue NameError, NoMethodError => e
150
+ if parent
151
+ parent.send(name, *args, &block)
152
+ else
153
+ raise e
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ def respond_to?(name)
160
+ self[name] ||
161
+ super ||
162
+ (parent ? parent.respond_to?(name) : false)
163
+ end
164
+
165
+ # Runs all tests in this context and any subcontexts.
166
+ # Returns true if all tests passed; otherwise false
167
+ def run(runner=nil)
168
+ @ran_tests = []
169
+ runner.context_started(self) if runner
170
+ tests.each { |t| instance = t.new; instance.run(runner); ran_tests << instance }
171
+ subcontexts.each { |s| s.run(runner) }
172
+ runner.context_finished(self) if runner
173
+ passed?
174
+ end
175
+
176
+ private
177
+
178
+ def de_methodize(name)
179
+ name.to_s.gsub("_", " ")
180
+ end
181
+
182
+ def ran_tests
183
+ @ran_tests || []
184
+ end
185
+ end
186
+ end
187
+ end