SlimTest 4.6.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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