Controlling input to the class under test with stubs

Một phần của tài liệu Manning java testing with spock (Trang 188 - 198)

Now that you know the theory behind mocks and stubs and the system that you’ll test, you’re ready to see how to use them in your Spock tests. Let’s start with stubs, which are simpler. You stub all collaborator classes that are used by your class under test but aren’t otherwise tested. Either they have their own unit tests or they’re external librar- ies and frameworks that are assumed to work correctly.

In general, your class under test makes requests to your stubs. You need to tell Spock what to do when any of the stubbed methods are called. By default, Spock won’t complain if a method is called that wasn’t explicitly stubbed.

Therefore, creating a stub is a two-step process:

1 Showing Spock which class won’t use its real implementation and instead will be stubbed

2 Declaring what will happen when any of the stubbed methods are called by the class under test (what the return values will be)

6.2.1 Basic stubbing of return values

The first thing you want to test is the canShipCompletely() method of the basket from listing 6.1. This method returns true when all products selected by the customer are available in the warehouse, and false in any other case. You’ll stub the warehouse inventory so that you can emulate both cases: the product is in stock and the product isn’t currently available.

The warehouse inventory is a concrete class that’s used by the basket class during checkout. Imagine that this class is part of an external system that you don’t have any control over. You don’t want your unit tests to be based on the real inventory of this e-shop. You need a way to trick the inventory to contain what you want for each busi- ness scenario that you test. The following listing shows an example of stubbing the warehouse inventory.

def "If warehouse is empty nothing can be shipped"() { given: "a basket and a TV"

Product tv = new Product(name:"bravia",price:1200,weight:18) Basket basket = new Basket()

and:"an empty warehouse"

WarehouseInventory inventory = Stub(WarehouseInventory) Listing 6.2 Creating a simple stub with Spock

Creates Spock stub

inventory.isEmpty() >> true basket.setWarehouseInventory(inventory) when: "user checks out the tv"

basket.addProduct tv

then: "order cannot be shipped"

!basket.canShipCompletely() }

The most important lines from listing 6.2 are the following:

WarehouseInventory inventory = Stub(WarehouseInventory) inventory.isEmpty() >> true

The first line creates a Spock stub that looks and “acts” as the class WarehouseInven- tory, but all methods that are called on this stub are intercepted automatically by Spock and never reach the real implementation.

The second line uses the right-shift operator. This special Spock operator (remem- ber that Groovy allows for operator overloading, unlike Java) hardwires the isEmpty() method to return true regardless of the real implementation of the original class.

When the basket is asked to respond about the shipping status of the order it calls (behind the scenes), the stubbed method gets a negative result from the inventory.

To stub a method for specific arguments, you can use the right-shift operator directly on the method call you want to emulate, as shown in the next listing.

def "If warehouse has the product on stock everything is fine"() { given: "a basket and a TV"

Product tv = new Product(name:"bravia",price:1200,weight:18) Basket basket = new Basket()

and:"a warehouse with enough stock"

WarehouseInventory inventory = Stub(WarehouseInventory) inventory.isProductAvailable("bravia",1) >> true

inventory.isEmpty() >> false basket.setWarehouseInventory(inventory)

when: "user checks out the tv"

basket.addProduct tv

then: "order can be shipped right away"

basket.canShipCompletely() }

Here you change the inventory to emulate the happy scenario in which the product exists in the warehouse. It’s also possible to differentiate method calls according to their arguments and stub them with different return results. This is demonstrated in the following listing.

Listing 6.3 Stubbing specific arguments

Instructs the stub to return true when isEmpty() is called Injects the

stub into the class under test

Calls the stub behind the scenes

Creating a Spock stub

Instructing the stub to return true when specific arguments are used Instructing

the warehouse to respond with false

165 Controlling input to the class under test with stubs

def "If warehouse does not have all products, order cannot be shipped"() { given: "a basket, a TV and a camera"

Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) Basket basket = new Basket()

and:"a warehouse with partial availability"

WarehouseInventory inventory = Stub(WarehouseInventory)

inventory.isProductAvailable("bravia",1) >> true inventory.isProductAvailable("panasonic",1) >> false inventory.isEmpty() >> false

basket.setWarehouseInventory(inventory) when: "user checks out both products"

basket.addProduct tv basket.addProduct camera

then: "order cannot be shipped right away"

!basket.canShipCompletely() }

Finally, you can group all stubbing instructions in a single code block in a similar way to the with() method shown in chapter 4. The following code listing behaves in exactly the same way as listing 6.4; only the syntax differs. You should decide for your- self which of the two you prefer.

def "If warehouse does not have all products, order cannot be shipped (alt)"() {

given: "a basket, a TV and a camera"

Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) Basket basket = new Basket()

and:"a warehouse with partial availability"

WarehouseInventory inventory = Stub(WarehouseInventory) { isProductAvailable("bravia",1) >> true isProductAvailable("panasonic",1) >> false isEmpty() >> false }

basket.warehouseInventory = inventory when: "user checks out both products"

basket.addProduct tv basket.addProduct camera

then: "order cannot be shipped right away"

!basket.canShipCompletely() }

Listing 6.4 Argument-based stub differentiation

Listing 6.5 Grouping all stubbed methods

Different stub results depending on the argument

Compact way of stubbing methods Setter injection using Groovy style

Notice that in all the preceding code listings, the real code of warehouse inventory never runs. The Spock unit tests shown can run on their own, regardless of the status of the real inventory. As long as the signature of the warehouse class stays the same (that is, the method definitions), these unit tests will continue to run correctly, even if new methods are added to the original class. Now you know how you can stub classes in Spock!

6.2.2 Matching arguments leniently when a stubbed method is called

The previous section showed how to stub methods by using the exact arguments you expect to be called. This works for trivial tests, but for bigger tests, this precision isn’t always needed. For example, if I wanted to create a unit test that involved 10 different products, I’d have to stub 10 different calls for the same method.

Spock offers a more practical solution in the form of argument matchers when you don’t want so much detail. The character Spock uses is the underscore (_), and in general it plays the role of “I don’t care what goes in here,” depending on the context (as you’ll see throughout this chapter). The following listing shows the use of the underscore as an argument matcher.

def "If warehouse has both products everything is fine"() { given: "a basket, a TV and a camera"

Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) Basket basket = new Basket()

and:"a warehouse with enough stock"

WarehouseInventory inventory = Stub(WarehouseInventory) inventory.isProductAvailable(_, 1) >> true basket.setWarehouseInventory(inventory)

when: "user checks out the tv and the camera"

basket.addProduct tv basket.addProduct camera

then: "order can be shipped right away"

basket.canShipCompletely()

Here I’ve stubbed the inventory method only once, and I know that it can be called for all products I ask for, regardless of their names.4 I’ve chosen this approach because in this particular test I’m not interested in examining the correctness of the ware- house (the focus of the test is still the basket class).

Listing 6.6 Using argument matchers in stubs

4 Unlike Mockito, Spock supports partial matching of arguments, where some have specific values and some don’t. Mockito requires that all arguments use matchers if any matcher is used at all.

Stubbing a method call regardless of the value of an argument

167 Controlling input to the class under test with stubs

It’s also possible to use matchers for all arguments of a method, resulting in power- ful stubbing combinations. The following listing shows an example.

def "If warehouse is fully stocked stock everything is fine"() { given: "a basket, a TV and a camera"

Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) Basket basket = new Basket()

and:"a warehouse with limitless stock"

WarehouseInventory inventory = Stub(WarehouseInventory) inventory.isProductAvailable( _, _) >> true basket.setWarehouseInventory(inventory)

when: "user checks out multiple products"

basket.addProduct tv,33 basket.addProduct camera,12 then: "order can be shipped right away"

basket.canShipCompletely()

Here I’ve instructed my warehouse to answer that the product is always in stock, regardless of the product. I don’t care if the class under test asks for a TV or a camera;

it will always be in stock.

Stubbing a method regardless of its arguments is a powerful technique that can be helpful in large unit tests in which the stub is a secondary dependency that’s outside the focus of the test. Dispatchers, delegates, facades, decorators, and other design pat- terns are perfect candidates for this kind of stubbing, as often they get in the way of the class under test.

6.2.3 Using sequential stubs with different responses for each method call

Listing 6.5 showed how to differentiate the stub response based on the argument.

This is one dimension of different responses. The other dimension is to stub different responses depending on the number of times a method is called.

This is accomplished with the unsigned right-shift operator (>>>), which was intro- duced in chapter 3. An example is shown in the next listing.

def "Inventory is always checked in the last possible moment"() { given: "a basket and a TV"

Product tv = new Product(name:"bravia",price:1200,weight:18) Basket basket = new Basket()

and:"a warehouse with fluctuating stock levels"

WarehouseInventory inventory = Stub(WarehouseInventory)

inventory.isProductAvailable( "bravia", _) >>> true >> false Listing 6.7 Ignoring all arguments of a stubbed method when returning a response

Listing 6.8 Stubbing subsequent method calls

Stubbing a method for all its possible arguments Both these calls

will be matched.

First call will return true, and second will return false.

inventory.isEmpty() >>> [false, true]

basket.setWarehouseInventory(inventory) when: "user checks out the tv"

basket.addProduct tv

then: "order can be shipped right away"

basket.canShipCompletely() when: "user wants another TV"

basket.addProduct tv

then: "order can no longer be shipped"

!basket.canShipCompletely() }

The unsigned shift operator signifies to Spock that the expression following it will be used as a response for each subsequent call of the exact same method. Multiple answers can be chained together by using the normal shift operator, >>. In this listing this happens with the isProductAvailableMethod(). The first time it will return true and the second time it will return false to a query for a TV.

An alternative syntax (and the one I prefer) is to use a collection after the unsigned shift operator. Each item of the collection will be used in turn as a response when the stubbed method is called. As with parameterized tests, remember that Groovy has a more general idea of “iterable” things than Java, so you don’t have to use a list as shown in listing 6.8.

6.2.4 Throwing exceptions when a stubbed method is called

I said in the introduction of this chapter that stubs are essential if you want to emulate a hard-to-reproduce situation or a corner case that doesn’t typically happen with pro- duction code. For large-scale applications, when the code that handles error condi- tions can easily outweigh the code for happy-path scenarios, it’s essential to create unit tests that trigger those error conditions.

In the most common case, the error conditions come in the form of Java excep- tions. These can be easily emulated, as shown in the following listing.

def "A problematic inventory means nothing can be shipped"() { given: "a basket and a TV"

Product tv = new Product(name:"bravia",price:1200,weight:18) Basket basket = new Basket()

and:"a warehouse with serious issues"

WarehouseInventory inventory = Stub(WarehouseInventory) inventory.isProductAvailable( "bravia", _) >> { throw new RuntimeException("critical error") } basket.setWarehouseInventory(inventory)

Listing 6.9 Instructing stubs to throw exceptions

Spock can also iterate on a collection for ordered responses.

The inventory stub is called for the first time behind the scenes.

The inventory stub is called a second time.

Stub is instructed to throw an exception

169 Controlling input to the class under test with stubs

when: "user checks out the tv"

basket.addProduct tv

then: "order cannot be shipped"

!basket.canShipCompletely() }

The basket class calls the warehouse class, and if anything goes wrong (even if an exception is thrown), the canShipCompletely() method recovers by returning false (while leaving the basket class in a valid state). To verify this capability of the basket class, you need to instruct the warehouse to throw an exception when the stubbed method is called.

You still use the right-shift operator (>>) for stubbing, but instead of returning a standard value as in the previous examples, you can place any Java code inside the brackets (which in reality is a Groovy closure, as you know if you paid attention in chapter 2).

Inside the brackets, you can put any code with Java statements, so a useful capabil- ity is to throw an exception. The beauty of Spock5 is that throwing exceptions isn’t something extraordinary that requires special syntax. Instead, you’re offered a generic way to do anything when a stubbed method is called, and throwing an excep- tion is one possibility of many.

The great power of using Groovy closures in Spock stubs is revealed fully in the next section.

6.2.5 Using dynamic stubs that check arguments when responding

The previous section showed how to throw an exception in a stub by using a Groovy closure. I consider Groovy closures a powerful feature, even in the presence of Java 8, because the way Groovy closures are used in Spock is refreshing. In other mocking frameworks, such as Mockito, you need to learn separate syntax semantics for argu- ment catchers, exception throwing, and dynamic responses. In Spock, all these are unified under Groovy closures.

Before I show any code, I’ll repeat the suggestion in chapter 2. If you don’t feel comfortable with Groovy closures (or Java 8 lambda expressions), feel free to skip this section and come back later. Closures can also be used with mocks, as you’ll see later in this chapter.

5 Thanks to Groovy's way of handling all exceptions as unchecked.

Closures—the Swiss army knife of Groovy

I briefly talked about closures in chapter 2. In their simplest form, you can think of them as anonymous Java functions with greater flexibility. If you’ve already worked with Java 8 and lambda expressions, Groovy closures will be familiar.

Ensures that the basket class can recover from the exception

To make this example more interesting, I’ll add another dependency to the basket class. This time, it will be an interface (instead of a concrete class), as shown here:

public interface ShippingCalculator {

int findShippingCostFor(Product product, int times);

}

This interface is responsible for shipping charges. It accepts a product and the num- ber of times it was added in the basket, and returns the shipping costs (in whatever currency the e-shop uses). The basket class is also augmented with a findTotalCost() method that calls the shipping calculator behind the scenes in order to add shipping charges to the product value. You want to test this new method of the basket class.

As a business scenario, you choose a simple shipping strategy. For each product, you add 10 dollars to the final cost for each time it’s added to the basket, regardless of product type.6 A naive way of stubbing the shipping calculator would be the following:

ShippingCalculator shippingCalculator = Stub(ShippingCalculator) shippingCalculator.findShippingCostFor(tv, 2) >> 20

shippingCalculator.findShippingCostFor(camera, 2) >> 20 shippingCalculator.findShippingCostFor(hifi, 1) >> 10 shippingCalculator.findShippingCostFor(laptop, 3) >> 30

Here you instruct the shipping module with specific responses according to the argu- ments of the called method. This code isn’t readable and clearly suffers from verbos- ity. Writing unit tests for a large number of products will also be difficult (imagine a unit test that adds 100 products from an external source).

With the power of closures, Spock allows you to capture a simple pricing strategy with a single line of code! The following listing demonstrates this technique and shows that Spock can stub both interfaces and concrete classes in an agnostic way.

def "Basket handles shipping charges according to product count"() { given: "a basket and several products"

Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) Product hifi = new Product(name:"jvc",price:600,weight:5)

Product laptop = new Product(name:"toshiba",price:800,weight:10) Basket basket = new Basket()

and: "a fully stocked warehouse"

WarehouseInventory inventory = Stub(WarehouseInventory) inventory.isProductAvailable( _ , _) >> true

basket.setWarehouseInventory(inventory)

and: "a shipping calculator that charges 10 dollars for each product"

ShippingCalculator shippingCalculator = Stub(ShippingCalculator)

6 Not really sustainable for a true shop, but it’s sufficient for illustration purposes.

Listing 6.10 Stubs that respond according to arguments

Stubbing a concrete class

Stubbing a Java interface

171 Controlling input to the class under test with stubs

shippingCalculator.findShippingCostFor( _, _) >> { Product product, int count -> 10 * count}

basket.setShippingCalculator(shippingCalculator)

when: "user checks out several products in different quantities"

basket.addProduct tv, 2 basket.addProduct camera, 2 basket.addProduct hifi basket.addProduct laptop, 3 then: "cost is correctly calculated"

basket.findTotalCost() == 2 * tv.price + 2 * camera.price + hifi.price + 3 * laptop.price + basket.getProductCount() * 10 }

Using the Groovy closure, you’ve instrumented the shipping calculator stub with your selected pricing strategy in a single line of code. With that one line, the shipping cal- culator can respond to 1, 2, or 100 products added to the basket. Its behavior is no longer statically defined, but it can understand its arguments.

For an even smarter stub, assume that the e-shop also sells downloadable goods. For these, the shipping cost should obviously be zero. Again, a naive way to cater to this case would be to instruct your stub with specific products to return 0—for example:

shippingCalculator.findShippingCostFor(ebook, _) >> 0

Doing this wouldn’t be scalable because if a unit test examines 10 different download- able goods, you’ll have to manually stub the response 10 times. Remember that the interface for the shipping calculator also gives you access to the product, as well as the number of times it was added to the basket. Therefore, you can modify the Groovy clo- sure to look at both arguments, as shown in the next listing.

def "Downloadable goods do not have shipping cost"() { given: "a basket and several products"

Product tv = new Product(name:"bravia",price:1200,weight:18) Product camera = new Product(name:"panasonic",price:350,weight:2) Product hifi = new Product(name:"jvc",price:600,weight:5)

Product laptop = new Product(name:"toshiba",price:800,weight:10)

Product ebook = new Product(name:"learning exposure",price:30,weight:0) Product suite = new Product(name:"adobe essentials",price:200,weight:0) Basket basket = new Basket()

and: "a fully stocked warehouse"

WarehouseInventory inventory = Stub(WarehouseInventory) inventory.isProductAvailable( _ , _) >> true

basket.setWarehouseInventory(inventory)

and: "a shipping calculator that charges 10 dollars for each physical product"

ShippingCalculator shippingCalculator = Stub(ShippingCalculator) Listing 6.11 A smart stub that looks at both its arguments

Using a Groovy closure for a dynamic response

Adding different quantities to the basket Verifying that

shipping charges are included

Stubbing a Java interface

Một phần của tài liệu Manning java testing with spock (Trang 188 - 198)

Tải bản đầy đủ (PDF)

(306 trang)