kintama 0.1.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.
@@ -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