A spy secretly obtains the information of a rival or someone very important. As the name suggests, a spy object spies on a real object. A spy is a variation of a stub, but instead of only setting the expectation, a spy records the method calls made to the collaborator. A spy can act as an indirect output of the unit under test and can also act as an audit log.
We'll create a spy object and examine its behavior; the following are the steps to create a spy object:
<work_space>
, and go to the 3605OS_TestDoubles
project.com.packt.testdoubles.spy
package and create a StudentService
class. This class will act as a course register service. The following is the code for the StudentService
class:public class StudentService { private Map<String, List<Student>> studentCouseMap = new HashMap<>(); public void enrollToCourse(String courseName,Student student){ List<Student> list = studentCouseMap.get(courseName); if (list == null) { list = new ArrayList<>(); } if (!list.contains(student)) { list.add(student); } studentCouseMap.put(courseName, list); } }
The StudentService
class contains a map of the course names and students. The enrollToCourse
method looks up the map; if no student is enrolled, then it creates a collection of students, adds the student to the collection, and puts the collection back in the map. If a student has previously enrolled for the course, then the map already contains a Student
collection. So, it just adds the new student to the collection.students
list.
enrollToCourse
method is a void
method and doesn't return a response. To verify that the enrollToCourse
method was invoked with a specific set of parameters, we can create a spy object. The service will write to the spy log, and the spy will act as an indirect output for verification. Create a spy object to register method invocations. The following code gives the method invocation details:class MethodInvocation { private List<Object> params = new ArrayList<>(); private Object returnedValue = null; private String method; public List<Object> getParams() { return params; } public MethodInvocation addParam(Object parm){ getParams().add(parm); return this; } public Object getReturnedValue() { return returnedValue; } public MethodInvocation setReturnedValue(Object returnedValue) { this.returnedValue = returnedValue; return this; } public String getMethod() { return method; } public MethodInvocation setMethod(String method) { this.method = method; return this; } }
The MethodInvocation
class represents a method invocation: the method name, a parameter list, and a return value. Suppose a sum()
method is invoked with two numbers and the method returns the sum of two numbers, then the MethodInvocation
class will contain a method name as sum
, a parameter list that will include the two numbers, and a return value that will contain the sum of the two numbers.
The following is the spy object snippet. It has a registerCall
method to log a method call instance. It has a map of strings and a List<MethodInvocation>
method. If a method is invoked 10 times, then the map will contain the method name and a list of 10 MethodInvocation
objects. The spy object provides an invocation method that accepts a method name and returns the method invocation count from the invocationMap
class:
public class StudentServiceSpy { private Map<String, List<MethodInvocation>> invocationMap = new HashMap<>(); void registerCall(MethodInvocation invocation) { List<MethodInvocation> list = invocationMap.get(invocation.getMethod()); if (list == null) { list = new ArrayList<>(); } if (!list.contains(invocation)) { list.add(invocation); } invocationMap.put(invocation.getMethod(), list); } public int invocation(String methodName){ List<MethodInvocation> list = invocationMap.get(methodName); if(list == null){ return 0; } return list.size(); } public MethodInvocation arguments(String methodName, int invocationIndex){ List<MethodInvocation> list = invocationMap.get(methodName); if(list == null || (invocationIndex > list.size())){ return null; } return list.get(invocationIndex-1); } }
The registerCall
method takes a MethodInvocation
object and puts it in a map.
StudentService
class to set a spy and log every method invocation to the spy object:private StudentServiceSpy spy; public void setSpy(StudentServiceSpy spy) { this.spy = spy; } public void enrollToCourse(String courseName, Student student) { MethodInvocation invocation = new MethodInvocation(); invocation.addParam(courseName).addParam(student).setMethod("enrollToCourse"); spy.registerCall(invocation); List<Student> list = studentCouseMap.get(courseName); if (list == null) { list = new ArrayList<>(); } if (!list.contains(student)) { list.add(student); } studentCouseMap.put(courseName, list); }
public class StudentServiceTest { StudentService service = new StudentService(); StudentServiceSpy spy = new StudentServiceSpy(); @Test public void enrolls_students() throws Exception { //create student objects Student bob = new Student("001", "Robert Anthony"); Student roy = new Student("002", "Roy Noon"); //set spy service.setSpy(spy); //enroll Bob and Roy service.enrollToCourse("english", bob); service.enrollToCourse("history", roy); //assert that the method was invoked twice assertEquals(2, spy.invocation("enrollToCourse")); //get the method arguments for the first call List<Object> methodArguments = spy.arguments ("enrollToCourse", 1).getParams(); //get the method arguments for the 2nd call List<Object> methodArguments2 = spy.arguments ("enrollToCourse", 2).getParams(); //verify that Bob was enrolled to English first assertEquals("english", methodArguments.get(0)); assertEquals(bob, methodArguments.get(1)); //verify that Roy was enrolled to history assertEquals("history", methodArguments2.get(0)); assertEquals(roy, methodArguments2.get(1)); } }