lookout 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data/README +645 -0
  2. data/Rakefile +9 -0
  3. data/lib/lookout.rb +34 -0
  4. data/lib/lookout/aphonic.rb +40 -0
  5. data/lib/lookout/benchmark.rb +11 -0
  6. data/lib/lookout/diff.rb +10 -0
  7. data/lib/lookout/diff/algorithms.rb +5 -0
  8. data/lib/lookout/diff/algorithms/difflib.rb +38 -0
  9. data/lib/lookout/diff/algorithms/difflib/position.rb +92 -0
  10. data/lib/lookout/diff/algorithms/difflib/position/to.rb +47 -0
  11. data/lib/lookout/diff/formats.rb +7 -0
  12. data/lib/lookout/diff/formats/hash.rb +53 -0
  13. data/lib/lookout/diff/formats/inline.rb +39 -0
  14. data/lib/lookout/diff/formats/unified.rb +57 -0
  15. data/lib/lookout/diff/group.rb +61 -0
  16. data/lib/lookout/diff/groups.rb +34 -0
  17. data/lib/lookout/diff/match.rb +36 -0
  18. data/lib/lookout/diff/operation.rb +33 -0
  19. data/lib/lookout/diff/operations.rb +36 -0
  20. data/lib/lookout/diff/operations/delete.rb +9 -0
  21. data/lib/lookout/diff/operations/equal.rb +27 -0
  22. data/lib/lookout/diff/operations/insert.rb +9 -0
  23. data/lib/lookout/diff/operations/replace.rb +9 -0
  24. data/lib/lookout/diff/range.rb +91 -0
  25. data/lib/lookout/equality.rb +178 -0
  26. data/lib/lookout/expectation.rb +50 -0
  27. data/lib/lookout/expectations.rb +62 -0
  28. data/lib/lookout/expectations/behavior.rb +20 -0
  29. data/lib/lookout/expectations/state.rb +29 -0
  30. data/lib/lookout/mock.rb +18 -0
  31. data/lib/lookout/mock/method.rb +70 -0
  32. data/lib/lookout/mock/method/arguments.rb +33 -0
  33. data/lib/lookout/mock/method/arguments/any.rb +11 -0
  34. data/lib/lookout/mock/method/arguments/anything.rb +11 -0
  35. data/lib/lookout/mock/method/arguments/list.rb +15 -0
  36. data/lib/lookout/mock/method/arguments/none.rb +11 -0
  37. data/lib/lookout/mock/method/arguments/one.rb +11 -0
  38. data/lib/lookout/mock/method/calls.rb +11 -0
  39. data/lib/lookout/mock/method/calls/class.rb +21 -0
  40. data/lib/lookout/mock/method/calls/exactly.rb +28 -0
  41. data/lib/lookout/mock/method/calls/instance.rb +25 -0
  42. data/lib/lookout/mock/method/calls/lower.rb +22 -0
  43. data/lib/lookout/mock/method/calls/upper.rb +22 -0
  44. data/lib/lookout/mock/methods.rb +12 -0
  45. data/lib/lookout/mock/object.rb +12 -0
  46. data/lib/lookout/object.rb +11 -0
  47. data/lib/lookout/output.rb +21 -0
  48. data/lib/lookout/rake/tasks.rb +36 -0
  49. data/lib/lookout/rake/tasks/gem.rb +49 -0
  50. data/lib/lookout/rake/tasks/tags.rb +16 -0
  51. data/lib/lookout/rake/tasks/test.rb +46 -0
  52. data/lib/lookout/rake/tasks/test/loader.rb +19 -0
  53. data/lib/lookout/recorder.rb +45 -0
  54. data/lib/lookout/recorder/not.rb +11 -0
  55. data/lib/lookout/recorder/tape.rb +21 -0
  56. data/lib/lookout/recorders.rb +6 -0
  57. data/lib/lookout/recorders/reception.rb +47 -0
  58. data/lib/lookout/recorders/state.rb +35 -0
  59. data/lib/lookout/result.rb +23 -0
  60. data/lib/lookout/results.rb +46 -0
  61. data/lib/lookout/results/error.rb +18 -0
  62. data/lib/lookout/results/error/exception.rb +36 -0
  63. data/lib/lookout/results/error/exception/backtrace.rb +41 -0
  64. data/lib/lookout/results/failure.rb +16 -0
  65. data/lib/lookout/results/failures.rb +6 -0
  66. data/lib/lookout/results/failures/behavior.rb +4 -0
  67. data/lib/lookout/results/failures/state.rb +4 -0
  68. data/lib/lookout/results/fulfilled.rb +9 -0
  69. data/lib/lookout/runners.rb +5 -0
  70. data/lib/lookout/runners/console.rb +22 -0
  71. data/lib/lookout/stub.rb +16 -0
  72. data/lib/lookout/stub/method.rb +105 -0
  73. data/lib/lookout/stub/methods.rb +18 -0
  74. data/lib/lookout/stub/object.rb +11 -0
  75. data/lib/lookout/ui.rb +7 -0
  76. data/lib/lookout/ui/console.rb +36 -0
  77. data/lib/lookout/ui/silent.rb +12 -0
  78. data/lib/lookout/version.rb +5 -0
  79. data/lib/lookout/xml.rb +17 -0
  80. data/test/unit/examples.rb +169 -0
  81. data/test/unit/lookout.rb +7 -0
  82. data/test/unit/lookout/diff.rb +4 -0
  83. data/test/unit/lookout/diff/algorithms/difflib.rb +56 -0
  84. data/test/unit/lookout/diff/algorithms/difflib/position.rb +92 -0
  85. data/test/unit/lookout/diff/algorithms/difflib/position/to.rb +12 -0
  86. data/test/unit/lookout/diff/formats/inline.rb +17 -0
  87. data/test/unit/lookout/diff/formats/unified.rb +67 -0
  88. data/test/unit/lookout/diff/group.rb +4 -0
  89. data/test/unit/lookout/diff/groups.rb +102 -0
  90. data/test/unit/lookout/diff/match.rb +5 -0
  91. data/test/unit/lookout/diff/operations.rb +22 -0
  92. data/test/unit/lookout/diff/operations/delete.rb +45 -0
  93. data/test/unit/lookout/diff/operations/equal.rb +45 -0
  94. data/test/unit/lookout/diff/operations/insert.rb +45 -0
  95. data/test/unit/lookout/diff/operations/replace.rb +45 -0
  96. data/test/unit/lookout/diff/range.rb +50 -0
  97. data/test/unit/lookout/equality.rb +113 -0
  98. data/test/unit/lookout/expectation.rb +39 -0
  99. data/test/unit/lookout/expectations.rb +58 -0
  100. data/test/unit/lookout/expectations/behavior.rb +35 -0
  101. data/test/unit/lookout/expectations/state.rb +29 -0
  102. data/test/unit/lookout/mock.rb +4 -0
  103. data/test/unit/lookout/mock/method.rb +143 -0
  104. data/test/unit/lookout/mock/method/arguments.rb +57 -0
  105. data/test/unit/lookout/mock/method/arguments/any.rb +11 -0
  106. data/test/unit/lookout/recorder.rb +11 -0
  107. data/test/unit/lookout/results.rb +30 -0
  108. data/test/unit/lookout/results/error.rb +7 -0
  109. data/test/unit/lookout/results/failures/behavior.rb +7 -0
  110. data/test/unit/lookout/results/failures/state.rb +7 -0
  111. data/test/unit/lookout/results/fulfilled.rb +7 -0
  112. data/test/unit/lookout/runners/console.rb +4 -0
  113. data/test/unit/lookout/stub.rb +4 -0
  114. data/test/unit/lookout/stub/method.rb +48 -0
  115. data/test/unit/lookout/ui/formatters/exception.rb +5 -0
  116. data/test/unit/lookout/ui/formatters/exception/backtrace.rb +11 -0
  117. data/test/unit/lookout/xml.rb +55 -0
  118. metadata +496 -0
data/README ADDED
@@ -0,0 +1,645 @@
1
+ # Lookout #
2
+
3
+ Lookout is a lightweight unit testing framework. Tests (expectations) can be
4
+ written as follows
5
+
6
+ expect 2 do
7
+ 1 + 1
8
+ end
9
+
10
+ expect NoMethodError do
11
+ Object.invalid_method_call
12
+ end
13
+
14
+ Lookout is designed to encourage – force, even – unit testing best practices
15
+ such as
16
+
17
+ * Setting up only one expectation per test
18
+ * Not setting expectations on non-public APIs
19
+ * Test isolation
20
+
21
+ This is done by
22
+
23
+ * Only allowing one expectation to be set per test
24
+ * Providing no (additonal) way of accessing private state
25
+ * Providing no setup and teardown methods, nor a method of providing test
26
+ helpers
27
+
28
+ Other important points are
29
+
30
+ * A unified syntax for setting up both state-based and behavior-based
31
+ expectations
32
+ * A focus on code readability by providing no mechanism for describing an
33
+ expectation other than the code in the expectation itself
34
+
35
+ The way Lookout works has been heavily influenced by [expectations], by [Jay
36
+ Fields][]. The code base was once also heavily based on [expectations],
37
+ based at Subversion [revision r76][]. A lot has happened since then and all of the
38
+ work past that revision are due to [Nikolai Weibull][].
39
+
40
+ [expectations]: http://expectations.rubyforge.org
41
+ [Jay Fields]: http://blog.jayfields.com
42
+ [revision r76]: https://github.com/now/lookout/commit/537bedf3e5b3eb4b31c066b3266f42964ac35ebe
43
+ [Nikolai Weibull]: http://bitwi.se
44
+
45
+
46
+ ## Installation ##
47
+
48
+ Install Lookout with
49
+
50
+ % gem install lookout
51
+
52
+
53
+ ## Usage ##
54
+
55
+ Lookout allows you to set expectations on an object’s state or behavior. We’ll
56
+ begin by looking at state expectations and then take a look at expectations on
57
+ behavior.
58
+
59
+ ### Expectations on State ###
60
+
61
+ An expectation can be made on the result of a computation:
62
+
63
+ expect 2 do
64
+ 1 + 1
65
+ end
66
+
67
+ Most objects, in fact, have their state expectations checked by invoking `#==`
68
+ on the expected value with the result as its argument.
69
+
70
+ Checking that a result is within a given range is also simple:
71
+
72
+ expect 0.099..0.101 do
73
+ 0.4 - 0.3
74
+ end
75
+
76
+ Here, the more general `#===` is being used on the `Range`.
77
+
78
+ `Strings` of course match against `Strings`:
79
+
80
+ expect 'ab' do
81
+ 'abc'[0..1]
82
+ end
83
+
84
+ but we can also match a `String` against a `Regexp`:
85
+
86
+ expect %r{a substring} do
87
+ 'a string with a substring'
88
+ end
89
+
90
+ (Please note the use of `%r{…}` to avoid warnings that will be generated when
91
+ Ruby parses `expect /…/`.)
92
+
93
+ Checking that the result includes a certain module is done by expecting the
94
+ `Module`.
95
+
96
+ expect Enumerable do
97
+ []
98
+ end
99
+
100
+ This, due to the nature of Ruby, of course also works for classes (as they are
101
+ also modules):
102
+
103
+ expect String do
104
+ 'a string'
105
+ end
106
+
107
+ This doesn’t hinder us from expecting the actual `Module` itself:
108
+
109
+ expect Enumerable do
110
+ Enumerable
111
+ end
112
+
113
+ As you may have figured out yourself, this is accomplished by first trying
114
+ `#==` and, if it returns `false`, then trying `#===` on the expected `Module`.
115
+ This is also true of `Ranges` and `Regexps`.
116
+
117
+ Truthfulness is expected with `true` and `false`:
118
+
119
+ expect true do
120
+ 1
121
+ end
122
+
123
+ expect false do
124
+ nil
125
+ end
126
+
127
+ Results equaling `true` or `false` are slightly different:
128
+
129
+ expect TrueClass do
130
+ true
131
+ end
132
+
133
+ expect FalseClass do
134
+ false
135
+ end
136
+
137
+ The rationale for this is that you should only care if the result of a
138
+ computation evaluates to a value that Ruby considers to be either true or
139
+ false, not the exact literals `true` or `false`.
140
+
141
+ Expecting output on an IO object is also common:
142
+
143
+ expect output("abc\ndef\n") do |io|
144
+ io.puts 'abc', 'def'
145
+ end
146
+
147
+ This can be used to capture the output of a formatter that takes an output
148
+ object as a parameter.
149
+
150
+ You should always be expecting errors from – and in, but that’s a different
151
+ story – your code:
152
+
153
+ expect NoMethodError do
154
+ Object.no_method
155
+ end
156
+
157
+ Often, not only the type of the error, but its description, is important to
158
+ check:
159
+
160
+ expect StandardError.new('message') do
161
+ raise StandardError.new('message')
162
+ end
163
+
164
+ As with `Strings`, `Regexps` can be used to check the error description:
165
+
166
+ expect StandardError.new(/mess/) do
167
+ raise StandardError.new('message')
168
+ end
169
+
170
+ (Note that some of Ruby’s built-in error classes have slightly complicated
171
+ behavior and will not allow you to pass a `Regexp` as a parameter. `NameError`
172
+ is such a class. This may warrant further investigation into whether or not
173
+ this is a bug, but I’ll leave that up to the reader to decide.)
174
+
175
+ Lookout further provides a fluent way of setting up expectations on boolean
176
+ results. An object can “be”
177
+
178
+ expect Class.new{ attr_accessor :running; }.new.to.be.running do |process|
179
+ process.running = true
180
+ end
181
+
182
+ or “not be”
183
+
184
+ expect Class.new{ attr_accessor :running; }.new.not.to.be.running do |process|
185
+ process.running = false
186
+ end
187
+
188
+ or to “have”
189
+
190
+ expect Class.new{ attr_accessor :finished; }.new.to.have.finished do |process|
191
+ process.finished = true
192
+ end
193
+
194
+ or “not have”
195
+
196
+ expect Class.new{ attr_accessor :finished; }.new.not.to.have.finished do |process|
197
+ process.finished = false
198
+ end
199
+
200
+ On the same note
201
+
202
+ expect nil.to.be.nil?
203
+
204
+ and
205
+
206
+ expect Object.new.not.to.be.nil?
207
+
208
+ As not every boolean method “is” or “has” you can even
209
+
210
+ expect nil.to.respond_to? :nil?
211
+
212
+ The rules here are that all `Objects` respond to `#to`. After `#to` you may
213
+ call
214
+
215
+ * `#not`
216
+ * `#be`
217
+ * `#have`
218
+ * Any method whose name ends with `?`
219
+
220
+ A call to `#not` must be followed by a call to one of the three alternatives
221
+ that follow it in the list. `#Be` and `#Have` must be followed by a call to a
222
+ method.
223
+
224
+ ### Expectations on Behavior ###
225
+
226
+ We expect our objects to be on their best behavior. Lookout allows you to make
227
+ sure that they are.
228
+
229
+ Mocks let use verify that a method is called in the way that we expect it to
230
+ be:
231
+
232
+ expect mock.to.receive.dial('2125551212').twice do |phone|
233
+ phone.dial('2125551212')
234
+ phone.dial('2125551212')
235
+ end
236
+
237
+ Here, `#mock` creates a mock object, an object that doesn’t respond to
238
+ anything unless you tell it to. We tell it to expect to receive a call to
239
+ `#dail` with `'2125551212'` as its formal argument, and we expect it to receive
240
+ it twice. The mock object is then passed in to the block so that the
241
+ expectations placed upon it can be fulfilled.
242
+
243
+ Sometimes we only want to make sure that a method is called in the way that we
244
+ expect it to be, but we don’t care if any other methods are called on the
245
+ object. A stub object, created with `#stub`, expects any method and returns a
246
+ stub object that, again, expects any method, and thus fits the bill.
247
+
248
+ expect stub.to.receive.dial('2125551212').twice do |phone|
249
+ phone.dial('2125551212')
250
+ phone.hangup
251
+ phone.dial('2125551212')
252
+ end
253
+
254
+ We can also use stubs without any expectations on them:
255
+
256
+ expect 3 do
257
+ s = stub(:a => 1, :b => 2)
258
+ s.a + s.b
259
+ end
260
+
261
+ and we can stub out a specific method on an object:
262
+
263
+ expect 'def' do
264
+ a = 'abc'
265
+ stub(a).to_str{ 'def' }
266
+ a.to_str
267
+ end
268
+
269
+ You don’t have to use a mock object to verify that a method is called:
270
+
271
+ expect Object.to.receive.deal do
272
+ Object.deal
273
+ end
274
+
275
+ As you have figured out by now, the expected method call is set up by calling
276
+ `#receive` after `#to`. `#Receive` is followed by a call to the method to
277
+ expect with any expected arguments. The body of the mocked method can be given
278
+ as the block to the method. Finally, an expected invocation count may follow
279
+ the method. Let’s look at this formal specification in more detail.
280
+
281
+ The expected method arguments may be given in a variety of ways. Let’s
282
+ introduce them by giving some examples:
283
+
284
+ expect mock.to.receive.a do |m|
285
+
286
+ end
287
+
288
+ Here, the method `#a` must be called with any number of arguments. It may be
289
+ called any number of times, but it must be called at least once.
290
+
291
+ If a method must receive exactly one argument, you can use `arg`:
292
+
293
+ expect mock.to.receive.a(arg) do |m|
294
+
295
+ end
296
+
297
+ If a method must receive a specific argument, you can use that argument:
298
+
299
+ expect mock.to.receive.a(1..2) do |m|
300
+
301
+ end
302
+
303
+ The same matching rules apply for arguments as they do for state expectations,
304
+ so the previous example expects a call to `#a` with 1, 2, or the Range 1..2 as
305
+ an argument on `m`.
306
+
307
+ If a method must be invoked without any arguments you can use
308
+ `without_arguments`:
309
+
310
+ expect mock.to.receive.a(without_arguments) do |m|
311
+
312
+ end
313
+
314
+ You can of course use both `arg` and actual arguments:
315
+
316
+ expect mock.to.receive.a(arg, 1, arg) do |m|
317
+
318
+ end
319
+
320
+ The body of the mock method may be given as the block. Here, calling `#a` on
321
+ `m` will give the result `1`:
322
+
323
+ expect mock.to.receive.a{ 1 } do |m|
324
+
325
+ end
326
+
327
+ If no body has been given, the result will be a stub object.
328
+
329
+ There is a caveat here in that a block can’t yield in Ruby 1.8. To work around
330
+ this deficiency you have to use the `#yield` method:
331
+
332
+ expect mock.to.receive.a.yield(1) do |m|
333
+
334
+ end
335
+
336
+ Any number of values to yield upon successive calls may be given. The last
337
+ value given will be used repeatedly when all preceding values have been
338
+ consumed. It’s also important to know that values are splatted when they are
339
+ yielded.
340
+
341
+ To simulate an `#each`-like method you can use `#each`. The following horrible
342
+ example should give you an idea of how to use it.
343
+
344
+ expect Object.new.to.receive.each.each(1, 2, 3) do |o|
345
+ class << o
346
+ include Enumerable
347
+ end
348
+ o.inject{ |i, a| i + a }
349
+ end
350
+
351
+ Invocation count expectations can also be set if the default expectation of
352
+ “at least once” isn’t good enough. The following expectations are possible
353
+
354
+ * `#at_most_once`
355
+ * `#once`
356
+ * `#at_least_once`
357
+ * `#twice`
358
+
359
+ And, for a given `N`,
360
+
361
+ * `#at_most(N)`
362
+ * `#exactly(N)`
363
+ * `#at_least(N)`
364
+
365
+ Method stubs are another useful thing to have in a unit testing framework.
366
+ Sometimes you need to override a method that does something a test shouldn’t
367
+ do, like access and alter bank accounts. We can override – stub out – a method
368
+ by using the `#stub` method. Let’s assume that we have an `Account` class that
369
+ has two methods, `#slips` and `#total`. `#Slips` retrieves the bank slips that
370
+ keep track of your deposits to the `Account` from a database. `#Total` sums
371
+ the `#slips`. In the following test we want to make sure that `#total` does
372
+ what it should do without accessing the database. We therefore stub out
373
+ `#slips` and make it return something that we can easily control.
374
+
375
+ expect 6 do |m|
376
+ account = Account.new
377
+ stub(account).slips{ [1, 2, 3] }
378
+ account.total
379
+ end
380
+
381
+ As with mock methods, if no body is given, the result will be a stub object.
382
+
383
+ To make it easy to create objects with a set of stubbed methods there’s also a
384
+ convenience method:
385
+
386
+ expect 3 do
387
+ s = stub(:a => 1, :b => 2)
388
+ s.a + s.b
389
+ end
390
+
391
+ Please note that this makes it impossible to stub a method on a Hash, but you
392
+ shouldn’t be doing that anyway. In fact, you should never mock or stub methods
393
+ on value objects.
394
+
395
+ ## Integration ##
396
+
397
+ Lookout can be used from [Rake][]. Simply include the following code in your
398
+ `Rakefile`:
399
+
400
+ require 'lookout/rake/tasks'
401
+
402
+ Lookout::Rake::Tasks::Test.new
403
+
404
+ If the `:default` task hasn’t been defined it will be set to depend on the
405
+ `:test` task.
406
+
407
+ As an added bonus you can use Lookouts own [gem][RubyGems] tasks:
408
+
409
+ Lookout::Rake::Tasks::Gem.new
410
+
411
+ This provides tasks to `build`, `check`, `install`, and `push` your gem.
412
+
413
+ To use Lookout together with [Vim][], place `contrib/rakelookout.vim` in
414
+ `~/.vim/compiler` and add
415
+
416
+ compiler rakelookout
417
+
418
+ to `~/.vim/after/ftplugin/ruby.vim`. Executing `:make` from inside [Vim][]
419
+ will now run your tests and an errors and failures can be visited with
420
+ `:cnext`. Execute `:help quickfix` for additional information.
421
+
422
+ Another useful addition to your `~/.vim/after/ftplugin/ruby.vim` file may be
423
+
424
+ nnoremap <buffer> <silent> <Leader>M <Esc>:call <SID>run_test()<CR>
425
+ let b:undo_ftplugin .= ' | nunmap <buffer> <Leader>M'
426
+
427
+ function! s:run_test()
428
+ let test = expand('%')
429
+ let line = 'LINE=' . line('.')
430
+ if test =~ '^lib/'
431
+ let test = substitute(test, '^lib/', 'test/', '')
432
+ let line = ""
433
+ endif
434
+ execute 'make' 'TEST=' . shellescape(test) line
435
+ endfunction
436
+
437
+ Now, pressing `<Leader>M` will either run all tests for a given class, if the
438
+ implementation file is active, or run the test at or just before the cursor, if
439
+ the test file is active. This is useful if you’re currently receiving a lot of
440
+ errors and/or failures and want to focus on those associated with a specific
441
+ class or on a specific test.
442
+
443
+ [Rake]: http://rake.rubyforge.org
444
+ [RubyGems]: http://rubygems.org
445
+
446
+ ## Interface Design ##
447
+
448
+ The default output of Lookout can Spartanly be described as Spartan. If no
449
+ errors or failures occur, no output is generated. This is unconventional, as
450
+ unit testing frameworks tend to dump a lot of information on the user,
451
+ concerning things such as progress, test count summaries, and flamboyantly
452
+ colored text telling you that your tests passed. None of this output is
453
+ needed. Your tests should run fast enough to not require progress reports.
454
+ The lack of output provides you with the same amount of information as
455
+ reporting success. Test count summaries are only useful if you’re worried that
456
+ your tests aren’t being run, but if you worry about that, then providing such
457
+ output doesn’t really help. Testing your tests requires something beyond
458
+ reporting some arbitrary count that you would have to verify by hand anyway.
459
+
460
+ When errors or failures do occur, however, the relevant information is output
461
+ in a format that can easily be parsed by an `'errorformat'` for [Vim][] or with
462
+ [Compilation Mode][] for [Emacs][]. Diffs are generated for Strings, Arrays,
463
+ Hashes, and I/O.
464
+
465
+ [Vim]: http://www.vim.org
466
+ [Compilation Mode]: http://www.emacswiki.org/emacs/CompilationMode
467
+ [Emacs]: http://www.gnu.org/software/emacs/
468
+
469
+ ## External Design ##
470
+
471
+ Let’s now look at some of the points made in the introduction in greater
472
+ detail.
473
+
474
+ Lookout only allows you to set one expectation per test. If you’re testing
475
+ behavior with a mock, then only one method-invocation expectation can be set.
476
+ If you’re testing state, then only one result can be verified. It may seem
477
+ like this would cause unnecessary duplication between tests. While this is
478
+ certainly a possibility, when you actually begin to try to avoid such
479
+ duplication you find that you often do so by improving your interfaces. This
480
+ kind of restriction tends to encourage the use of value objects, which are easy
481
+ to test, and more focused objects, which require simpler tests, as they have
482
+ less behavior to test, per method. By keeping your interfaces focused you’re
483
+ also keeping your tests focused.
484
+
485
+ Keeping your tests focused improves, in itself, test isolation, but let’s look
486
+ at something that hinders it: setup and teardown methods. Most unit testing
487
+ frameworks encourage test fragmentation by providing setup and teardown
488
+ methods.
489
+
490
+ Setup methods create objects and, perhaps, just their behavior for a set of
491
+ tests. This means that you have to look in two places to figure out what’s
492
+ being done in a test. This may work fine for few methods with simple set-ups,
493
+ but makes things complicated when the number of tests increases and the set-up
494
+ is complex. Often, each test further adjusts the previously set-up object
495
+ before performing any verifications, further complicating the process of
496
+ figuring out what state an object has in a given test.
497
+
498
+ Teardown methods clean up after tests, perhaps by removing records from a
499
+ database or deleting files from the file-system.
500
+
501
+ The duplication that setup methods and teardown methods hope to remove is
502
+ better avoided by improving your interfaces. This can be done by providing
503
+ better set-up methods for your objects and using idioms such as [Resource
504
+ Acquisition Is Initialization][] for guaranteed clean-up, test or no test.
505
+
506
+ By not using setup and teardown methods we keep everything pertinent to a test
507
+ in the test itself, thus improving test isolation. (You also won’t [slow down
508
+ your tests][Broken Test::Unit] by keeping unnecessary state.)
509
+
510
+ Most unit test frameworks also allow you to create arbitrary test helper
511
+ methods. Lookout doesn’t. The same rationale as that that has been
512
+ crystallized in the preceding paragraphs applies. If you need helpers you’re
513
+ interface isn’t good enough. It really is as simple as that.
514
+
515
+ To clarify: there’s nothing inherently wrong with test helper methods, but they
516
+ should be general enough that they reside in their own library. The support
517
+ for mocks in Lookout is provided through a set of test helper methods that make
518
+ it easier to create mocks than it would have been without them.
519
+ [Lookout-rack][] is another example of a library providing test helper methods
520
+ (well, one of method, actually) that are very useful in testing web
521
+ applications that use [Rack][].
522
+
523
+ A final point at which some unit test frameworks try to fragment tests further
524
+ is documentation. These frameworks provide ways of describing the whats and
525
+ hows of what’s being tested, the rationale being that this will provide
526
+ documentation of both the test and the code being tested. Describing how a
527
+ stack data structure is meant to work is a common example. A stack is,
528
+ however, a rather simple data structure, so such a description provides little,
529
+ if any, additional information that can’t be extracted from the implementation
530
+ and its tests themselves. The implementation and its tests is, in fact, its
531
+ own best documentation. Taking the points made in the previous paragraphs into
532
+ account, we should already have simple, self-describing, interfaces that have
533
+ easily understood tests associated with them. Rationales for the use of a
534
+ given data structure or system-design design documentation is better suited in
535
+ separate documentation focused at describing exactly those issues.
536
+
537
+ [Resource Acquisition Is Initialization]: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
538
+ [Broken Test::Unit]: http://37signals.com/svn/posts/2742-the-road-to-faster-tests
539
+ [Lookout-rack]: http://github.com/now/lookout-rack
540
+ [Rack]: http://rack.rubyforge.org
541
+
542
+ ## Internal Design ##
543
+
544
+ The internal design of Lookout has had a couple of goals.
545
+
546
+ * As few external dependencies as possible
547
+ * As few internal dependencies as possible
548
+ * Internal extensibility provides external extensibility
549
+ * As fast load times as possible
550
+ * As high a ratio of value objects to mutable objects as possible
551
+ * Each object must have a simple, obvious name
552
+ * Use mix-ins, not inheritance for shared behavior
553
+ * As few responsibilities per object as possible
554
+ * Optimizing for speed can only be done when you have all the facts
555
+
556
+ ### External Dependencies ###
557
+
558
+ Lookout used to depend on Mocha for mocks and stubs. While benchmarking I
559
+ noticed that a method in Mocha was taking up more than 300 percent of the
560
+ runtime. It turned out that Mocha’s method for cleaning up back-traces
561
+ generated when a mock failed was doing something incredibly stupid:
562
+
563
+ backtrace.reject{ |l| Regexp.new(@lib).match(File.expand_path(l)) }
564
+
565
+ Here `@lib` is a `String` containing the path to the lib subdirectory in the
566
+ Mocha installation directory. I reported it, provided a patch five days later,
567
+ then waited. Nothing happened. 254 days later, according to [Wolfram
568
+ Alpha][254 days], half of my patch was, apparently – I say “apparently”, as I
569
+ received no notification – applied. By that time I had replaced the whole
570
+ mocking-and-stubbing subsystem and dropped the dependency.
571
+
572
+ Many Ruby developers claim that Ruby and its gems are too fast-moving for
573
+ normal package-managing systems to keep up. This is testament to the fact
574
+ that this isn’t the case and that the real problem is instead related to sloppy
575
+ practices.
576
+
577
+ Please note that I don’t want to single out the Mocha library nor its
578
+ developers. I only want to provide an example where relying on external
579
+ dependencies can be “considered harmful”.
580
+
581
+ [254 days]: http://www.wolframalpha.com/input/?i=days+between+march+17%2C+2010+and+november+26%2C+2010
582
+
583
+ ### Internal Dependencies ###
584
+
585
+ Lookout has been designed so as to keep each subsystem independent of any
586
+ other. The diff subsystem is, for example, completely decoupled from any other
587
+ part of the system as a whole and could be moved into its own library at a time
588
+ where that would be of interest to anyone. What’s perhaps more interesting is
589
+ that the diff subsystem is itself very modular. The data passes through a set
590
+ of filters that depends on what kind of diff has been requested, each filter
591
+ yielding modified data as it receives it. If you want to read some rather
592
+ functional Ruby I can highly recommend looking at the code in the
593
+ `lib/lookout/diff` directory.
594
+
595
+ This lookout on the design of the library also makes it easy to extend Lookout.
596
+ Lookout-rack was, for example, written in about four hours and about 5 of those
597
+ 240 minutes were spent on setting up the interface between the two.
598
+
599
+ ### Optimizing For Speed ###
600
+
601
+ The following paragraph is perhaps a bit personal, but might be interesting
602
+ nonetheless.
603
+
604
+ I’ve always worried about speed. The original Expectations library used
605
+ `extend` a lot to add new behavior to objects. Expectations, for example, used
606
+ to hold the result of their execution (what we now term “evaluation”) by being
607
+ extended by a module representing success, failure, or error. For the longest
608
+ time I used this same method, worrying about the increased performance cost
609
+ that creating new objects for results would incur. I finally came to a point
610
+ where I felt that the code was so simple and clean that rewriting this part of
611
+ the code for a benchmark wouldn’t take more than perhaps ten minutes. Well,
612
+ ten minutes later I had my results and they confirmed that creating new objects
613
+ wasn’t harming performance. I was very pleased.
614
+
615
+ ### Naming ###
616
+
617
+ I hate low lines (underscores). I try to avoid them in method names and I
618
+ always avoid them in file names. Since the current “best practice” in the Ruby
619
+ community is to put `BeginEndStorage` in a file called `begin_end_storage.rb`,
620
+ I only name constants using a single noun. This has had the added benefit that
621
+ classes seem to have acquired less behavior, as using a single noun doesn’t
622
+ allow you to tack on additional behavior without questioning if it’s really
623
+ appropriate to do so, given the rather limited range of interpretation for that
624
+ noun. It also seems to encourage the creation of value objects, as something
625
+ named `Range` feels a lot more like a value than `BeginEndStorage`. (To reach
626
+ object-oriented-programming Nirvana you must achieve complete value.)
627
+
628
+
629
+ ## Contributors ##
630
+
631
+ Contributors to the original expectations codebase are mentioned there. We
632
+ hope no one on that list feels left out of this list. Please [let us
633
+ know][Lookout issues] if you do.
634
+
635
+ * Nikolai Weibull
636
+
637
+ [Lookout issues]: https://github.com/now/lookout/issues
638
+
639
+
640
+ ## License ##
641
+
642
+ You may use, copy, and redistribute this library under the same [terms][] as
643
+ Ruby itself.
644
+
645
+ [terms]: http://www.ruby-lang.org/en/LICENSE.txt