thumblemonks-protest 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,42 +1,284 @@
1
1
  # Protest
2
2
 
3
- An extremely fast-running, context-driven, unit testing framework.
3
+ An extremely fast, expressive, and context-driven unit-testing framework.
4
4
 
5
- context "Foo" do
6
- setup do
7
- # some setup stuff
8
- @foo = Foo.new
5
+ #### Example: Basic booleans
6
+
7
+ **NOTE:** For no specific reason, I'm going to use an ActiveRecord model in the following examples.
8
+
9
+ At it's very basic, Protest simply tries to assert that an expression is true or false. Protest does this through its `asserts` and `denies` tests. An `asserts` test will pass if the result of running the test is neither `nil` or `false`. A `denies` test confirms just the opposite.
10
+
11
+ For instance, given a test file named `foo_test.rb`, you might have the following code in it:
12
+
13
+ require 'protest'
14
+
15
+ context "a new user" do
16
+ setup { @user = User.new }
17
+ asserts("that it is not yet created") { @user.new_record? }
18
+ denies("that it is valid") { @user.valid? }
19
+ end
20
+
21
+ Notice that you do not define a class anywhere. That would be the entire contents of that test file.
22
+
23
+ #### Example: Equality
24
+
25
+ One of the most common assertions you will (or do already) utilize is that of equality; is this equal to that? Protest supports this in a slightly different manner than most other frameworks. With Protest, you add the expectation to the assertion itself.
26
+
27
+ For example:
28
+
29
+ context "a new user" do
30
+ setup { @user = User.new(:email => 'foo@bar.com') }
31
+ asserts("email address") { @user.email }.equals('foo@bar.com')
32
+ end
33
+
34
+ Here, you should begin to notice that tests themselves return the actual value. You do not write assertions into the test. Assertions are "aspected" onto the test. If the test above did not return 'foo@bar.com' for `@user.email`, the assertion would have failed.
35
+
36
+ The `equals` modifier works with any type of value, including nil's. However, if you wanted to test for nil explicitly, you could simple do this:
37
+
38
+ context "a new user" do
39
+ setup { @user = User.new }
40
+ asserts("email address") { @user.email }.nil
41
+ end
42
+
43
+ Notice the `nil` modifier added to asserts. Also notice how the test almost reads as "a new user asserts email address *is* nil". With Test::Unit, you would have probably written:
44
+
45
+ class UserTest < Test::Unit::TestCase
46
+ def setup
47
+ @user = User.new
9
48
  end
10
49
 
11
- asserts("this block returns true") do
12
- true
50
+ def test_email_address_is_nil
51
+ assert_nil @user.email
13
52
  end
53
+ end
14
54
 
15
- asserts("this block returns a specific string").equals("my friend") do
16
- @foo.your_mom
55
+ Which, to me, seems like a redundancy. The test already says it's nil! Maybe Shoulda woulda helped:
56
+
57
+ class UserTest < Test::Unit::TestCase
58
+ def setup
59
+ @user = User.new
17
60
  end
61
+
62
+ should "have nil email" { assert_nil @user.email }
18
63
  end
19
64
 
20
- MORE TO COME
65
+ In my opinion, the same redundancy exists. Sure, I could write a macro like `should_be_nil {@user.email}`, but the redundancy exists in the framework itself.
66
+
67
+ #### Example: Matches
68
+
69
+ If you need to assert that a test result matches a regular expression, use the `matches` modifier like so:
70
+
71
+ context "a new user" do
72
+ setup { @user = User.new }
73
+
74
+ # I'm a contrived example
75
+ asserts("random phone number") { @user.random_phone_number }.matches(/^\d{3}-\d{3}-\d{4}$/)
76
+ end
77
+
78
+ #### Example: Raises
79
+
80
+ Sometimes, your test raises an exception that you actually expected.
81
+
82
+ context "a new user" do
83
+ setup { @user = User.new }
84
+ asserts("with bad data") { @user.save! }.raises(ActiveRecord::RecordInvalid)
85
+ end
86
+
87
+ #### Example: Kind Of
88
+
89
+ When you want to test that an expression returns an object of an expected type:
90
+
91
+ context "a new user" do
92
+ setup { @user = User.new }
93
+ asserts("the balance") { @user.balance }.kind_of(Currency)
94
+ end
95
+
96
+ #### Example: Nested contexts
97
+
98
+ Oh yeah, Protest does those, too. The `setup` from each parent is "loaded" into the context and then the context is executed as normal. Test naming is a composite of the parent contexts' names. Here, we'll do a little Sinatra testing (see below for instructions on how to make it Protest work seamlessly with Sinatra tests).
99
+
100
+ context "My App:" do
101
+ setup { @app = MyApp }
102
+
103
+ context "get /" do
104
+ setup { get '/' }
105
+ # ...
106
+ # assertions
107
+
108
+ context "renders a body" do
109
+ setup { @body = last_response.body }
110
+ # ...
111
+ # assertions
112
+ end
113
+ end
114
+
115
+ context "get /books/1" do
116
+ setup { get '/books/1' }
117
+ # ...
118
+ # assertions
119
+ end
120
+ end
121
+
122
+ #### More examples/documentation
123
+
124
+ There are many more basic assertion modifiers to come. See below for writing Protest extensions if you want to help out.
125
+
126
+ See the TODO section for everything that's missing.
127
+
128
+ Also, see [the wiki](http://wiki.github.com/thumblemonks/protest) for more examples and documentation.
21
129
 
22
130
  ## You say, "OMG! Why did you write this?"
23
131
 
24
- #### Some background, I guess
132
+ ### Some background, I guess
25
133
 
26
134
  You start a new project. You get all excited. You're adding tests. You're adding factories. You're fixturating your setups. You're adding more tests. Your tests start slowing down, but you need to keep pushing because your backlog has a lot of new, nifty features in it. You've got 3000+ lines of test code, 2000+ assertions. Your tests are annoyingly slow. Your tests have become a burden.
27
135
 
28
- I hate this and it happens a lot, even when I'm conscience that it's happening.
136
+ I hate this and it happens a lot, even when I'm conscience that it's happening. Even when I'm not hitting the database and I'm mocking the crap out my code.
29
137
 
30
- #### How Protest is different
138
+ I really, really hate slow test suites.
31
139
 
32
- Protest differs in that it does not rerun setup for each test in a context. Each assertion should not mangle the context and therefore not require setup to be run more than once. Contexts can be nested and setups inherited, but setup is called only once per context.
140
+ #### Did ... you look at Shoulda
141
+
142
+ I should say that I love Shoulda in theory and in practice. It changed the way I coded. I added macros all over the place. I built macros into the gems I wrote for the gem itself. But, alas, Shoulda is slow. Shoulda is based on Test::Unit. Shoulda reruns setups for every should. Shoulda could make my life even easier with even more expressiveness.
143
+
144
+ #### Did ... you look at RSpec
145
+
146
+ :| yes, no, I don't care. It's slow, too. Anyways, I was at [ObjectMentor](http://objectmentor.com) many, many moons ago when Dave Astels (accompanied by David Chelimsky) were explaining this brand new approach to testing called BDD. Mr. Astels explained to us that we if we already understood TDD, then BDD wouldn't help a bunch. Why argue with him?
147
+
148
+ ### How Protest is the same
149
+
150
+ 1. It defines a context
151
+ 1. It prints .'s, F's, and E's when tests pass, fail, or error
152
+ 1. It tells you how long it took to run just the tests
153
+ 1. Contexts can be nested and setups inherited
154
+
155
+ ### How Protest is different
156
+
157
+ Protest differs primarily in that it does not rerun setup for each test in a context. I know this is going to shock and awe a lot of folks. However, over the past several years of my doing TDD in some capacity or another, there are certain habits I have tried to pick up on any many others I have tried to drop.
158
+
159
+ For instance, I believe that no assertion should mangle the context of the test data it is running in. Following this allows me to require setup be run only once for a collection of related assertions. Even in a nested context where setups are inherited, the setup's are called only once per the specific context.
160
+
161
+ Following all of this allows me to have very fast tests (so far).
33
162
 
34
163
  ...
35
164
 
36
- You say, "Ok, but Shoulda is like this!"
165
+ Protest is also different in that assertions are not added to the test block. Each test block is it's own assertion (and assertion block). Whatever the result is of processing an assertion block will be used to determine if an assertion passed or failed. Each assertion block can have a specific validator tied to it for asserting any number of things, like: the result of the test **equals** some expected value, the result of the test **matches** some expected expression, or the result of the test **raises** some exception.
166
+
167
+ I like this approach because I only want to test one thing, but that one thing may require some work on my behalf to get the value. Protest does not let me cheat in this regard. There is no way for me to add more than one assertion to an assertion block.
168
+
169
+ ...
170
+
171
+ I imagine this approach will persuade many of you to avoid Protest altogether. I don't blame you. A few years ago I would have avoided it, too. As of this writing though, I have ported Chicago and Slvu (which were previously written in Test::Unit + Shoulda) to Protest, cut the number of lines of code in almost half, definitely more than halved the amount of time the tests took to run, and did so in less than half a day (I was building Protest while porting them :).
172
+
173
+ ## Running tests
174
+
175
+ Create or modify your existing Rakefile to define a test task like so:
176
+
177
+ desc 'Default task: run all tests'
178
+ task :default => [:test]
179
+
180
+ desc "Run all tests"
181
+ task :test do
182
+ require 'protest'
183
+ $:.concat ['./lib', './test']
184
+ Dir.glob("./test/*_test.rb").each { |test| require test }
185
+ Protest.report
186
+ end
187
+
188
+ Then, from the command line, you only need to run `rake` or `rake test`. Please make sure to remove all references to any other testing frameworks before running tests. For instance, do not require `test/unit`, `shoulda`, `minitest`, or anything else like it.
189
+
190
+ ### With Sinatra
191
+
192
+ Protest definitely works with the latest Sinatra. I personally use it to run tests for [Chicago](http://github.com/thumblemonks/chicago) and [Slvu](http://github.com/jaknowlden/slvu). Setup is pretty easy and very much like getting your tests to run with Test::Unit. In a test helper file that gets loaded into all of your tests (that need it), enter the following:
193
+
194
+ require 'protest'
195
+ class Protest::Context
196
+ include Rack::Test::Methods
197
+ def app; @app; end
198
+ end
199
+
200
+ And then define a context (or many) for testing your Sinatra app. For instance:
201
+
202
+ require 'test_helper'
203
+
204
+ context 'Some App' do
205
+ setup { @app = SomeApp }
206
+
207
+ context "/index" do
208
+ setup { get '/' }
209
+ # ...
210
+ end
211
+ end
212
+
213
+ Make sure to check out the Protest + Sinatra testing macros in Chicago.
214
+
215
+ ### With Rails
216
+
217
+ It's doubtful that Protest works with Rails very easily as Protest completely replaces Test::Unit. I haven't tried it yet, but intend to with my next new Rails project. Porting would probably take some time unless you only have a few test cases.
218
+
219
+ ## Extending Protest with Macros
220
+
221
+ To extend Protest, similar to how you would with Shoulda, you simply need to include your methods into the `Protest::Context` class. For example, let's say you wanted to add a helpful macro for asserting the response status of some HTTP result in Sinatra. You could do this easily by defining your macro like so:
222
+
223
+ module Custom
224
+ module Macros
225
+ def asserts_response_status(expected)
226
+ asserts("response status is #{expected}") do
227
+ last_response.status
228
+ end.equals(expected)
229
+ end
230
+ end # Macros
231
+ end # Custom
232
+ Protest::Context.instance_eval { include Custom::Macros }
233
+
234
+ And then in your actual test, you might do the following:
235
+
236
+ context 'Some App' do
237
+ setup { @app = SomeApp }
238
+
239
+ context "/index" do
240
+ setup { get '/' }
241
+ asserts_response_status 200
242
+ end
243
+ end
244
+
245
+ **COMING SOON:** Protest will look into test/protest\_macros, but not today.
246
+
247
+ #### Assertion macros
248
+
249
+ If you want to add special macros to an Assertion, this is as easy as adding them to a Context. Similar to Context macros, Assertion macros are included into the Assertion class.
250
+
251
+ For instance, let's say you wanted to add a macro for verifying that the result of an assertion is the same kind\_of object as you would expect. You would define the macro like so:
252
+
253
+ module Custom
254
+ module AssertionMacros
255
+ def kind_of(expected_class)
256
+ actual.kind_of?(expected) || failure("expected kind of #{expected}, not #{actual.inspect}")
257
+ end
258
+ end # AssertionMacros
259
+ end # Custom
260
+ Protest::Assertion.instance_eval { include Custom::AssertionMacros }
261
+
262
+ And in your context, you would use it like so:
263
+
264
+ context "example" do
265
+ asserts("a hash is defined") { {:foo => 'bar'} }.kind_of(Hash)
266
+ end
267
+
268
+ Notice in the new macro we defined the use of the magical **actual** variable. `actual` is evaluated when the assertion is defined and made available to any Assertion macro. If you think you might want to chain assertions checks together, know that only the last failure will get recorded.
37
269
 
38
- Well, it is ... sort of. I love Shoulda. It changed the way I coded. But, Shoulda is slow. Shoulda is based on Test::Unit. Shoulda reruns setups for every should. Shoulda could make my life even easier with some more expressiveness.
270
+ ## TODO
39
271
 
40
- #### How Protest is the same
272
+ TONS OF STUFF
41
273
 
42
- It defines a context. It prints .'s, F's, and E's when tests pass, fail, or error.
274
+ 1. Documentation
275
+ 1. Refactor reporting; some abstracting is needed for recording a result (for instance)
276
+ 1. Need to know where in backtrace a test failed (line number, etc.)
277
+ 1. More assertion macros: throws, etc.
278
+ 1. Handle denies macro different, so that an entire failure message can translated to the 'negative' assertion. I don't want to add deny\_this and deny\_that macros
279
+ 1. Aliases for context "with, without, when, ..."; add those words to test description
280
+ 1. Optimization and simplification (ex. flog is complaining print\_result\_stack)
281
+ 1. Better error messages (maybe need to rename asserts to should for better readability)
282
+ 1. Perhaps: Multiple setup blocks in one context
283
+ 1. Perhaps: association macro chaining
284
+ 1. Perhaps: Uhhhhh ... a teardown method (maybe :)
data/Rakefile CHANGED
@@ -7,5 +7,10 @@ desc "Run tests"
7
7
  task :test do
8
8
  $:.concat ['./test', './lib']
9
9
  Dir.glob("./test/*_test.rb").each { |test| require test }
10
- Protest.run
10
+ Protest.report
11
+ end
12
+
13
+ desc "Run flog against library (except tests)"
14
+ task :flog do
15
+ puts %x[find ./lib -name *.rb | xargs flog]
11
16
  end
@@ -1,31 +1,59 @@
1
1
  module Protest
2
2
 
3
3
  class Assertion
4
- def initialize(description, &block)
4
+ attr_reader :raised
5
+ def initialize(description, target, &block)
5
6
  @description = description
6
- assert_block do |scope|
7
- actual = scope.instance_eval(&block)
8
- actual || failure("expected to be true, not #{actual.inspect}")
9
- end
7
+ actualize(target, &block)
10
8
  end
11
9
 
12
- def assert_block(&block)
13
- @block = block if block_given?
14
- self
10
+ def actual
11
+ @default_failure = @failure = nil if @default_failure
12
+ @actual
15
13
  end
16
14
 
17
- def run(binding_scope) @block.call(binding_scope); end
18
- def failure(message) raise Protest::Failure.new(message, self); end
15
+ def failure(message)
16
+ raise Failure.new(message, self)
17
+ rescue Failure => e
18
+ @failure = e # Smelly (for now)
19
+ end
20
+
21
+ def error
22
+ Error.new("errored with #{raised}", self, raised) if error?
23
+ end
24
+
25
+ def failure?; @failure; end
26
+ def error?; !failure? && raised; end
27
+ def passed?; !failure? && !error?; end
28
+ def result; @failure || error; end
19
29
  def to_s; @description; end
30
+ private
31
+ def actualize(target, &block)
32
+ @actual = target.instance_eval(&block)
33
+ @default_failure = base_assertion
34
+ rescue Exception => e
35
+ @raised = e
36
+ end
37
+
38
+ def base_assertion
39
+ failure("expected true, not #{@actual.inspect}") unless @actual
40
+ end
20
41
  end # Assertion
21
42
 
43
+ # Denial will evaulate to true if the assertion failed in some way. Errors pass through. A Failure
44
+ # is generated if the assertion actually passed.
22
45
  class Denial < Assertion
23
- def run(binding_scope)
24
- super
25
- rescue Protest::Failure => e
26
- true
27
- else
28
- failure("expected to fail, but did not")
46
+ def actual
47
+ @actual # Do not forget default failure unless a failure is thrown
48
+ end
49
+
50
+ alias :actual_failure :failure
51
+ def failure(message)
52
+ @default_failure = @failure = nil # this is a good thing
53
+ end
54
+ private
55
+ def base_assertion
56
+ actual_failure("expected assertion to fail") if @actual
29
57
  end
30
58
  end # Denial
31
59
 
@@ -1,7 +1,8 @@
1
1
  module Protest
2
2
  class Context
3
3
  attr_reader :description, :assertions
4
- def initialize(description, parent=nil)
4
+ def initialize(description, reporter, parent=nil)
5
+ @reporter = reporter
5
6
  @description = description
6
7
  @assertions = []
7
8
  @parent = parent
@@ -20,27 +21,25 @@ module Protest
20
21
  end
21
22
 
22
23
  def to_s; @to_s ||= [@parent.to_s, @description].join(' ').strip; end
23
- def context(description, &block) Protest.context(description, self, &block); end
24
24
 
25
+ def context(description, &block) Protest.context(description, @reporter, self, &block); end
25
26
  def asserts(description, &block) new_assertion(Assertion, description, &block); end
26
27
  def denies(description, &block) new_assertion(Denial, description, &block); end
27
28
 
28
- def run(report)
29
+ def report
29
30
  assertions.each do |assertion|
30
- begin
31
- result = assertion.run(self)
32
- rescue Protest::Failure => e
33
- result = e
34
- rescue Exception => e
35
- result = Protest::Error.new("errored with #{e}", assertion, e)
36
- ensure
37
- report.record self, result
31
+ if assertion.passed?
32
+ @reporter.passed
33
+ else
34
+ result = assertion.result.contextualize(self)
35
+ @reporter.errored(result) if assertion.error?
36
+ @reporter.failed(result) if assertion.failure?
38
37
  end
39
38
  end
40
39
  end
41
40
  private
42
41
  def new_assertion(klass, description, &block)
43
- (assertions << klass.new(description, &block)).last
42
+ (assertions << klass.new(description, self, &block)).last
44
43
  end
45
44
  end # Context
46
45
  end # Protest
@@ -1,34 +1,36 @@
1
1
  module Protest
2
2
  module AssertionMacros
3
- def equals(expected, &block)
4
- assert_block do |scope|
5
- actual = scope.instance_eval(&block)
6
- expected == actual || failure("expected #{expected.inspect}, not #{actual.inspect}")
7
- end
3
+ # Asserts that the result of the test equals the expected value
4
+ # asserts("test") { "foo" }.equals("foo")
5
+ def equals(expected)
6
+ expected == actual || failure("expected #{expected.inspect}, not #{actual.inspect}")
8
7
  end
9
-
10
- def nil(&block)
11
- equals(nil, &block)
8
+
9
+ # Asserts that the result of the test is nil
10
+ # asserts("test") { nil }.nil
11
+ def nil
12
+ actual.nil? || failure("expected nil, not #{actual.inspect}")
12
13
  end
13
14
 
14
- def raises(expected, &block)
15
- assert_block do |scope|
16
- begin
17
- scope.instance_eval(&block)
18
- rescue Exception => e
19
- failure("should have raised #{expected}, not #{e.class}") unless expected == e.class
20
- else
21
- failure("should have raised #{expected}, but raised nothing")
22
- end
23
- end
15
+ # Asserts that the test raises the expected Exception
16
+ # asserts("test") { raise My::Exception }.raises(My::Exception)
17
+ def raises(expected)
18
+ failure("should have raised #{expected}, but raised nothing") unless raised
19
+ failure("should have raised #{expected}, not #{error.class}") unless expected == raised.class
20
+ @raised = nil
24
21
  end
25
-
26
- def matches(expected, &block)
22
+
23
+ # Asserts that the result of the test equals matches against the proved expression
24
+ # asserts("test") { "12345" }.matches(/\d+/)
25
+ def matches(expected)
27
26
  expected = %r[#{Regexp.escape(expected)}] if expected.kind_of?(String)
28
- assert_block do |scope|
29
- actual = scope.instance_eval(&block)
30
- actual =~ expected || failure("expected #{expected.inspect} to match #{actual.inspect}")
31
- end
27
+ actual =~ expected || failure("expected #{expected.inspect} to match #{actual.inspect}")
28
+ end
29
+
30
+ # Asserts that the result of the test is an object that is a kind of the expected type
31
+ # asserts("test") { "foo" }.kind_of(String)
32
+ def kind_of(expected)
33
+ actual.kind_of?(expected) || failure("expected kind of #{expected}, not #{actual.inspect}")
32
34
  end
33
35
  end # AssertionMacros
34
36
  end # Protest
@@ -1,32 +1,30 @@
1
1
  module Protest
2
2
  class Report
3
- attr_reader :bad_results, :passes, :failures, :errors
3
+ attr_reader :bad_results, :passes, :failures, :errors, :time_taken
4
4
  def initialize
5
5
  @bad_results = []
6
- @passes, @failures, @errors = 0, 0 ,0
6
+ @passes, @failures, @errors, @time_taken = 0, 0, 0, 0.0
7
7
  end
8
8
 
9
9
  def passed?; failures + errors == 0; end
10
10
  def assertions; passes + failures + errors; end
11
11
 
12
- def start; @start = Time.now; end
13
- def stop; @stop = Time.now; end
14
- def time_taken; @stop - @start; end
12
+ def time(&block)
13
+ @start = Time.now
14
+ yield
15
+ @time_taken += (Time.now - @start).to_f
16
+ end
17
+
18
+ def passed; @passes += 1; end
15
19
 
16
- def record(context, object)
17
- case object
18
- when Protest::Error then
19
- @bad_results << [context, object]
20
- @errors += 1
21
- errored
22
- when Protest::Failure then
23
- @bad_results << [context, object]
24
- @failures += 1
25
- failed
26
- else
27
- @passes += 1
28
- passed
29
- end
20
+ def failed(failure)
21
+ @failures += 1
22
+ @bad_results << failure
23
+ end
24
+
25
+ def errored(error)
26
+ @errors += 1
27
+ @bad_results << error
30
28
  end
31
29
  end # Report
32
30
 
@@ -36,29 +34,46 @@ module Protest
36
34
  @writer ||= STDOUT
37
35
  end
38
36
 
39
- def passed; @writer.print('.'); end
40
- def failed; @writer.print('F'); end
41
- def errored; @writer.print('E'); end
37
+ def passed
38
+ super
39
+ @writer.print('.')
40
+ end
41
+
42
+ def failed(failure)
43
+ super
44
+ @writer.print('F')
45
+ end
46
+
47
+ def errored(error)
48
+ super
49
+ @writer.print('E')
50
+ end
42
51
 
43
52
  def results
44
53
  @writer.puts "\n\n"
45
- bad_results.each_with_index do |recorded, idx|
46
- ctx, failure = recorded
47
- @writer.puts "#%d - %s asserted %s: %s" % [idx + 1, ctx.to_s, failure.assertion.to_s, failure.to_s]
48
- @writer.puts " " + failure.backtrace.join("\n ") + "\n\n"
49
- end
54
+ print_result_stack
50
55
  format = "%d assertions, %d failures, %d errors in %s seconds"
51
56
  @writer.puts format % [assertions, failures, errors, ("%0.6f" % time_taken)]
52
57
  end
53
58
  private
54
- def bad_results_stack
59
+ def print_result_stack
60
+ bad_results.each_with_index do |result, idx|
61
+ @writer.puts render_result(idx + 1, result)
62
+ @writer.puts " " + result.backtrace.join("\n ") + "\n\n"
63
+ end
64
+ end
65
+
66
+ def render_result(idx, result)
67
+ format_args = [idx, result.context.to_s, result.assertion.to_s, result.to_s]
68
+ "#%d - %s asserts %s: %s" % format_args
55
69
  end
56
- end
70
+ end # TextReport
57
71
 
58
72
  class NilReport < Report
59
73
  def passed; end
60
- def failed; end
61
- def errored; end
74
+ def failed(failure); end
75
+ def errored(error); end
62
76
  def results; end
63
- end
77
+ def time(&block); yield; end
78
+ end # NilReport
64
79
  end # Protest
data/lib/protest.rb CHANGED
@@ -10,9 +10,11 @@ module Protest
10
10
  @contexts ||= []
11
11
  end
12
12
 
13
- def self.context(description, parent = nil, &block)
14
- context = Context.new(description, parent)
15
- context.instance_eval(&block)
13
+ def self.context(description, reporter = nil, parent = nil, &block)
14
+ reporter ||= self.reporter
15
+ context = Context.new(description, reporter, parent)
16
+ reporter.time { context.instance_eval(&block) }
17
+ context.report # Results get buffered this way, not necessarily the best
16
18
  (contexts << context).last
17
19
  end
18
20
 
@@ -20,29 +22,35 @@ module Protest
20
22
  contexts.delete(context)
21
23
  end
22
24
 
23
- def self.run(report=nil)
24
- report ||= TextReport.new
25
- report.start
26
- @contexts.each { |context| context.run(report) }
27
- report.stop
28
- report.results
29
- at_exit { exit false unless report.passed? }
25
+ def self.report
26
+ reporter.results
27
+ at_exit { exit false unless reporter.passed? }
30
28
  end
31
29
 
30
+ #
31
+ # Reporter
32
+
33
+ def self.reporter; @reporter ||= TextReport.new; end
34
+ def self.reporter=(report); @reporter = report; end
35
+
32
36
  #
33
37
  # Exception
34
38
 
35
39
  class Failure < Exception
36
- attr_reader :assertion
40
+ attr_reader :assertion, :context
37
41
  def initialize(message, assertion)
38
42
  super(message)
39
43
  @assertion = assertion
40
44
  end
45
+ def contextualize(ctx)
46
+ @context = ctx
47
+ self
48
+ end
41
49
  end
42
50
  class Error < Failure
43
- def initialize(message, assertion, e)
51
+ def initialize(message, assertion, error)
44
52
  super(message, assertion)
45
- set_backtrace(e.backtrace)
53
+ set_backtrace(error.backtrace)
46
54
  end
47
55
  end
48
56
  end # Protest
data/protest.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "protest"
3
- s.version = "0.0.5"
4
- s.date = "2009-06-29"
3
+ s.version = "0.0.6"
4
+ s.date = "2009-07-05"
5
5
  s.summary = "An extremely fast, expressive, and context-driven unit-testing framework"
6
6
  s.email = %w[gus@gusg.us]
7
7
  s.homepage = "http://github.com/thumblemonks/protest"
@@ -1,54 +1,79 @@
1
1
  require 'protest'
2
2
 
3
- fake_object = Object.new
3
+ fake_context = Object.new
4
4
 
5
5
  context "basic assertion:" do
6
- asserts("its description").equals("i will pass") do
7
- Protest::Assertion.new("i will pass").to_s
6
+ asserts("its description") do
7
+ Protest::Assertion.new("i will pass", fake_context).to_s
8
+ end.equals("i will pass")
9
+
10
+ asserts("passed? if assertion returns true") do
11
+ Protest::Assertion.new("i will pass", fake_context) { true }.passed?
12
+ end
13
+
14
+ asserts("failure? when assertion does not pass") do
15
+ Protest::Assertion.new("i will pass", fake_context) { false }.failure?
8
16
  end
9
17
 
10
- asserts("true is expected") { Protest::Assertion.new("i will pass") { true }.run(fake_object) }
18
+ asserts("error? when an unexpected Exception is raised") do
19
+ Protest::Assertion.new("error", fake_context) { raise Exception, "blah" }.error?
20
+ end
21
+ end
11
22
 
12
- asserts("a Failure if not true").raises(Protest::Failure) do
13
- Protest::Assertion.new("i will pass") { false }.run(fake_object)
23
+ context "basic denial:" do
24
+ asserts("false assertion passes") do
25
+ Protest::Denial.new("i will pass", fake_context) { false }.passed?
14
26
  end
15
27
 
16
- asserts("an Exception error is thrown").raises(Exception) do
17
- Protest::Assertion.new("error") { raise Exception, "blah" }.run(fake_object)
28
+ asserts("true assertion fails") do
29
+ Protest::Denial.new("i will not pass", fake_context) { true }.failure?
18
30
  end
19
31
  end # basic assertion
20
32
 
21
33
  context "equals assertion:" do
22
34
  asserts("results equals expectation") do
23
- Protest::Assertion.new("i will pass").equals("foo bar") { "foo bar" }.run(fake_object)
35
+ Protest::Assertion.new("i will pass", fake_context) { "foo bar" }.equals("foo bar")
24
36
  end
25
37
 
26
- asserts("a Failure if results don't equal eachother").raises(Protest::Failure) do
27
- Protest::Assertion.new("failure").equals("foo") { "bar" }.run(fake_object)
28
- end
38
+ asserts("a Failure if results don't equal eachother") do
39
+ Protest::Assertion.new("failure", fake_context) { "bar" }.equals("foo")
40
+ end.kind_of(Protest::Failure)
41
+
42
+ asserts("a non-matching string is a good thing when in denial") do
43
+ Protest::Denial.new("pass", fake_context) { "bar" }.equals("foo")
44
+ end.nil
29
45
  end # equals assertion
30
46
 
31
47
  context "nil assertion:" do
32
- asserts("actual result is nil") { Protest::Assertion.new("foo").nil { nil }.run(fake_object) }
33
- asserts("a Failure if not nil").raises(Protest::Failure) do
34
- Protest::Assertion.new("foo").nil { "a" }.run(fake_object)
35
- end
48
+ asserts("actual result is nil") { Protest::Assertion.new("foo", fake_context) { nil }.nil }
49
+ asserts("a Failure if not nil") do
50
+ Protest::Assertion.new("foo", fake_context) { "a" }.nil
51
+ end.kind_of(Protest::Failure)
36
52
  end # nil assertion
37
53
 
54
+ context "raises assertion:" do
55
+ asserts("an Exception is raised") { raise Exception }.raises(Exception)
56
+ end # maching assertion
57
+
38
58
  context "matching assertion:" do
39
- asserts("actual result matches expression").equals(0) do
40
- Protest::Assertion.new("foo").matches(%r[.]) { "a" }.run(fake_object)
41
- end
42
- asserts("a Failure if not nil").raises(Protest::Failure) do
43
- Protest::Assertion.new("foo").matches(%r[.]) { "" }.run(fake_object)
44
- end
45
- asserts("string matches string").equals(0) do
46
- Protest::Assertion.new("foo").matches("a") { "a" }.run(fake_object)
47
- end
59
+ asserts("actual result matches expression") do
60
+ Protest::Assertion.new("foo", fake_context) { "a" }.matches(%r[.])
61
+ end.equals(0)
62
+
63
+ asserts("a Failure if not nil") do
64
+ Protest::Assertion.new("foo", fake_context) { "" }.matches(%r[.])
65
+ end.kind_of(Protest::Failure)
66
+
67
+ asserts("string matches string") do
68
+ Protest::Assertion.new("foo", fake_context) { "a" }.matches("a")
69
+ end.equals(0)
48
70
  end # maching assertion
49
71
 
50
- context "a denial" do
51
- asserts("that a passing assertion evaluates to false") do
52
- Protest::Denial.new("foo") { false }.run(fake_object)
72
+ context "kind_of assertion:" do
73
+ asserts("result is kind of String") do
74
+ Protest::Assertion.new("foo", fake_context) { "a" }.kind_of(String)
53
75
  end
54
- end # a denial
76
+ asserts("a Failure if not a kind of String") do
77
+ Protest::Assertion.new("foo", fake_context) { 0 }.kind_of(String)
78
+ end.kind_of(Protest::Failure)
79
+ end # kind_of assertion
data/test/context_test.rb CHANGED
@@ -3,30 +3,35 @@ require 'stringio'
3
3
 
4
4
  context "any context" do
5
5
  setup do
6
- @context = Protest::Context.new("a")
6
+ @reporter = Protest::NilReport.new
7
+ @context = Protest::Context.new("a", @reporter)
7
8
  end
8
9
 
9
- denies("two contexts with same name are the same").equals(@context) { Protest::Context.new("a") }
10
+ # denies("two contexts with same name are the same").equals(@context) { Protest::Context.new("a") }
10
11
 
11
12
  context "that doesn't have passing tests" do
12
13
  setup do
13
- @report = Protest::NilReport.new
14
14
  @context.asserts("a") { true }
15
15
  @context.asserts("b") { false }
16
16
  @context.asserts("c") { raise Exception, "blah" }
17
- @context.run(@report)
18
17
  end
19
18
 
20
- asserts("that passes are disctinct").equals(1) { @report.passes }
21
- asserts("that failures are captured").equals(1) { @report.failures }
22
- asserts("that unexpected errors are captured").equals(2) { @report.errors }
19
+ asserts("that passes are disctinct") { @reporter.passes }.equals(1)
20
+ asserts("that failures are captured") { @reporter.failures }.equals(1)
21
+ asserts("that unexpected errors are captured") { @reporter.errors }.equals(1)
23
22
  end # that doesn't have passing tests
24
23
  end # any context
25
24
 
25
+ context "when denying things" do
26
+ denies("true is false") { false }
27
+ denies("bar equals foo") { "bar" }.equals("foo")
28
+ denies("bar matches only digits") { "bar" }.matches(/^\d+$/)
29
+ end
30
+
26
31
  #
27
32
  # Test Context
28
33
 
29
- test_context = context "foo" do
34
+ test_context = context("foo", Protest::NilReport.new) do
30
35
  setup { @test_counter = 0 }
31
36
  asserts("a block returns true") { @test_counter += 1; true }
32
37
  asserts("another block returns true") { @test_counter += 1; true }
@@ -34,20 +39,19 @@ end # A CONTEXT THAT IS DEQUEUED
34
39
 
35
40
  context "test context" do
36
41
  setup { Protest.dequeue_context(test_context) }
37
- asserts("context description").equals("foo") { test_context.to_s }
38
- asserts("assertion count").equals(2) { test_context.assertions.length }
42
+ asserts("context description") { test_context.to_s }.equals("foo")
43
+ asserts("assertion count") { test_context.assertions.length }.equals(2)
39
44
 
40
- asserts("setup runs only once").equals(2) do
41
- test_context.run(Protest::NilReport.new)
45
+ asserts("setup runs only once") do
42
46
  test_context.instance_variable_get(:@test_counter)
43
- end
47
+ end.equals(2)
44
48
  end
45
49
 
46
50
  #
47
51
  # Nested Context
48
52
 
49
53
  inner_nested_context, other_nested_context = nil, nil
50
- nested_context = context "foo" do
54
+ nested_context = context("foo", Protest::NilReport.new) do
51
55
  setup do
52
56
  @test_counter = 0
53
57
  @foo = "bar"
@@ -65,19 +69,16 @@ context "nested context" do
65
69
  setup do
66
70
  [nested_context, inner_nested_context, other_nested_context].each do |c|
67
71
  Protest.dequeue_context(c)
68
- c.run(Protest::NilReport.new)
69
72
  end
70
73
  end
71
74
 
72
- asserts("inner context inherits parent context setup").equals(10) do
75
+ asserts("inner context inherits parent context setup") do
73
76
  inner_nested_context.instance_variable_get(:@test_counter)
74
- end
77
+ end.equals(10)
75
78
 
76
- asserts("nested context name").equals("foo baz") do
77
- inner_nested_context.to_s
78
- end
79
+ asserts("nested context name") { inner_nested_context.to_s }.equals("foo baz")
79
80
 
80
- asserts("inner context without setup is still bootstrapped").equals("bar") do
81
+ asserts("inner context without setup is still bootstrapped") do
81
82
  other_nested_context.instance_variable_get(:@foo)
82
- end
83
+ end.equals("bar")
83
84
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thumblemonks-protest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Knowlden
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-29 00:00:00 -07:00
12
+ date: 2009-07-05 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15