1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
|
= Extended FlexMock Example Using Google4R
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.
== Purchase.rb
Here is the bit of code that we will be testing...
require 'google4r/checkout'
require 'item'
class Purchase
def initialize(config)
@frontend = Frontend.new(config)
@frontend.tax_table_factory = TaxTableFactory.new
end
# Purchase the +quantity+ items identified by +item_id+. Return the
# confirmation page URL.
def purchase(item_id, quantity=1)
item = Item.find(item_id)
checkout = @frontend.create_checkout_command
checkout.cart.create_item do |cart_item|
cart_item.name = item.name
cart_item.description = item.description
cart_item.unit_price = item.unit_price
cart_item.quantity = quantity
end
response = checkout.send_to_google_checkout
response.redirect_url
end
end
+FrontEnd+ is a Google4R class that provides a lot of the front end work for
talking to the Google APIs. The config object given to the Purchase
initializer is simply a hash of values defining the merchant_id, merchant_key
and sandbox flag. To use the real Google checkout APIs, you will need to
obtains a merchant id and key from Google. Since we will be mocking the Google
interaction, we can use dummy values in our test.
The tax table factory is required by the Google4R software. We provide
the following simplified one. Read the Google API documents for more
information.
class TestTaxTableFactory
def effective_tax_tables_at(time)
tax_free_table = TaxTable.new(false)
tax_free_table.name = "default table"
tax_free_table.create_rule do |rule|
rule.area = UsCountryArea.new(UsCountryArea::ALL)
rule.rate = 0.0
end
return [tax_free_table]
end
end
+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.
== Testing Without Using External Resources
Our first test attempt will be to run the +purchase+ method without talking to
either the live Google web services, or hitting an actual ActiveRecord
database.
=== Mocking Active Record
The ActiveRecord part is easy to mock. The following will handle it:
flexmock(Item).should_receive(:find).with(1).and_return(
flexmock("guitar",
:name => "Deschutes",
:description => "Deschutes model Guitar",
:unit_price => Money.new(2400.00)))
We have mocked out the +find+ method on +Item+ so that whenever we call find
with an integer argument of 1, we will return a mock item that will report its
name, description and unit_price. This gives us an item for testing without
actually reading the database.
=== Mocking the Google Web Services Call
Next we want to prevent the Google4R API from actually talking to the live web
service. Everything that happens in the purchase method is all done locally
except for the final call to +send_to_google_checkout+. All we need to do is
mock out that one method.
flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
instance.should_receive(:send_to_google_checkout).once.
and_return(flexmock(:redirect_url => "http://google.response.url"))
end
When we ask +FrontEnd+ to create a check out command, it returns an instance
of <tt>Google4R::Checkout::CheckoutCommand</tt>. We then use flexmock to
specify that when Google4R::Checkout::CheckoutCommand creates a new instance,
it should actually return a partial mock of that instance. The block given to
the +new_instances+ method allows us to configure the mocked checkout command.
We tell it return a response object (yes, another mock) that report our dummy
response URL.
=== The Final Result
Here is the complete unit test:
def test_buying_a_guitar
# Setup
flexmock(Item).should_receive(:find).with(1).and_return(
flexmock("guitar",
:name => "Deschutes",
:description => "Deschutes model Guitar",
:unit_price => Money.new(2400.00)))
flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
instance.should_receive(:send_to_google_checkout).once.
and_return(flexmock(:redirect_url => "http://google.response.url"))
end
# Execute
p = Purchase.new({
:merchant_id => 'dummy_id',
:merchant_key => 'dummy_key',
:use_sandbox => true })
url = p.purchase(1)
# Assert
assert_equal "http://google.response.url", url
end
== Testing the Details
The above test is fine as far as it goes. It demonstrates how to use
mocks to avoid talking to external resources such as databases and web
services. But as a unit test, it is sorely lacking in several areas.
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:
def purchase(item_id, quantity=1)
@frontend.create_checkout_command.send_to_google_checkout.redirect_url
end
it would still pass the unit test we designed, even though the rewrite
is obviously an incorrect implementation.
A more complete test is a bit more complicated. Here are the details.
=== Mocking Active Record
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.
Finally, we are going to break the guitar mock out into its own
declaration. The reason will become obvious in a bit.
mock_guitar = flexmock("guitar",
:name => "Deschutes",
:description => "Deschutes model guitar",
:unit_price => Money.new(2400.00))
flexmock(Item).should_receive(:find).with(1).at_least.once.
and_return(mock_guitar)
=== Mocking a Cart Item
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.
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.
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.
mock_item = flexmock("item")
mock_item.should_receive(:name=).with(mock_guitar.name).once
mock_item.should_receive(:description=).with(mock_guitar.description).once
mock_item.should_receive(:unit_price=).with(mock_guitar.unit_price).once
mock_item.should_receive(:quantity=).with(1).once
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!).
=== Mocking the Cart
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:
mock_cart = flexmock("cart")
mock_cart.should_receive(:create_item).with(Proc).once.and_return { |block|
block.call(mock_item)
}
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.
=== Mocking the Checkout Command
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.
flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
instance.should_receive(:cart).with().once.and_return(mock_cart)
instance.should_receive(:send_to_google_checkout).once.
and_return(flexmock(:redirect_url => "http://google.response.url"))
end
=== The Final Test Method
Here is the complete detailed version of the test method.
def test_buying_a_guitar_with_details
# Setup
mock_guitar = flexmock("guitar",
:name => "Deschutes",
:description => "Deschutes model guitar",
:unit_price => Money.new(2400.00))
flexmock(Item).should_receive(:find).with(1).at_least.once.
and_return(mock_guitar)
mock_item = flexmock("item")
mock_item.should_receive(:name=).with(mock_guitar.name).once
mock_item.should_receive(:description=).with(mock_guitar.description).once
mock_item.should_receive(:unit_price=).with(mock_guitar.unit_price).once
mock_item.should_receive(:quantity=).with(1).once
mock_cart = flexmock("cart")
mock_cart.should_receive(:create_item).with(Proc).once.and_return { |block|
block.call(mock_item)
}
flexmock(Google4R::Checkout::CheckoutCommand).new_instances do |instance|
instance.should_receive(:cart).with().once.and_return(mock_cart)
instance.should_receive(:send_to_google_checkout).once.
and_return(flexmock(:redirect_url => "http://google.response.url"))
end
# Execute
p = Purchase.new({
:merchant_id => 'dummy_id',
:merchant_key => 'dummy_key',
:use_sandbox => true })
url = p.purchase(1)
# Assert
assert_equal "http://google.response.url", url
end
== Summary
Testing with mock objects can get complex. We used seven different mock or
partial mock objects in testing the interaction of our code with the Google
checkout API. Most testing scenarios won't require that many, but anytime your
code touches something external, it might require a mock object for testing.
We should stop and ask ourselves: was it worth it? It seems like an awful lot
of work just to test a very simple purchase method. Wouldn't it just be easier
to just use the Google API directly for testing and forget about the mocks?
Perhaps, but using mock objects have several definite advantages:
* You can run the test at any time without worrying whether Google, the
internet, or anything else is up and connected.
* You can easy test for error conditions using mock objects. For example, does
your code correctly handle the case where you get an exception when
connecting to google? Mocks can easily create those error conditions that
are difficult to achieve with real objects.
E.g.
instance.should_receive(:send_to_google_checkout).once.
and_return { raise Google4R::Checkout::GoogleCheckoutError }
Some might point out that in the final test method we are hardly using
Google4R software at all, most of the code we interact with are mock objects.
Doesn't that defeat the purpose of testing?
The answer is simple. Always keep in mind what you are testing. The goal of
the TestPurchase test case is not the make sure the Google4R code is correct,
but that our Purchase class correctly interoperates with it. We do that by
carefully stating what methods are called with what arguments and what they
return. The test just checks that we are using to external software as we
expect it to. We don't actually care about the Google4R software itself in
this test case (presumably we do have tests that cover Google4R, but those are
different tests).
In the end, mock objects are a powerful tool to have in your testing toolbox.
|