All the examples shown so far illustrate various features of mocks and stubs. I’ll close this chapter with a bigger example that combines most of the techniques shown so far and is closer to what you’d write in a production application.
If you look back at listing 6.14, you’ll see that the basket class also contains the fullCheckout()method. This method does the following:
1 Checks the credit card of the customer. If the card is invalid or doesn’t have enough funds, the method stops there.
2 If the credit card is OK, the price for the products is reserved from the credit card. (This is called an authorization event in credit card terminology.)
3 The inventory is checked. If the products are in stock and can be shipped, the amount from the card that was previously reserved is now transferred to the account of the e-shop. (This is called a capturing event in credit card terminology.) In listing 6.12, you can see these two methods (for authori- zation and capturing) in the credit card processor class. Fig- ure 6.4 is a diagram of what you want to test.
Figure 6.4 Business requirements for credit card charging
Custom expression for both mocked arguments
Customer checks out products
Credit card is checked Card does not
have money
Card has money
Inventory is checked
Products are in stock
Credit card is charged No charge
185 Putting it all together: credit card charging in two steps
As a starting point, the first scenario that you’ll test is the case where the card doesn’t have enough money. The Spock test is shown in the next listing.
def "card has no funds"() {
given: "a basket, a customer and some products"
Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard") and: "a credit card service"
CreditCardProcessor creditCardSevice = Mock(CreditCardProcessor) basket.setCreditCardProcessor(creditCardSevice)
and: "a fully stocked warehouse"
WarehouseInventory inventory = Stub(WarehouseInventory) { isProductAvailable(_ , _) >> true isEmpty() >> false }
basket.setWarehouseInventory(inventory) when: "user checks out two products"
basket.addProduct tv basket.addProduct camera
boolean charged = basket.fullCheckout(customer)
then: "nothing is charged if credit card does not have enough money"
1 * creditCardSevice.authorize(1550, customer) >>
CreditCardResult.NOT_ENOUGH_FUNDS !charged
0 * _
}
The resulting code doesn’t have any surprises. Because you directly mock the credit card processor to assume that the card doesn’t have enough money, the charging pro- cess stops.
Things get more interesting if you want to write a unit test for the full scenario, where the card has money. The complicated part here is the two-step process between the authorize and capture steps. The reason for this is that the response from the first is a special token (assume that in this example it’s a single string). Then when the bas- ket calls the capture step, it must pass the same token to the credit card processor.
This way, the credit card processor can link the two events together and distinguish multiple capture events.
Listing 6.25 Using mocks and stubs in the same test
Create a Spock mock.
Stub the inventory to be full.
Trigger the tested action.
Mock the credit card to be invalid.
Verify that nothing was charged.
To further complicate things, assume also that the credit card processor wants the cur- rent date prepended to the token for logistical reasons. Figure 6.5 shows a sample conversation between the basket class and the credit card processor.
The respective unit test is shown next.
def "happy path for credit card sale"() {
given: "a basket, a customer and some products"
Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) BillableBasket basket = new BillableBasket()
Customer customer = new
Customer(name:"John",vip:false,creditCard:"testCard") and: "a credit card that has enough funds"
CreditCardProcessor creditCardSevice = Mock(CreditCardProcessor) basket.setCreditCardProcessor(creditCardSevice)
CreditCardResult sampleResult = CreditCardResult.OK sampleResult.setToken("sample");
and: "a warehouse"
WarehouseInventory inventory = Mock(WarehouseInventory) basket.setWarehouseInventory(inventory)
when: "user checks out two products"
basket.addProduct tv basket.addProduct camera
boolean charged = basket.fullCheckout(customer) then: "credit card is checked"
1 * creditCardSevice.authorize(1550, customer) >> sampleResult Listing 6.26 Verifying a sequence of events with interconnected method calls
Electronic basket
1. Authorize (amount, customer) OK - token: 45f89khg
2. Capture - token: 05/02/2015-45f89khg OK
Credit card processor
Figure 6.5 Two steps of charging a credit card with the same token
Mock the credit card service.
Create a sample credit card token.
Mock the warehouse.
Trigger the tested action.
Pass the sample token to the basket class.
187 Putting it all together: credit card charging in two steps
then: "inventory is checked"
with(inventory) { 2 * isProductAvailable(!null , 1) >> true _ * isEmpty() >> false
}
then: "credit card is charged"
1 * creditCardSevice.capture({myToken -> myToken.endsWith("sample")}, customer) >> CreditCardResult.OK charged
0 * _
}
This listing demonstrates several key points. First, this time the warehouse inventory is a mock instead of a stub because you want to verify the correct calling of its methods.
You also want to verify that it gets non-null arguments.
Mocks and stubs support the with() Spock method that was introduced in chapter 4. You’ve used it to group the two interactions of the warehouse inventory.
To verify that the basket class honors the token given back by the credit card pro- cessor, you create your own dummy token (named sample) and pass it to the basket when the authorization step happens. You can then verify that the token handed to the capture event is the same. Because the basket also prepends the token with the date (which is obviously different each time the test runs), you have to use the ends- With() method in the Groovy closure that matches the token.
And there you have it! You’ve tested two credit card scenarios without charging a real credit card and without calling the real credit card service, which might be slow to ini- tialize. As an exercise,11 feel free to create more unit tests to cover these scenarios:
■ The card becomes invalid between the authorize and capture steps.
■ The authorize step succeeds, but the inventory doesn’t have the products in stock.
Mocks and stubs are relevant only to the scenario being tested
If you look at listing 6.25, you’ll see that the warehouse is a stub. But in listing 6.26, it’s a mock. It’s therefore possible to create stubs of a specific class in one unit test, and mocks of the same class in another unit test, depending on your business needs.
Also, it’s possible to have Spock tests that use only stubs, tests that use only mocks, and tests that use both depending on the case (as you’ll see if you look back at the examples of this chapter). Use whatever you need according to the situation.
11A possible solution can be found in the source code of the book at GitHub.
Group interactions using with()
Verify that the inventory is queried twice (once for each product).
Verify that the previous token is reused by the basket class.
Verify that the credit card was charged.
Ensure that no other method from mocks was called.