In this section you’ll see how to create partial mocks.8 Chapter 6 explained how Spock can create fake objects that are useful for testing and showed you mocks and stubs.
Spock supports a third type of “fake” object: spies.
Spies, shown in figure 8.5, work as partial mocks. They take over a Java object and mock only some of its methods. Method calls can either by stubbed (like mocks) or can pass through to the real object.
I purposely didn’t show you spies in chapter 6 because they’re a controversial tech- nique that implies problematic Java code. They can be useful in a narrow set of cases.
Their primary use is in creating unit tests for badly designed production code that can’t be refactored (a common scenario with legacy code).
8.3.1 A sample application with special requirements
Let's see an example that’s well-suited for writing a Spock test with spies instead of mocks/stubs. Say you’re tasked with the development of unit tests for an existing Java application. The Java application in question is a security utility that gets video feed from an external camera, and upon detecting intruders, deletes all files of the hard drive (to hide incriminating evidence).
The application code is implemented by two Java classes. The first class is responsi- ble for deleting the hard drive, and the second class implements the face-recognition algorithms that decide whether the person in front of the camera is a friend or enemy, as shown in the next listing.
public class CameraFeed { [...code redacted for brevity...]
public void setCurrentFrame(Image image){
[...code redacted for brevity...]
} }
8 And why you shouldn’t use them!
Listing 8.23 Java code with questionable design
Java class
Mock/
stub Spy
Partial fake Fake
Real object
Figure 8.5 A spy is a real class in which only a subset of methods are fake. The rest are the real methods.
Gets frames from video camera
public class HardDriveNuker { public void deleteHardDriveNow(){
[...code redacted for brevity...]
} }
public class SmartHardDriveNuker extends HardDriveNuker{
public void activate(CameraFeed cameraFeed){
[...code redacted for brevity...]
} }
You should instantly see the flawed design of this Java code. Figure 8.6 provides an overview.
The application doesn’t use dependency injection. Instead of splitting responsibil- ities into separate entities, the application contains both the logic of deletion and the face recognition in a single “object.”
You see this design flaw and start refactoring the application in order to write your unit tests. Unfortunately, your boss says that the binary application is digitally signed, and changing even the slightest thing in the source code will create an invalid signa- ture.9 Your boss adds that even if you successfully refactor the code, your department
9 My example is a bit extreme. Usually code can’t be changed for political reasons.
Responsible for hard disk deletion Immediately
deletes the hard drive
Contains complex image- recognition logic Calls deleteHardDriveNow() behind the scenes
Face detection from video
camera
Deletes hard drive SmartHardDriveNuker.java
CameraFeed
HardDriveNuker.java
Figure 8.6 Hard drive deletion logic is hidden inside the face-recognition logic
247 Creating partial mocks with spies
doesn’t have access to the digital certificate, so you couldn’t re-sign the binary after your change.
You need to write a unit test with the source code as is. You’re asked to examine the effectiveness of the face-recognition software by using images of both kinds (those that have a threat and those that don’t). This is one of the rare occasions that spies can be employed for unit testing.
8.3.2 Spies with Spock
You need to write a unit test that examines the activate() method of the SmartHard- DriveNuker class. You know that behind the scenes it calls the deleteHardDriveNow() method. It wouldn’t be realistic to delete your hard drive each time you write a unit test that triggers the face-recognition logic. You need to find a way to mock the dan- gerous method while the real method of the face-recognition logic is kept as is.
Spock supports the creation of spies, as shown in the next listing. A spy is a fake object that automatically calls the real methods of a class unless they’re explicitly mocked.
def "automatic deletion of hard disk when agents are here"() { given: "a camera feed"
CameraFeed cameraFeed = new CameraFeed() and: "the auto-nuker program"
SmartHardDriveNuker nuker = Spy(SmartHardDriveNuker)
nuker.deleteHardDriveNow() >> {println "Hard disk is cleared"}
when:"agents are knocking the door"
cameraFeed.setCurrentFrame(ImageIO.read(getClass().getResourceAsStream(
"agents.jpg")))
nuker.activate(cameraFeed);
then: "all files of hard drive should be deleted"
1 * nuker.deleteHardDriveNow() }
Here you create a spy of your class under test. By default, after creation, all methods are real and pass through to the real object.10 Then you specifically mock the method that deletes the hard drive. But the method that employs the face-recognition logic is still the real one.
When the activate() method is called, it runs its real code (so you can pass it dif- ferent images and test the effectiveness of the face-recognition code). In the case of an image that represents a “threat” and so triggers the hard drive deletion process, you know that the mocked method will be called (and thus your hard drive is safe).
Listing 8.24 Creating a spy with Spock
10Creating a spy without mocking any method is the same as using the object itself—not very exciting.
Creates a spy for the SmartHardDriveNuker class Mocks the
dangerous method—all other methods are real
Real face-recognition code runs
Examines the mocked method
This listing shows only one test, but in reality you’d need to write a parameterized test with multiple images that examines the behavior of the face-recognition code.
8.3.3 The need for spies shows a problematic code base
Spies are used for legacy code primarily because of the bad quality of legacy code.11 Well-designed code doesn’t ever need spies in the first place. Figure 8.7 shows a flow diagram of using spies that you should keep in your head at all times. The diagram isn’t specific to Spock. It applies to all testing frameworks (including Mockito).
In the example of the security utility, a spy is essential because the Java code doesn’t use dependency injection. This is just one of the code smells of badly designed code.
Java code that comes as a big ball of mud,12 breaks the SOLID principles,13 contains God14 objects, and generally suffers from big design flaws isn’t directly testable with mocks/stubs, and spies are needed.
In those cases, you should resist the temptation to write a Spock test with spies and instead refactor the code before writing your unit tests. You’ll find that in most cases (if not all), spies aren’t needed after the refactoring is complete.
8.3.4 Replacement of spies with mock
You use spies with the security utility because you can’t refactor the Java code first, as this would invalidate the digital signature of the binary. If that constraint didn’t hold,
11A universal fact: legacy code is always badly designed code.
12https://en.wikipedia.org/wiki/Big_ball_of_mud
13https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)
14https://en.wikipedia.org/wiki/God_object
No Yes
Can you refactor
your Java code?
You think you need to use spies
Use spies Refactor it?
Use mocks/stubs You improved the design of your code!
Your Java code is not OOP
Figure 8.7 Spies can always be replaced with mocks in well-designed code.
249 Creating partial mocks with spies
you’d instead modify the Java code to properly use dependency injection. An obvious decoupling of dependencies is shown in the following listing.
public class SmartHardDriveNuker{
private final HardDriveNuker hardDriveNuker;
public SmartHardDriveNuker(final HardDriveNuker hardDriveNuker) {
this.hardDriveNuker = hardDriveNuker;
}
public void activate(CameraFeed cameraFeed) {
[...code redacted for brevity..]
hardDriveNuker.deleteHardDriveNow();
[...code redacted for brevity..]
} }
Here you refactor your Java code to use composition instead of inheritance. You also introduce the “dangerous” hard drive deletion code as an external dependency. After this refactoring, you can rewrite your unit test by using a normal mock, as shown in the next listing.
def "automatic deletion of hard disk when agents are here"() { given: "a camera feed and a fake nuker"
CameraFeed cameraFeed = new CameraFeed()
HardDriveNuker nuker = Mock(HardDriveNuker) and: "the auto-nuker program"
SmartHardDriveNuker smartNuker = new SmartHardDriveNuker(nuker) when:"agents are knocking the door"
cameraFeed.setCurrentFrame(ImageIO.read(getClass().getResourceAsStream(
"agents.jpg")))
smartNuker.activate(cameraFeed);
then: "all files of hard drive should be deleted"
1 * nuker.deleteHardDriveNow() }
If you’ve read chapter 6, the code in listing 8.26 should be easy to understand.
Because you’ve refactored the Java code and hard drive deletion is now an external dependency, you can mock that class and pass it the face-recognition code. This way,
Listing 8.25 Refactoring Java code to avoid spies
Listing 8.26 Using a mock instead of a spy
No inheritance is used.
Code reuse via composition
Gets hard drive nuker via constructor injection
Calls the dangerous method of the external dependency
Uses a mock instead of a spy Mock is passed
as a dependency
Calls the mocked nuker class behind the scenes Examines the interaction of the mock
your class under test—SmartHardDriveNuker—is a real one, and a mock is used for the collaborator class: HardDriveNuker.
The end result is that no spies are used. What you need to take away from this sec- tion of the book is that despite Spock support for spies, you should avoid using them, and instead spend time improving the design of your code so that spies aren’t needed.
And with that knowledge about spies, we conclude this book! You can now put it down and go write your own Spock tests!