SlimTest 4.6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,393 @@
1
+ How to Use ZenTest with Ruby
2
+ by Pat Eyler <pate@kohalabs.com>
3
+ http://linuxjournal.com/article.php?sid=7776
4
+ (included in this package with permission)
5
+
6
+ Refactoring and unit testing are a great pair of tools for every
7
+ programmer's workbench. Sadly, not every programmer knows how to use
8
+ them. My first exposure to them came when I started using Ruby,
9
+ refactoring and unit testing are a big part of the landscape in the
10
+ Ruby community.
11
+
12
+ Some time ago, I translated the refactoring example from the first
13
+ chapter of Martin Fowler's excellent book, Refactoring, out of Java
14
+ and into Ruby. I felt this would be a great way to learn more about
15
+ refactoring and brush up on my Ruby while I was at it. Recently, I
16
+ decided to update the translation for Ruby 1.8.X. One of the things I
17
+ needed to change was to convert the old unit tests to work with
18
+ Test::Unit, the new unit testing framework for Ruby.
19
+
20
+ I wasn't really looking forward to building a new test suite though.
21
+ Fortunately, help was available. Ryan Davis has written a great tool
22
+ called ZenTest, which creates test suites for existing bodies of
23
+ code. Since a lot of people are new to refactoring, unit testing, and
24
+ ZenTest, I thought this would be a great chance to introduce you to
25
+ this trio of tools.
26
+
27
+ Martin's example code is built around a video store application. In
28
+ his original code, there are three classes; Customer, Movie, and
29
+ Rental. I'll focus on just the Customer class in this article.
30
+ Here's the original code:
31
+
32
+ class Customer
33
+ attr_accessor :name
34
+
35
+ def initialize(name)
36
+ @name = name
37
+ @rentals = Array.new
38
+ end
39
+
40
+ def addRental(aRental)
41
+ @rentals.push(aRental)
42
+ end
43
+
44
+ def statement
45
+ totalAmount = 0.0
46
+ frequentRenterPoints = 0
47
+ rentals = @rentals.length
48
+ result = "\nRental Record for #{@name}\n"
49
+ thisAmount = 0.0
50
+ @rentals.each do |rental|
51
+ # determine amounts for each line
52
+ case rental.aMovie.pricecode
53
+ when Movie::REGULAR
54
+ thisAmount += 2
55
+ if rental.daysRented > 2
56
+ thisAmount += (rental.daysRented - 2) * 1.5
57
+ end
58
+
59
+ when Movie::NEW_RELEASE
60
+ thisAmount += rental.daysRented * 3
61
+
62
+ when Movie::CHILDRENS
63
+ thisAmount += 1.5
64
+ if each.daysRented > 3
65
+ thisAmount += (rental.daysRented - 3) * 1.5
66
+ end
67
+
68
+ end
69
+
70
+ # add frequent renter points
71
+ frequentRenterPoints += 1
72
+ # add bonus for a two day new release rental
73
+ if ( rental.daysRented > 1) &&
74
+ (Movie::NEW_RELEASE == rental.aMovie.pricecode)
75
+ frequentRenterPoints += 1
76
+ end
77
+
78
+ # show figures for this rental
79
+ result +="\t#{rental.aMovie.title}\t#{thisAmount}\n"
80
+ totalAmount += thisAmount
81
+ end
82
+ result += "Amount owed is #{totalAmount}\n"
83
+ result += "You earned #{frequentRenterPoints} frequent renter points"
84
+ end
85
+ end
86
+
87
+
88
+ Not the cleanest code in the world, but it is supposed to be that
89
+ way. This represents the code as you get it from the user. No
90
+ tests, poorly laid out, but working -- and it's your job to make it
91
+ better without breaking it. So, where to start? With unit tests of
92
+ course.
93
+
94
+ Time to grab ZenTest. You can run it like this:
95
+
96
+ $ zentest videostore.rb > test_videostore.rb
97
+
98
+ which produces a file full of tests. Running the test suite doesn't
99
+ do quite what we were hoping though:
100
+
101
+ $ ruby testVideoStore.rb Loaded suite testVideoStore
102
+ Started
103
+ EEEEEEEEEEE
104
+ Finished in 0.008974 seconds.
105
+
106
+ 1) Error!!!
107
+ test_addRental(TestCustomer):
108
+ NotImplementedError: Need to write test_addRental
109
+ testVideoStore.rb:11:in `test_addRental'
110
+ testVideoStore.rb:54
111
+
112
+ 2) Error!!!
113
+ test_name=(TestCustomer):
114
+ NotImplementedError: Need to write test_name=
115
+ testVideoStore.rb:15:in `test_name='
116
+ testVideoStore.rb:54
117
+
118
+ 3) Error!!!
119
+ test_statement=(TestCustomer):
120
+ NotImplementedError: Need to write test_statement
121
+ testVideoStore.rb:19:in `test_statement'
122
+ testVideoStore.rb:54
123
+ .
124
+ .
125
+ .
126
+
127
+ 11 tests, 0 assertions, 0 failures, 11 errors
128
+ $
129
+
130
+ So what exactly did we get out of this? Here's the portion of our
131
+ new test suite that matters for the Customer class:
132
+
133
+ # Code Generated by ZenTest v. 2.1.2
134
+ # classname: asrt / meth = ratio%
135
+ # Customer: 0 / 3 = 0.00%
136
+
137
+ require 'test/unit'
138
+
139
+ class TestCustomer < Test::Unit::TestCase
140
+ def test_addRental
141
+ raise NotImplementedError, 'Need to write test_addRental'
142
+ end
143
+
144
+ def test_name=
145
+ raise NotImplementedError, 'Need to write test_name='
146
+ end
147
+
148
+ def test_statement
149
+ raise NotImplementedError, 'Need to write test_statement'
150
+ end
151
+ end
152
+
153
+ ZenTest built three test methods: one for the accessor method, one for
154
+ the addRental method, and one for the statement method. Why nothing
155
+ for the initializer? Well, initializers tend to be pretty bulletproof
156
+ (if they're not, it's pretty easy to add the test method yourself).
157
+ Besides, we'll be testing it indirectly when we write test_name= (the
158
+ tests for the accessor method). There's one other thing we'll need to
159
+ add, the test suite doesn't load the code we're testing. Changing the
160
+ beginning of the script to require the videostore.rb file will do the
161
+ trick for us.
162
+
163
+
164
+ # Code Generated by ZenTest v. 2.1.2
165
+ # classname: asrt / meth = ratio%
166
+ # Customer: 0 / 3 = 0.00%
167
+
168
+ require 'test/unit'
169
+ require 'videostore'
170
+
171
+ That little snippet of comments at the top lets us know that we have three
172
+ methods under test in the Customer class, zero assertions testing
173
+ them, and no coverage. Let's fix that. We'll start by writing some
174
+ tests for test_name= (no, it really doesn't matter what order we go in --
175
+ this is just a convenient place to start).
176
+
177
+ def test_name=
178
+ aCustomer = Customer.new("Fred Jones")
179
+ assert_equal("Fred Jones",aCustomer.name)
180
+ aCustomer.name = "Freddy Jones"
181
+ assert_equal("Freddy Jones",aCustomer.name
182
+ end
183
+
184
+ Running testVideoStore.rb again gives us:
185
+
186
+ $ ruby testVideoStore.rb
187
+ Loaded suite testVideoStore
188
+ Started
189
+ E.EEEEEEEEE
190
+ Finished in 0.011233 seconds.
191
+
192
+ 1) Error!!!
193
+ test_addRental(TestCustomer):
194
+ NotImplementedError: Need to write test_addRental
195
+ testVideoStore.rb:13:in `test_addRental'
196
+ testVideoStore.rb:58
197
+
198
+ 2) Error!!!
199
+ test_statement(TestCustomer):
200
+ NotImplementedError: Need to write test_statement
201
+ testVideoStore.rb:23:in `test_statement'
202
+ testVideoStore.rb:58
203
+ .
204
+ .
205
+ .
206
+ 11 tests, 2 assertions, 0 failures, 10 errors
207
+ $
208
+
209
+ So far, so good. The line of 'E's (which shows errors in the test run)
210
+ has been reduced by one, and the summary line at the bottom tells us
211
+ roughly the same thing.
212
+
213
+ We really don't have a way to test addRental directly, so we'll just
214
+ write an stub test for now.
215
+
216
+ def test_addRental
217
+ assert(1) # stub test, since there is nothing in the method to test
218
+ end
219
+
220
+ When we run the tests again, we get:
221
+
222
+ $ ruby testVideoStore.rb
223
+ Loaded suite testVideoStore
224
+ Started
225
+ ..EEEEEEEEE
226
+ Finished in 0.008682 seconds.
227
+
228
+ 1) Error!!!
229
+ test_statement(TestCustomer):
230
+ NotImplementedError: Need to write test_statement
231
+ testVideoStore.rb:22:in `test_statement'
232
+ testVideoStore.rb:57
233
+ .
234
+ .
235
+ .
236
+ 11 tests, 3 assertions, 0 failures, 9 errors
237
+ $
238
+
239
+ Better and better, just one error left in the TestCustomer class.
240
+ Let's finish up with a test that will clear our test_statement error
241
+ and verify that addRental works correctly:
242
+
243
+ def test_statement
244
+ aMovie = Movie.new("Legacy",0)
245
+
246
+ aRental = Rental.new(aMovie,2)
247
+
248
+ aCustomer = Customer.new("Fred Jones")
249
+ aCustomer.addRental(aRental)
250
+ aStatement = "\nRental Record for Fred Jones\n\tLegacy\t2.0
251
+ Amount owed is 2.0\nYou earned 1 frequent renter points"
252
+
253
+ assert_equal(aStatement,aCustomer.statement)
254
+
255
+ end
256
+
257
+ We run the tests again, and see:
258
+
259
+ $ ruby testVideoStore.rb
260
+ Loaded suite testVideoStore
261
+ Started
262
+ ...EEEEEEEE
263
+ Finished in 0.009378 seconds.
264
+ .
265
+ .
266
+ .
267
+ 11 tests, 4 assertions, 0 failures, 8 errors
268
+ $
269
+
270
+ Great! The only errors left are on the Movie and Rental classes,
271
+ the Customer class is clean.
272
+
273
+ We can continue along like this for the remaining classes, but I'll
274
+ not bore you with those details. Instead, I'd like to look at how
275
+ ZenTest can help when you've already got some tests in place. Later
276
+ development allows us to do just that -- the video store owner
277
+ wants a new web based statement for web using customers.
278
+
279
+ After a bit of refactoring and new development, the code looks like
280
+ this:
281
+
282
+ class Customer
283
+ attr_accessor :name
284
+
285
+ def initialize(name)
286
+ @name = name
287
+ @rentals = Array.new
288
+ end
289
+
290
+ def addRental(aRental)
291
+ @rentals.push(aRental)
292
+ end
293
+
294
+ def statement
295
+ result = "\nRental Record for #{@name}\n"
296
+ @rentals.each do
297
+ |each|
298
+ # show figures for this rental
299
+ result +="\t#{each.aMovie.title}\t#{each.getCharge}\n"
300
+ end
301
+ result += "Amount owed is #{getTotalCharge}\n"
302
+ result +=
303
+ "You earned #{getFrequentRenterPoints} frequent renter points"
304
+ end
305
+
306
+ def htmlStatement
307
+ result = "\n<H1>Rentals for <EM>#{name}</EM></H1><P>\n"
308
+ @rentals.each do
309
+ |each|
310
+ result += "#{each.aMovie.title}: #{each.getCharge}<BR>\n"
311
+ end
312
+ result += "You owe <EM>#{getTotalCharge}</EM><P>\n"
313
+ result +=
314
+ "On this rental you earned <EM>#{getFrequentRenterPoints}" +
315
+ "</EM> frequent renter points<P>"
316
+ end
317
+
318
+ def getTotalCharge
319
+ result = 0.0
320
+ @rentals.each do
321
+ |each|
322
+ result += each.getCharge()
323
+ end
324
+ result
325
+ end
326
+
327
+ def getFrequentRenterPoints
328
+ result = 0
329
+ @rentals.each do
330
+ |each|
331
+ result += each.getFrequentRenterPoints
332
+ end
333
+ result
334
+ end
335
+ end
336
+
337
+ There's a lot of new stuff in here. If we run ZenTest again, it'll
338
+ pick up the methods we don't have any coverage on (we should have
339
+ written them as we wrote the new methods, but this is a bit more
340
+ illustrative). This time, we'll invoke ZenTest a little bit
341
+ differently:
342
+
343
+ $ zentest videostore.rb testVideoStore.rb > Missing_tests
344
+
345
+ and our (trimmed) output looks like this:
346
+
347
+ # Code Generated by ZenTest v. 2.1.2
348
+ # classname: asrt / meth = ratio%
349
+ # Customer: 4 / 6 = 66.67%
350
+
351
+
352
+ require 'test/unit'
353
+
354
+ class TestCustomer < Test::Unit::TestCase
355
+ def test_getFrequentRenterPoints
356
+ raise NotImplementedError,
357
+ 'Need to write test_getFrequentRenterPoints'
358
+ end
359
+
360
+ def test_getTotalCharge
361
+ raise NotImplementedError, 'Need to write test_getTotalCharge'
362
+ end
363
+
364
+ def test_htmlStatement
365
+ raise NotImplementedError, 'Need to write test_htmlStatement'
366
+ end
367
+ end
368
+
369
+ Hmmm, three more test methods to fill in to get our complete
370
+ coverage. As we write these, we can just migrate them into our
371
+ existing testVideoStore.rb test suite. Then we can keep moving ahead
372
+ with refactoring and adding new features. In the future, let's just
373
+ be sure we add tests as we go along. ZenTest can help you here too.
374
+ You can write stubs for new development, then run ZenTest to create
375
+ your new test stubs as well. After some refactorings (like 'extract
376
+ method'), ZenTest can be used the same way.
377
+
378
+ Refactoring and unit testing are powerful tools for programmers, and
379
+ ZenTest provides an easy way to start using them in a Ruby
380
+ environment. Hopefully, this introduction has whetted your appetite.
381
+
382
+ If you're interested in learning more about refactoring, please grab a
383
+ copy of 'Refactoring: Improving the Design of Existing Code' and take
384
+ a look at www.refactoring.com. For more information about unit
385
+ testing, please see: c2.com/cgi/wiki?UnitTest,
386
+ www.junit.org/index.htm, and
387
+ www.extremeprogramming.org/rules/unittests.html.
388
+
389
+ The latest information about Test::Unit and ZenTest are available at
390
+ their home pages: testunit.talbott.ws (for Test::Unit) and
391
+ www.zenspider.com/ZSS/Products/ZenTest.
392
+
393
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'autotest'
4
+
5
+ Autotest.parse_options
6
+ Autotest.runner.run
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ exec "multiruby", "-S", "gem", *ARGV
4
+
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'multiruby'
4
+
5
+ root_dir = Multiruby.root_dir
6
+
7
+ def setenv dir
8
+ bin = "#{dir}/bin"
9
+ gem = Dir["#{dir}/lib/ruby/gems/*"].first
10
+
11
+ ENV['PATH'] = bin + File::PATH_SEPARATOR + ENV['PATH']
12
+ ENV['GEM_HOME'] = gem
13
+ ENV['GEM_PATH'] = gem
14
+ end
15
+
16
+ ##
17
+ # multiruby -1 1.8.7 ruby_args...
18
+
19
+ if ARGV.first == "-1" then
20
+ ARGV.shift
21
+ vers = Dir["#{root_dir}/install/#{ARGV.shift}*"]
22
+
23
+ abort "ambiguous version: #{vers.map { |p| File.basename p }.inspect}" if
24
+ vers.size != 1
25
+
26
+ dir = vers.first
27
+ setenv dir
28
+
29
+ exec "#{dir}/bin/ruby", *ARGV
30
+ end
31
+
32
+ versions = Multiruby.build_and_install
33
+ versions = ENV['VERSIONS'].split(/:/) if ENV.has_key? 'VERSIONS'
34
+
35
+ if ENV.has_key? 'EXCLUDED_VERSIONS' then
36
+ excludes = Regexp.union(*ENV['EXCLUDED_VERSIONS'].split(/:/))
37
+ versions = versions.delete_if { |v| v =~ excludes }
38
+ end
39
+
40
+ # safekeep original PATH
41
+ original_path = ENV['PATH']
42
+
43
+ results = {}
44
+ versions.each do |version|
45
+ dir = "#{root_dir}/install/#{version}"
46
+ ruby = "#{dir}/bin/ruby"
47
+
48
+ ruby.sub!(/bin.ruby/, 'bin/rbx') if version =~ /rubinius/
49
+
50
+ puts
51
+ puts "VERSION = #{version}"
52
+ cmd = [ruby, ARGV].flatten.map { |s| s =~ /\"/ ? "'#{s}'" : s }.join(' ')
53
+ cmd.sub!(/#{ENV['HOME']}/, '~')
54
+ puts "CMD = #{cmd}"
55
+ puts
56
+
57
+ setenv dir
58
+
59
+ system ruby, *ARGV
60
+ puts
61
+ puts "RESULT = #{$?}"
62
+ results[version] = $?
63
+
64
+ # restore the path to original state
65
+ ENV['PATH'] = original_path
66
+ end
67
+
68
+ passed, failed = results.keys.partition { |v| results[v] == 0 }
69
+
70
+ puts
71
+ puts "TOTAL RESULT = #{failed.size} failures out of #{results.size}"
72
+ puts
73
+ puts "Passed: #{passed.join(", ")}"
74
+ puts "Failed: #{failed.join(", ")}"
75
+
76
+ exit failed.size