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 +260 -18
- data/Rakefile +6 -1
- data/lib/protest/assertion.rb +44 -16
- data/lib/protest/context.rb +11 -12
- data/lib/protest/macros.rb +26 -24
- data/lib/protest/report.rb +47 -32
- data/lib/protest.rb +21 -13
- data/protest.gemspec +2 -2
- data/test/assertion_test.rb +54 -29
- data/test/context_test.rb +23 -22
- metadata +2 -2
data/README.markdown
CHANGED
@@ -1,42 +1,284 @@
|
|
1
1
|
# Protest
|
2
2
|
|
3
|
-
An extremely fast
|
3
|
+
An extremely fast, expressive, and context-driven unit-testing framework.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
12
|
-
|
50
|
+
def test_email_address_is_nil
|
51
|
+
assert_nil @user.email
|
13
52
|
end
|
53
|
+
end
|
14
54
|
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
138
|
+
I really, really hate slow test suites.
|
31
139
|
|
32
|
-
|
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
|
-
|
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
|
-
|
270
|
+
## TODO
|
39
271
|
|
40
|
-
|
272
|
+
TONS OF STUFF
|
41
273
|
|
42
|
-
|
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.
|
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
|
data/lib/protest/assertion.rb
CHANGED
@@ -1,31 +1,59 @@
|
|
1
1
|
module Protest
|
2
2
|
|
3
3
|
class Assertion
|
4
|
-
|
4
|
+
attr_reader :raised
|
5
|
+
def initialize(description, target, &block)
|
5
6
|
@description = description
|
6
|
-
|
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
|
13
|
-
@
|
14
|
-
|
10
|
+
def actual
|
11
|
+
@default_failure = @failure = nil if @default_failure
|
12
|
+
@actual
|
15
13
|
end
|
16
14
|
|
17
|
-
def
|
18
|
-
|
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
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
|
data/lib/protest/context.rb
CHANGED
@@ -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
|
29
|
+
def report
|
29
30
|
assertions.each do |assertion|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
result =
|
34
|
-
|
35
|
-
|
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
|
data/lib/protest/macros.rb
CHANGED
@@ -1,34 +1,36 @@
|
|
1
1
|
module Protest
|
2
2
|
module AssertionMacros
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
data/lib/protest/report.rb
CHANGED
@@ -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
|
13
|
-
|
14
|
-
|
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
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
15
|
-
context.
|
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.
|
24
|
-
|
25
|
-
|
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,
|
51
|
+
def initialize(message, assertion, error)
|
44
52
|
super(message, assertion)
|
45
|
-
set_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.
|
4
|
-
s.date = "2009-
|
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"
|
data/test/assertion_test.rb
CHANGED
@@ -1,54 +1,79 @@
|
|
1
1
|
require 'protest'
|
2
2
|
|
3
|
-
|
3
|
+
fake_context = Object.new
|
4
4
|
|
5
5
|
context "basic assertion:" do
|
6
|
-
asserts("its description")
|
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("
|
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
|
-
|
13
|
-
|
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("
|
17
|
-
Protest::
|
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"
|
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")
|
27
|
-
Protest::Assertion.new("failure")
|
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")
|
33
|
-
asserts("a Failure if not nil")
|
34
|
-
Protest::Assertion.new("foo")
|
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")
|
40
|
-
Protest::Assertion.new("foo")
|
41
|
-
end
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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 "
|
51
|
-
asserts("
|
52
|
-
Protest::
|
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
|
-
|
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
|
-
@
|
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")
|
21
|
-
asserts("that failures are captured")
|
22
|
-
asserts("that unexpected errors are captured")
|
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
|
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")
|
38
|
-
asserts("assertion count")
|
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")
|
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
|
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")
|
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")
|
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")
|
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.
|
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-
|
12
|
+
date: 2009-07-05 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|