Exploring a test spy
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:
- Launch Eclipse, open
<work_space>
, and go to the3605OS_TestDoubles
project. - Create a
com.packt.testdoubles.spy
package and create aStudentService
class. This class will act as a course register service. The following is the code for theStudentService
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. TheenrollToCourse
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 aStudent
collection. So, it just adds the new student to thecollection.students
list. - The
enrollToCourse
method is avoid
method and doesn't return a response. To verify that theenrollToCourse
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 asum()
method is invoked with two numbers and the method returns the sum of two numbers, then theMethodInvocation
class will contain a method name assum
, a parameter list that will include the two numbers, and a return value that will contain the sum of the two numbers.Note
Note that the setter methods return
this(MethodInvocation)
. This coding approach is known as builder pattern. It helps to build an object in multiple steps. JavaStringBuilder
is an example of such a use:StringBuilder builder = new StringBuilder(); builder.append("step1").append("step2")…
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 aList<MethodInvocation>
method. If a method is invoked 10 times, then the map will contain the method name and a list of 10MethodInvocation
objects. The spy object provides an invocation method that accepts a method name and returns the method invocation count from theinvocationMap
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 aMethodInvocation
object and puts it in a map. - Modify the
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); }
- Write a test to examine the method invocation and arguments. The following JUnit test uses the spy object and verifies the method invocation:
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)); } }