flexmock 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG +10 -1
  2. data/README +390 -209
  3. data/Rakefile +31 -10
  4. data/doc/GoogleExample.rdoc +275 -0
  5. data/doc/releases/flexmock-0.6.0.rdoc +136 -0
  6. data/lib/flexmock.rb +3 -1160
  7. data/lib/flexmock/argument_matchers.rb +57 -0
  8. data/lib/flexmock/argument_types.rb +42 -0
  9. data/lib/flexmock/base.rb +22 -0
  10. data/lib/flexmock/composite.rb +10 -0
  11. data/lib/flexmock/core.rb +206 -0
  12. data/lib/flexmock/core_class_methods.rb +92 -0
  13. data/lib/flexmock/default_framework_adapter.rb +31 -0
  14. data/lib/flexmock/expectation.rb +334 -0
  15. data/lib/flexmock/expectation_director.rb +59 -0
  16. data/lib/flexmock/mock_container.rb +159 -0
  17. data/lib/flexmock/noop.rb +13 -0
  18. data/lib/flexmock/partial_mock.rb +226 -0
  19. data/lib/flexmock/recorder.rb +71 -0
  20. data/lib/flexmock/rspec.rb +34 -0
  21. data/lib/flexmock/test_unit.rb +32 -0
  22. data/lib/flexmock/test_unit_integration.rb +53 -0
  23. data/lib/flexmock/validators.rb +77 -0
  24. data/test/rspec_integration/integration_spec.rb +36 -0
  25. data/test/test_container_methods.rb +119 -0
  26. data/test/test_default_framework_adapter.rb +39 -0
  27. data/test/test_example.rb +1 -1
  28. data/test/test_extended_should_receive.rb +63 -0
  29. data/test/test_mock.rb +1 -1
  30. data/test/test_naming.rb +1 -1
  31. data/test/{test_any_instance.rb → test_new_instances.rb} +15 -8
  32. data/test/{test_stubbing.rb → test_partial_mock.rb} +44 -44
  33. data/test/test_record_mode.rb +1 -1
  34. data/test/test_samples.rb +6 -8
  35. data/test/test_should_receive.rb +7 -3
  36. data/test/test_tu_integration.rb +1 -1
  37. data/test/test_unit_integration/test_auto_test_unit.rb +34 -0
  38. metadata +30 -5
  39. data/test/test_class_interception.rb +0 -140
data/Rakefile CHANGED
@@ -1,15 +1,25 @@
1
1
  # Rakefile for flexmock -*- ruby -*-
2
2
 
3
+ #---
4
+ # Copyright 2003, 2004, 2005, 2006, 2007 by Jim Weirich (jim@weirichhouse.org).
5
+ # All rights reserved.
6
+
7
+ # Permission is granted for use, copying, modification, distribution,
8
+ # and distribution of modified versions of this work as long as the
9
+ # above copyright notice is included.
10
+ #+++
11
+
3
12
  require 'rubygems'
4
13
  require 'rake/gempackagetask'
5
14
  require 'rake/clean'
6
15
  require 'rake/rdoctask'
7
16
  require 'rake/testtask'
17
+ require 'rake/contrib/rubyforgepublisher'
8
18
 
9
19
  CLEAN.include('*.tmp')
10
20
  CLOBBER.include("html", 'pkg')
11
21
 
12
- PKG_VERSION = '0.5.1'
22
+ PKG_VERSION = '0.6.0'
13
23
 
14
24
  PKG_FILES = FileList[
15
25
  '[A-Z]*',
@@ -26,7 +36,6 @@ RDOC_FILES = FileList[
26
36
  'doc/**/*.rdoc',
27
37
  ]
28
38
 
29
-
30
39
  task :default => [:test_all]
31
40
  task :test_all => [:test]
32
41
  task :test_units => [:test]
@@ -43,7 +52,16 @@ end
43
52
  Rake::TestTask.new(:test_extended) do |t|
44
53
  t.test_files = FileList['test/extended/test_*.rb']
45
54
  t.verbose = true
46
- t.verbose = true
55
+ t.warning = true
56
+ end
57
+
58
+ task :rspec do
59
+ ENV['RUBYLIB'] = "/Users/jim/working/svn/software/rspec/rspec/lib:/Users/jim/working/svn/software/flexmock/lib"
60
+ sh 'echo $RUBYLIB'
61
+ ruby "/Users/jim/working/svn/software/rspec/rspec/bin/spec test/rspec_integration/*_spec.rb" rescue nil
62
+ puts
63
+ puts "*** There should be two failures in the above report. ***"
64
+ puts
47
65
  end
48
66
 
49
67
  # RCov Target --------------------------------------------------------
@@ -142,18 +160,21 @@ task :rf => :rubyfiles
142
160
  require 'rake/contrib/publisher'
143
161
  require 'rake/contrib/sshpublisher'
144
162
 
145
- task :publish => [:rdoc] do
146
- html_publisher = Rake::SshFreshDirPublisher.new(
163
+ publisher = Rake::CompositePublisher.new
164
+ publisher.add(Rake::RubyForgePublisher.new('flexmock', 'jimweirich'))
165
+ publisher.add(Rake::SshFreshDirPublisher.new(
147
166
  'umlcoop',
148
167
  'htdocs/software/flexmock',
149
- 'html')
150
- blurb_publisher = Rake::SshFilePublisher.new(
168
+ 'html'))
169
+ publisher.add(Rake::SshFilePublisher.new(
151
170
  'umlcoop',
152
171
  'htdocs/software/flexmock',
153
172
  '.',
154
- 'flexmock.blurb')
155
- html_publisher.upload
156
- blurb_publisher.upload
173
+ 'flexmock.blurb'))
174
+
175
+ desc "Publish the documentation on public websites"
176
+ task :publish => [:rdoc] do
177
+ publisher.upload
157
178
  end
158
179
 
159
180
  SVNHOME = 'svn://localhost/software/flexmock'
@@ -0,0 +1,275 @@
1
+ = Extended FlexMock Example Using Google4R
2
+
3
+ Google4R is a simple Ruby wrapper around the Google APIs. In this extended example, we will use FlexMock to test software that uses the Google APIs, without every communicating with Google itself.
4
+
5
+ == Purchase.rb
6
+
7
+ Here is the bit of code that we will be testing...
8
+
9
+ require 'google4r/checkout'
10
+ require 'item'
11
+
12
+ class Purchase
13
+
14
+ def initialize(config)
15
+ @frontend = Frontend.new(config)
16
+ @frontend.tax_table_factory = TaxTableFactory.new
17
+ end
18
+
19
+ # Purchase the +quantity+ items identified by +item_id+. Return the
20
+ # confirmation page URL.
21
+ def purchase(item_id, quantity=1)
22
+ item = Item.find(item_id)
23
+ checkout = @frontend.create_checkout_command
24
+ checkout.cart.create_item do |cart_item|
25
+ cart_item.name = item.name
26
+ cart_item.description = item.description
27
+ cart_item.unit_price = item.unit_price
28
+ cart_item.quantity = quantity
29
+ end
30
+ response = checkout.send_to_google_checkout
31
+ response.redirect_url
32
+ end
33
+
34
+ end
35
+
36
+ +FrontEnd+ is a Google4R class that provides a lot of the front end work for
37
+ talking to the Google APIs. The config object given to the Purchase
38
+ initializer is simply a hash of values defining the merchant_id, merchant_key
39
+ and sandbox flag. To use the real Google checkout APIs, you will need to
40
+ obtains a merchant id and key from Google. Since we will be mocking the Google
41
+ interaction, we can use dummy values in our test.
42
+
43
+ The tax table factory is required by the Google4R software. We provide the following simplified one. Read the Google API documents for more information.
44
+
45
+ class TestTaxTableFactory
46
+ def effective_tax_tables_at(time)
47
+ tax_free_table = TaxTable.new(false)
48
+ tax_free_table.name = "default table"
49
+ tax_free_table.create_rule do |rule|
50
+ rule.area = UsCountryArea.new(UsCountryArea::ALL)
51
+ rule.rate = 0.0
52
+ end
53
+ return [tax_free_table]
54
+ end
55
+ end
56
+
57
+ +Item+ is simply an ActiveRecord class that we are using to hold our purchase item information. It should respond to the +name+, +description+ and +unit_price+ messages.
58
+
59
+ == Testing Without Using External Resources
60
+
61
+ Our first test attempt will be to run the +purchase+ method without talking to
62
+ either the live Google web services, or hitting an actual ActiveRecord
63
+ database.
64
+
65
+ === Mocking Active Record
66
+
67
+ The ActiveRecord part is easy to mock. The following will handle it:
68
+
69
+ flexmock(Item).should_receive(:find).with(1).and_return(
70
+ flexmock("guitar",
71
+ :name => "Deschutes",
72
+ :description => "Deschutes model Guitar",
73
+ :unit_price => Money.new(2400.00)))
74
+
75
+ We have mocked out the +find+ method on +Item+ so that whenever we call find
76
+ with an integer argument of 1, we will return a mock item that will report its
77
+ name, description and unit_price. This gives us an item for testing without
78
+ actually reading the database.
79
+
80
+ === Mocking the Google Web Services Call
81
+
82
+ Next we want to prevent the Google4R API from actually talking to the live web
83
+ service. Everything that happens in the purchase method is all done locally
84
+ except for the final call to +send_to_google_checkout+. All we need to do is
85
+ mock out that one method.
86
+
87
+ flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
88
+ instance.should_receive(:send_to_google_checkout).once.
89
+ and_return(flexmock(:redirect_url => "http://google.response.url"))
90
+ end
91
+
92
+ When we ask +FrontEnd+ to create a check out command, it returns an instance
93
+ of <tt>Google4R::Checkout::CheckoutCommand</tt>. We then use flexmock to
94
+ specify that when Google4R::Checkout::CheckoutCommand creates a new instance,
95
+ it should actually return a partial mock of that instance. The block given to
96
+ the +new_instances+ method allows us to configure the mocked checkout command.
97
+ We tell it return a response object (yes, another mock) that report our dummy
98
+ response URL.
99
+
100
+ === The Final Result
101
+
102
+ Here is the complete unit test:
103
+
104
+ def test_buying_a_guitar
105
+ # Setup
106
+ flexmock(Item).should_receive(:find).with(1).and_return(
107
+ flexmock("guitar",
108
+ :name => "Deschutes",
109
+ :description => "Deschutes model Guitar",
110
+ :unit_price => Money.new(2400.00)))
111
+
112
+ flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
113
+ instance.should_receive(:send_to_google_checkout).once.
114
+ and_return(flexmock(:redirect_url => "http://google.response.url"))
115
+ end
116
+
117
+ # Execute
118
+ p = Purchase.new({
119
+ :merchant_id => 'dummy_id',
120
+ :merchant_key => 'dummy_key',
121
+ :use_sandbox => true })
122
+ url = p.purchase(1)
123
+
124
+ # Assert
125
+ assert_equal "http://google.response.url", url
126
+ end
127
+
128
+ == Testing the Details
129
+
130
+ The above test is fine as far as it goes. It demonstrates how to use mocks to
131
+ avoid talking to external resources such as databases and web services. But as a unit test, it is sorely lacking in several areas.
132
+
133
+ All the test really demonstrates is that the +send_to_google_checkout+ method is called. There are no tests to ensure that the right item descriptions and prices are correctly stored in the cart. In fact, if we rewrote the purchase method as follows:
134
+
135
+ def purchase(item_id, quantity=1)
136
+ @frontend.create_checkout_command.send_to_google_checkout.redirect_url
137
+ end
138
+
139
+ it would still pass the unit test we designed, even though the rewrite is obviously an incorrect implementation.
140
+
141
+ A more complete test is a bit more complicated. Here are the details.
142
+
143
+ === Mocking Active Record
144
+
145
+ Our incorrect version of purchase never calls the +find+ method of Item. We can easily test for that by adding a +once+ constraint one that mock specification. Since find is a read-only method, we don't really care if it is called multiple times, as long as it is called at least one time, so we will add an +at_least+ modifier as well.
146
+
147
+ Finally, we are going to break the guitar mock out into its own declaration. The reason will become obvious in a bit.
148
+
149
+ mock_guitar = flexmock("guitar",
150
+ :name => "Deschutes",
151
+ :description => "Deschutes model guitar",
152
+ :unit_price => Money.new(2400.00))
153
+
154
+ flexmock(Item).should_receive(:find).with(1).at_least.once.
155
+ and_return(mock_guitar)
156
+
157
+ === Mocking a Cart Item
158
+
159
+ The next bit is a wee bit complicated, but we will handle it a little bit at a time so that it doesn't become overwhelming.
160
+
161
+ There are three main objects in the Google checkout API that we deal with in the next section.: (1) the checkout command object returned by the front end, (2) the cart object returned by the checkout command, and (3) the item passed to the block in the +create_item+ call.
162
+
163
+ We will tackle them in reverse order, starting with the item objects given to the +create_item+ block. The item must respond to four attribute assignments. This is straightforward to mock, just make sure you include the +once+ constraint so that the assignments are required.
164
+
165
+ mock_item = flexmock("item")
166
+ mock_item.should_receive(:name=).with(mock_guitar.name).once
167
+ mock_item.should_receive(:description=).with(mock_guitar.description).once
168
+ mock_item.should_receive(:unit_price=).with(mock_guitar.unit_price).once
169
+ mock_item.should_receive(:quantity=).with(1).once
170
+
171
+ Notice how we used the mock_guitar object defined earlier to provide values in the +with+ constraint. This way we don't have to repeat the explicit strings and values we are checking. (Keep it DRY!).
172
+
173
+ === Mocking the Cart
174
+
175
+ The mock cart object will pass the mock_item to a block when the +create_item+ method is called. We specify that with the following:
176
+
177
+ mock_cart = flexmock("cart")
178
+ mock_cart.should_receive(:create_item).with(Proc).once.and_return { |block|
179
+ block.call(mock_item)
180
+ }
181
+
182
+ FlexMock objects can handle blocks passed to them by treating them as the final object in the calling list. Use +Proc+ in the +with+ constraint to match the block and then invoke the block explicitly via <tt>block.call(...)</tt> in the +and_return+ specification.
183
+
184
+ === Mocking the Checkout Command
185
+
186
+ Finally, we tie it all together by mocking the checkout command. As before, we use +new_instances+ to force newly created checkout commands to be stubbed. This time we not only mockout the +send_to_google+ method, but we also mock the +cart+ command to return the carefully crafted +mock_cart+ object from the previous section.
187
+
188
+ flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
189
+ instance.should_receive(:cart).with().once.and_return(mock_cart)
190
+ instance.should_receive(:send_to_google_checkout).once.
191
+ and_return(flexmock(:redirect_url => "http://google.response.url"))
192
+ end
193
+
194
+ === The Final Test Method
195
+
196
+ Here is the complete detailed version of the test method.
197
+
198
+ def test_buying_a_guitar_with_details
199
+ # Setup
200
+ mock_guitar = flexmock("guitar",
201
+ :name => "Deschutes",
202
+ :description => "Deschutes model guitar",
203
+ :unit_price => Money.new(2400.00))
204
+
205
+ flexmock(Item).should_receive(:find).with(1).at_least.once.
206
+ and_return(mock_guitar)
207
+
208
+ mock_item = flexmock("item")
209
+ mock_item.should_receive(:name=).with(mock_guitar.name).once
210
+ mock_item.should_receive(:description=).with(mock_guitar.description).once
211
+ mock_item.should_receive(:unit_price=).with(mock_guitar.unit_price).once
212
+ mock_item.should_receive(:quantity=).with(1).once
213
+
214
+ mock_cart = flexmock("cart")
215
+ mock_cart.should_receive(:create_item).with(Proc).once.and_return { |block|
216
+ block.call(mock_item)
217
+ }
218
+
219
+ flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
220
+ instance.should_receive(:cart).with().once.and_return(mock_cart)
221
+ instance.should_receive(:send_to_google_checkout).once.
222
+ and_return(flexmock(:redirect_url => "http://google.response.url"))
223
+ end
224
+
225
+ # Execute
226
+ p = Purchase.new({
227
+ :merchant_id => 'dummy_id',
228
+ :merchant_key => 'dummy_key',
229
+ :use_sandbox => true })
230
+ url = p.purchase(1)
231
+
232
+ # Assert
233
+ assert_equal "http://google.response.url", url
234
+ end
235
+
236
+ == Summary
237
+
238
+ Testing with mock objects can get complex. We used seven different mock or
239
+ partial mock objects in testing the interaction of our code with the Google
240
+ checkout API. Most testing scenarios won't require that many, but anytime your
241
+ code touches something external, it might require a mock object for testing.
242
+
243
+ We should stop and ask ourselves: was it worth it? It seems like an awful lot
244
+ of work just to test a very simple purchase method. Wouldn't it just be easier
245
+ to just use the Google API directly for testing and forget about the mocks?
246
+
247
+ Perhaps, but using mock objects have several definite advantages:
248
+
249
+ * You can run the test at any time without worrying whether Google, the
250
+ internet, or anything else is up and connected.
251
+
252
+ * You can easy test for error conditions using mock objects. For example, does
253
+ your code correctly handle the case where you get an exception when
254
+ connecting to google? Mocks can easily create those error conditions that
255
+ are difficult to achieve with real objects.
256
+
257
+ E.g.
258
+
259
+ instance.should_receive(:send_to_google_checkout).once.
260
+ and_return { raise Google4R::Checkout::GoogleCheckoutError }
261
+
262
+ Some might point out that in the final test method we are hardly using
263
+ Google4R software at all, most of the code we interact with are mock objects.
264
+ Doesn't that defeat the purpose of testing?
265
+
266
+ The answer is simple. Always keep in mind what you are testing. The goal of
267
+ the TestPurchase test case is not the make sure the Google4R code is correct,
268
+ but that our Purchase class correctly interoperates with it. We do that by
269
+ carefully stating what methods are called with what arguments and what they
270
+ return. The test just checks that we are using to external software as we
271
+ expect it to. We don't actually care about the Google4R software itself in
272
+ this test case (presumably we do have tests that cover Google4R, but those are
273
+ different tests).
274
+
275
+ In the end, mock objects are a power tool to have in your testing toolbox.
@@ -0,0 +1,136 @@
1
+ = FlexMock 0.6.0 Released
2
+
3
+ FlexMock is a flexible mocking library for use in unit testing and behavior specification in Ruby. Version 0.6.0 introduces a number of API enhancements to make testing with mocks even easier than before.
4
+
5
+ == New in 0.6.0
6
+
7
+ * Better integration with Test::Unit (no need to explicitly include
8
+ FlexMock::TestCase).
9
+
10
+ * Integration with RSpec (version 0.9.0 or later of RSpec is required).
11
+
12
+ * The +flexmock+ method will now create both regular mocks and partial mocks.
13
+
14
+ flexmock() # => a full mock
15
+ flexmock(person) # => a partial mock based on person
16
+
17
+ (+flexstub+ is still included for backwards compatibility).
18
+
19
+ * Quick and simple mocks my now be created using an expectation hash. For
20
+ example:
21
+
22
+ flexmock(:foo => 10, :bar => "Hello")
23
+
24
+ will create a mock with two methods, :foo and :bar,defined. :foo will
25
+ return 10 when invoked, and :bar will return "Hello".
26
+
27
+ * The +should_receive+ method will now allow multiple methods (with the same
28
+ constraints) be defined in a single call. For example, the following
29
+ declares that both :read and :write need to be called at least one time each
30
+ on the mock object.
31
+
32
+ flexmock.should_receive(:read, :write).at_least.once
33
+
34
+
35
+ * +should_recieve+ now will allow expectation hashes as arguments. This is
36
+ similar to the list of methods, but allows each defined method to have its
37
+ own return value.
38
+
39
+ flexmock.should_receive(:name => "John", :age => 32)
40
+
41
+
42
+ * In addition to using a block for defining constrains, constraints may now be
43
+ applied directly to the return value of +new_instances+. Combined with the
44
+ expectation hashes supported by +should_receive+, simple mocking scenarios
45
+ have become much more succinct. For example:
46
+
47
+ flexmock(Person).new_instances.should_receive(:name => "John", :age => 32)
48
+
49
+ * Improved implementation, allowing for more flexible use and greater
50
+ consistency between full mock and partial mocks.
51
+
52
+ * Version 0.6.0 also includes a fix for an incompatibility with some older
53
+ versions of RCov. The FlexMock Rakefile now includes a RCov task (and we
54
+ have 100% code coverage).
55
+
56
+ == What is FlexMock?
57
+
58
+ FlexMock is a flexible framework for creating mock object for testing. When
59
+ running unit tests, it is often desirable to use isolate the objects being
60
+ tested from the "real world" by having them interact with simplified test
61
+ objects. Sometimes these test objects simply return values when called, other
62
+ times they verify that certain methods were called with particular arguments
63
+ in a particular order.
64
+
65
+ FlexMock makes creating these test objects easy.
66
+
67
+ === Features
68
+
69
+ * Easy integration with both Test::Unit and RSpec. Mocks created with the
70
+ flexmock method are automatically verified at the end of the test or
71
+ example.
72
+
73
+ * A fluent interface that allows mock behavior to be specified very
74
+ easily.
75
+
76
+ * A "record mode" where an existing implementation can record its
77
+ interaction with a mock for later validation against a new
78
+ implementation.
79
+
80
+ * Easy mocking of individual methods in existing, non-mock objects.
81
+
82
+ * The ability to cause classes to instantiate test instances (instead of real
83
+ instances) for the duration of a test.
84
+
85
+ === Example
86
+
87
+ Suppose you had a Dog object that wagged a tail when it was happy.
88
+ Something like this:
89
+
90
+ class Dog
91
+ def initialize(a_tail)
92
+ @tail = a_tail
93
+ end
94
+ def happy
95
+ @tail.wag
96
+ end
97
+ end
98
+
99
+ To test the +Dog+ class without a real +Tail+ object (perhaps because
100
+ real +Tail+ objects activate servos in some robotic equipment), you
101
+ can do something like this:
102
+
103
+ require 'test/unit'
104
+ require 'flexmock/test_unit'
105
+
106
+ class TestDog < Test::Unit::TestCase
107
+ def test_dog_wags_tail_when_happy
108
+ tail = flexmock("tail")
109
+ tail.should_receive(:wag).once
110
+ dog = Dog.new(tail)
111
+ dog.happy
112
+ end
113
+ end
114
+
115
+ FlexMock will automatically verify that the mocked tail object received the
116
+ message +wag+ exactly one time. If it doesn't, the test will not pass.
117
+
118
+ See the FlexMock documentation at http://flexmock.rubyforge.org for details on
119
+ specifying arguments and return values on mocked methods, as well as a simple
120
+ technique for mocking tail objects when the Dog class creates the tail objects
121
+ directly.
122
+
123
+ == Availability
124
+
125
+ You can make sure you have the latest version with a quick RubyGems command:
126
+
127
+ gem install flexmock (you may need root/admin privileges)
128
+
129
+ Otherwise, you can get it from the more traditional places:
130
+
131
+ Download:: http://rubyforge.org/project/showfiles.php?group_id=170
132
+
133
+ You will find documentation at: http://flexmock.rubyforge.org.
134
+
135
+ -- Jim Weirich
136
+