given 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,206 @@
1
+ p{display=none}. <notextile><!-- -*- mode: textile; fill-column: 1000000; -*- --></notextile>
2
+
3
+ h1. Thoughts on a new Ruby Specification Framework
4
+
5
+ I've been playing aournd with some ideas for a new testing/specification framework for Ruby[1]. I've been trying to write down some of the motivation for this, but that's taking too long. I just want to get some ideas down and published for review and we will address the whys and wherefores later.
6
+
7
+ Essentially, I've been inspired by the Cucumber framework to bring the given/when/then style of specifications directly into unit tests. So let's get right into it.
8
+
9
+ h2. Status
10
+
11
+ Given is now at a point where it is usable for small or experimental projects. I would love for people to give it a try and see how it works out for them. I wouldn't recommend it for anything mainstream yet because the details of the API are still subject to change.
12
+
13
+ h2. Example Zero
14
+
15
+ Here's the spec that I've been playing with. Its gone through mulitple revisions and at least one prototype implementation. And this is probably not the final form.
16
+
17
+ With all that in mind, here's a specification in my imaginary framework:
18
+
19
+ <pre>
20
+ require 'given/test_unit'
21
+ require 'examples/stack'
22
+
23
+ class StackBehavior < Given::TestCase
24
+ Invariant { expect(@stack.depth) >= 0 }
25
+ Invariant { expect(@stack.empty?) == (@stack.depth == 0) }
26
+
27
+ Given(:empty_stack) do
28
+ Then { expect(@stack.depth) == 0 }
29
+
30
+ When { @stack.push(:an_item) }
31
+ Then { expect(@stack.depth) == 1 }
32
+ Then { expect(@stack.top) == :an_item }
33
+
34
+ When { @stack.pop }
35
+ FailsWith(Stack::UsageError)
36
+ Then { expect(exception.message) =~ /empty/ }
37
+ end
38
+
39
+ Given(:stack_with_two_items) {
40
+ Then { expect(@stack.top) == :top_item }
41
+ Then { expect(@stack.depth) == 2}
42
+
43
+ When { @result = @stack.pop }
44
+ Then { expect(@result) == :top_item }
45
+ Then { expect(@stack.top) == :bottom_item }
46
+ Then { expect(@stack.depth) == 1 }
47
+
48
+ When {
49
+ @stack.pop
50
+ @result = @stack.pop
51
+ }
52
+ Then { expect(@result) == :bottom_item }
53
+ Then { expect(@stack.depth) == 0 }
54
+ }
55
+
56
+ def empty_stack
57
+ @stack = Stack.new
58
+ end
59
+
60
+ def stack_with_two_items
61
+ empty_stack
62
+ @stack.push(:bottom_item)
63
+ @stack.push(:top_item)
64
+ end
65
+ end
66
+ </pre>
67
+
68
+ Let's talk about the individual sections.
69
+
70
+ h3. Given
71
+
72
+ The _Given_ section specifies a starting point, a set of preconditions that must be true before the code under test is allowed to be run. In standard test frameworks the preconditions are established with a combination of setup methods (or :before actions in RSpec) and code in the test.
73
+
74
+ In the example code above, we see two starting points of interest. One is an empty, just freshly created stack. The other starting point is a stack with several items already push onto it.
75
+
76
+ The setup methods are explicitly named by the given section. The name of the setup method should be carefully named to provide the human reader the necessary information.
77
+
78
+ h3. When
79
+
80
+ The _When_ section specifies the code to be tested ... oops, excuse me ... specified. After the preconditions in the given section are met, the when code block is run.
81
+
82
+ h3. Then
83
+
84
+ The _Then_ sections are the postconditions of the specification. These then conditions must be true after the code under test (the _When_ block) is run.
85
+
86
+ The code in the _Then_ block should be a single boolean condition that evaluates to true if the code in the _When_ block is correct. If the _Then_ block evaluates to false, then that is recorded as a failure.
87
+
88
+ h3. Invariant
89
+
90
+ The _Invariant_ block is a new idea that doesn't have an analog in RSpec or Test::Unit. The invariant allows you specify things that must always be true. In the stack example, <tt>empty?</tt> is defined in term of <tt>size</tt>. Whenever <tt>size</tt> is 0, <tt>empty?</tt> should be true. Whenever <tt>size</tt> is non-zero, <tt>empty?</tt> should be false.
91
+
92
+ You can conceptually think of an _Invariant_ block as a _Then_ block that automatically gets added to every _When_ within its scope.
93
+
94
+ h2. Other Ideas
95
+
96
+ That's the basics of what I'm trying to do. Here are some more ideas.
97
+
98
+ h3. Nesting Givens
99
+
100
+ Although the example doesn't demonstrate this, I think the _Given_ blocks should be allowed to nest. This is similar to the nested contexts in Shoulda or the nested describe blocks in RSpec.
101
+
102
+ h3. Direct Code in Givens
103
+
104
+ Since the block on a _Given_ section is used to scope the <em>When</em>'s and <em>Then</em>s, it can't be used to directly specify the setup code. That's why we put the setup code in a named method and pass the name of the method to the _Given_. I actually like the way that reads, but also am wondering if there is a way to allow direct code as well.
105
+
106
+ Here's one idea:
107
+
108
+ <pre>
109
+ Given(Setup { @stack = Stack.new }) do
110
+ When { ... }
111
+ Then { ... }
112
+ end
113
+ </pre>
114
+
115
+ Here's another idea:
116
+
117
+ <pre>
118
+ Given { @stack = Stack.new }.and do
119
+ When { ... }
120
+ Then { ... }
121
+ end
122
+ </pre>
123
+
124
+ I think both options are ugly.
125
+
126
+ h2. Recent Changes
127
+
128
+ h3. Contract/Behavior -> TestCase
129
+
130
+ I've been playing with the idea of calling the containing classes either Give::Behavior or Given::Contract. I like the name Contract. And while the pre/post condition thing is very close to the idea of contracts, its not quite the same thing. Behavior comes close to describing the idea in my head, but I'm not overly fond of the word itself.
131
+
132
+ So finally, I just tossed both ideas and just went with Given::TestCase. The first implementation of Given is on top of Test::Unit, and we allow Test::Unit style test_xxx methods in the class, so there's no need to hide it.
133
+
134
+ h3. Expectations
135
+
136
+ I really like the idea of being able to just say:
137
+
138
+ <pre>
139
+ Then { @result == 1 }
140
+ </pre>
141
+
142
+ and have that raw ruby code be the expectation for the specification. Unfortunately, that has two drawbacks when I got down into the nitty gritty details.
143
+
144
+ # It just returns a boolean value. So when the expectation fails, all the reporting software can to is just say the expectation failed. It can't give any details about why the expectation failed.
145
+ # The second problem is another reporting problem. When the Then block returns false an AssertionFailed error will be thrown and handled by the test running software. Unfortunately the error is thrown from a point in the stack after the Then code block has completed running, therefore the source code location of the offending Then block is no longer in the stack trace.
146
+
147
+ Both of these problems can be solved if the AssertionFailed error was thrown while the Then block was still active. So how do we accomplish that?
148
+
149
+ h4. Idea #1 -- Use assert_xxx
150
+
151
+ We could just code the assert_equal (and related assert_xxx functions) explicitly in the Then block.
152
+
153
+ <pre>
154
+ Then { assert_equal 1, @result }
155
+ </pre>
156
+
157
+ This will actually almost work with no changes. Only one small problem, assert_xxx methods do not necessarily return true if the assertion passed. Therefore, the assert can pass, but the Then will fail because assert_equal returns nil.
158
+
159
+ I imagine I might have to give up that particular behavior of Then blocks, but I'm not ready to yet.
160
+
161
+ The bigger problem is that it is just too ugly. I'm afraid people will be tempted to put multiple asserts in a single Then, something I'm trying to discourage.
162
+
163
+ h4. Idea #2 -- Use RSpec-like Syntax
164
+
165
+ We could just use RSpec like syntax in the Then block:
166
+
167
+ <pre>
168
+ Then { @result.should == 1 }
169
+ </pre>
170
+
171
+ In fact, the Matchy library will allow us to use the .should notation in test unit. It should work for Given as well. There is one minor problem with this and one philosophical problem. The minor problem is that like assert_xxx, the Matchy .should methods return non-true if the assertion passes.
172
+
173
+ The philosophical problem is that I really dislike polluting Object's namespace dropping a should method into every object. It just rubs me the wrong way.
174
+
175
+ h4. Idea #3 -- ParseTree
176
+
177
+ This suggestion falls in the category of bizzare and weird. Why not use ParseTree to get the s-expression of the Ruby code in the Then block and parse out the individual pieces of the code and find whatever it needs for the error messages.
178
+
179
+ Althought the idea sounds cool, it also sounds really fragile. ParseTree pulls out the internal parse tree generated by the Ruby runtime, and that internal data structure has been known to change between version. Also, ParseTree doesn't run under JRuby so we would be artificially limiting the usefulness of the Given library. No, this idea doesn't even get off the ground.
180
+
181
+ h4. Idea #4 -- Redefine operators
182
+
183
+ We could redefine operators like == throw exceptions at the appropriate times. This would allow the Then blocks to remain simple Ruby expressions. There are multiple problems with this:
184
+
185
+ # The operator redefinitions would have to be applied at the beginning of the block and removed after the block was done. Although doable, this seems like a lot of definition churn.
186
+ # The operator redefinitions should only apply at the top level of the expression. Currenly, I have no idea of how to accomplish this.
187
+ # Since operators have different definitions for each class, the operators would have to be redefined for all classes.
188
+ # This only addresses expressions with an operator at the top level. It still doesn't handle expressions like: <code>Then { @result.nil? }</code>.
189
+
190
+ h4. Idea #5 -- expect()
191
+
192
+ Here's idea stolen from one of the Javascript testing libraries. If we wrap the item we are making assertions about in a method, we can have that method return an expectation object that properly responds to operators and queries. If we name that object properly, then we should a nicely readable syntax too. Something like this:
193
+
194
+ <pre>
195
+ Then { expect(@result) == 1 }
196
+ Then { expect(@result).nil? }
197
+ </pre>
198
+
199
+ Technically, this is equivalent to the RSpec/Should solution, but without polluting the global object namespace. And it reads almost as well. I think I like this.
200
+
201
+ h2. Summary
202
+
203
+ Feel free to comment on the ideas here. Eventually I hope to have a working prototype.
204
+
205
+ <hr>
206
+ fn1. Right, like Ruby doesn't have enough of them.
@@ -0,0 +1,35 @@
1
+ require 'rake/clean'
2
+ require 'rake/testtask'
3
+
4
+ CLOBBER.include("html")
5
+
6
+ task :default => ["test:units", "test:functionals"]
7
+
8
+ task :tu => "test:units"
9
+ task :tf => "test:functionals"
10
+
11
+ EXAMPLE_FILES = FileList['examples/*_behavior.rb']
12
+ desc "Run Examples"
13
+ task :examples do
14
+ ruby "-I.:lib #{EXAMPLE_FILES}"
15
+ end
16
+
17
+ # README Formatting --------------------------------------------------
18
+
19
+ require 'redcloth'
20
+
21
+ directory 'html'
22
+
23
+ desc "Display the README file"
24
+ task :readme => "html/README.html" do
25
+ sh "open html/README.html"
26
+ end
27
+
28
+ desc "format the README file"
29
+ task "html/README.html" => ['html', 'README.textile'] do
30
+ open("README.textile") do |source|
31
+ open('html/README.html', 'w') do |out|
32
+ out.write(RedCloth.new(source.read).to_html)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ class Stack
2
+ class UsageError < RuntimeError
3
+ end
4
+
5
+ def initialize
6
+ @items = []
7
+ end
8
+
9
+ def depth
10
+ @items.size
11
+ end
12
+
13
+ def empty?
14
+ @items.size == 0
15
+ end
16
+
17
+ def top
18
+ @items.last
19
+ end
20
+
21
+ def push(item)
22
+ @items.push(item)
23
+ end
24
+
25
+ def pop
26
+ fail UsageError, "Cannot pop an empty stack" if empty?
27
+ @items.pop
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ require 'given/test_unit'
2
+ require 'examples/stack'
3
+
4
+ class StackBehavior < Given::TestCase
5
+ Invariant { expect(@stack.depth) >= 0 }
6
+ Invariant { expect(@stack.empty?) == (@stack.depth == 0) }
7
+
8
+ Given(:empty_stack) do
9
+ Then { expect(@stack.depth) == 0 }
10
+
11
+ When { @stack.push(:an_item) }
12
+ Then { expect(@stack.depth) == 1 }
13
+ Then { expect(@stack.top) == :an_item }
14
+
15
+ When { @stack.pop }
16
+ FailsWith(Stack::UsageError)
17
+ Then { expect(exception.message) =~ /empty/ }
18
+ end
19
+
20
+ Given(:stack_with_two_items) {
21
+ Then { expect(@stack.top) == :top_item }
22
+ Then { expect(@stack.depth) == 2}
23
+
24
+ When { @result = @stack.pop }
25
+ Then { expect(@result) == :top_item }
26
+ Then { expect(@stack.top) == :bottom_item }
27
+ Then { expect(@stack.depth) == 1 }
28
+
29
+ When {
30
+ @stack.pop
31
+ @result = @stack.pop
32
+ }
33
+ Then { expect(@result) == :bottom_item }
34
+ Then { expect(@stack.depth) == 0 }
35
+ }
36
+
37
+ def empty_stack
38
+ @stack = Stack.new
39
+ end
40
+
41
+ def stack_with_two_items
42
+ empty_stack
43
+ @stack.push(:bottom_item)
44
+ @stack.push(:top_item)
45
+ end
46
+ end
@@ -0,0 +1,6 @@
1
+ module Given
2
+ end
3
+
4
+ require 'given/errors'
5
+ require 'given/dsl'
6
+ require 'given/expectation'
@@ -0,0 +1,22 @@
1
+ module Given
2
+ class AnonymousCode
3
+ def initialize(block)
4
+ @block = block
5
+ end
6
+
7
+ def run(context)
8
+ context.instance_eval(&@block)
9
+ end
10
+
11
+ def line_marker
12
+ nil
13
+ end
14
+
15
+ def file_line
16
+ nil
17
+ end
18
+ end
19
+
20
+ DO_NOTHING = AnonymousCode.new(lambda { })
21
+ TRUE_CODE = AnonymousCode.new(lambda { true })
22
+ end
@@ -0,0 +1,20 @@
1
+ require 'given/anonymous_code'
2
+
3
+ module Given
4
+ class Code < AnonymousCode
5
+ def initialize(mark, block)
6
+ @mark = mark
7
+ super(block)
8
+ end
9
+
10
+ def line_marker
11
+ "%s%d" % [@mark, eval("__LINE__", @block)]
12
+ end
13
+
14
+ def file_line
15
+ file = eval("__FILE__", @block)
16
+ line = eval("__LINE__", @block)
17
+ "#{file}:#{line}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,114 @@
1
+ require 'given/anonymous_code'
2
+ require 'given/code'
3
+
4
+ module Given
5
+ module DSL
6
+ module TestHelper
7
+ def exception
8
+ @_given_exception
9
+ end
10
+
11
+ def given_check(ok, msg, args)
12
+ given_failure(msg % args) if ! ok
13
+ true
14
+ end
15
+
16
+ def expect(value)
17
+ Given::Expectation.new(value, self)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def Given(*args, &block)
24
+ _given_levels.push(Code.new('G', block))
25
+ @_given_setup_codes ||= []
26
+ @_given_invariant_codes ||= []
27
+ @_given_when_code = DO_NOTHING
28
+ @_given_exception_class = nil
29
+ old_setups = @_given_setup_codes
30
+ old_invariants = @_given_invariant_codes
31
+ @_given_setup_codes += args
32
+ yield
33
+ ensure
34
+ @_given_setup_codes = old_setups
35
+ @_given_invariant_codes = old_invariants
36
+ @_given_when_code = DO_NOTHING
37
+ _given_levels.pop
38
+ end
39
+
40
+ def When(&when_code)
41
+ _given_must_have_context("When")
42
+ @_given_when_code = Code.new('W', when_code)
43
+ @_given_exception_class = nil
44
+ end
45
+
46
+ def Then(&then_code)
47
+ _given_must_have_context("Then")
48
+ _given_make_test_method("Then", Code.new('T', then_code), @_given_exception_class)
49
+ end
50
+ alias And Then
51
+
52
+ def FailsWith(exception_class, &fail_code)
53
+ _given_must_have_context("FailsWith")
54
+ @_given_exception_class = exception_class
55
+ _given_make_test_method("FailsWith", TRUE_CODE, exception_class)
56
+ fail_code.call if fail_code
57
+ end
58
+
59
+ def Invariant(&block)
60
+ @_given_invariant_codes ||= []
61
+ @_given_invariant_codes += [Code.new('I', block)]
62
+ end
63
+
64
+ # Internal Use Methods -------------------------------------------
65
+
66
+ def _given_levels
67
+ @_given_levels ||= []
68
+ end
69
+
70
+ def _given_must_have_context(clause)
71
+ fail UsageError, "A #{clause} clause must be inside a given block" if
72
+ _given_levels.size <= 0
73
+ end
74
+
75
+ def _given_test_name(setup_codes, when_code, then_code)
76
+ tags = _given_levels.map { |code| code.line_marker }
77
+ tags << when_code.line_marker
78
+ if then_code
79
+ tags << then_code.line_marker
80
+ end
81
+ tags.compact!
82
+ sort = "%05d" % tags.last[1..-1].to_i
83
+ "test__#{sort}_#{tags.join('_')}_"
84
+ end
85
+
86
+ def _given_make_test_method(clause, then_code, exception_class)
87
+ setup_codes = @_given_setup_codes
88
+ when_code = @_given_when_code
89
+ invariant_codes = @_given_invariant_codes
90
+ define_method _given_test_name(setup_codes, when_code, then_code) do
91
+ setup_codes.each do |s| send s end
92
+ if exception_class.nil?
93
+ when_code.run(self)
94
+ else
95
+ begin
96
+ when_code.run(self)
97
+ given_failure("Expected #{exception_class} Exception", when_code)
98
+ rescue exception_class => ex
99
+ @_given_exception = ex
100
+ rescue Exception => ex
101
+ @_given_exception = ex
102
+ given_failure("Expected #{exception_class} Exception, " +
103
+ "but got #{exception.class}",
104
+ when_code)
105
+ end
106
+ end
107
+ given_assert(clause, then_code)
108
+ invariant_codes.each do |inv|
109
+ given_assert("Invariant", inv)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end