Mockito

Mockito Spy Example

In this article, I am going to show you an example of Mockito Spy calls.

There are times when we would like to use most of the original object’s behavior but mock only a portion of it. This is called spying objects, also called partial mocking. Before I start with the example, let me first brief you about my setup:

1. Example of Mockito Spy

Using Mockito’s spy feature, we can mock only those methods of a real object that we want to, thus retaining the rest of the original behavior.

The system under test is an Employee bean which takes in firstName, lastName and age. It has the getter methods for all the attributes. It also has an additional getter method getFullName() which returns us firstName and lastName together. Internally, it relies on the respective getter methods rather than accessing attributes directly. Finally, it also has a setter method on age attribute. Why a setter method on the age attribute? The answer is, purely to prove a point regarding how the spy works and there is no design secret behind it.

In the below example, I will show you how to set the expected behavior on a couple of Employee bean methods.

Employee:

package com.javacodegeeks.mockito;

import java.util.ArrayList;
import java.util.List;

public class Employee {
	private String firstName;
	private String lastName;
	private int age;

	public Employee(String firstName, String lastName, int age) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.age = age;
		this.engineerAware = new Dev();
	}

	public int getAge() {
		return age;
	}

	public String getFullName() {
		return getFirstName() + " " + getLastName();
	}

	public String getLastName() {
		return lastName;
	}

	public String getFirstName() {
		return firstName;
	}	
	
	public void setAge(int age) {
		this.age = age;
	}
}

Let’s analyze the test cases now.

We create the spy object using org.mockito.Mockito.spy(real object). In our example, we do it in the @BeforeMethod, buildSpy(). We create the Employee bean and then the spy object using spy(emp).

Since we have two Employee beans, one the original and the other spy, one question that naturally arises is, whether the spy object refers to the original object internally. The answer is No. Mockito creates a copy of the original object, so when methods are exercised on the spy object, the state of the original object remains unaffected. Likewise, if you interact with the real object, the object won’t be aware of those interactions.

This is proved in verifySpyEffectOnRealInstance() where we set the age on the spied Employee bean, but the state of the real Employee bean still retains the original age. In the next two test cases, we will analyze the verification and partial mocking.

MockitoSpyExample:

package com.javacodegeeks.mockito;

import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

import org.mockito.InOrder;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class MockitoSpyExample {
	private Employee spyEmp;
	private Employee emp;
	private static final String FIRST_NAME = "Joe";
	private static final String LAST_NAME = "M";
	private static final int AGE = 35;
	
	@BeforeMethod
	public void buildSpy() {
		emp = new Employee(FIRST_NAME, LAST_NAME, AGE);
		spyEmp = spy(emp);
	}
	
	@Test
	public void verifySpyEffectOnRealInstance() {
		spyEmp.setAge(20);
		assertFalse(emp.getAge() == spyEmp.getAge());
	}
	
	@Test
	public void verifyEmployeeDetails() {
		System.out.println("Full name:" + spyEmp.getFullName());
		System.out.println("Age:" + spyEmp.getAge());
		
		InOrder inOrder = inOrder(spyEmp);
		
		System.out.println("Verify emp.getFullName() calls getFirstName() and then getLastName()");
		inOrder.verify(spyEmp).getFirstName();
		inOrder.verify(spyEmp).getLastName();
		
		System.out.println("Verify emp.getAge() is called");
		verify(spyEmp).getAge();
		
		assertEquals(spyEmp.getFirstName(), FIRST_NAME);
		assertEquals(spyEmp.getLastName(), LAST_NAME);
		assertEquals(spyEmp.getFullName(), FIRST_NAME + " " + LAST_NAME);
		assertEquals(spyEmp.getAge(), AGE);
		
		System.out.println("Verify emp.getFullName() called twice");
		verify(spyEmp, times(2)).getFullName();
	}
	
	@Test
	public void spyEmployeeName() {
		final String I_AM = "I am";
		final String THE_SPY = "the Spy";
		System.out.println("Train employee to return " + I_AM + " when emp.getFirstName() is called");
		when(spyEmp.getFirstName()).thenReturn(I_AM);
		System.out.println("Full Name: " + spyEmp.getFullName());
		assertEquals(I_AM + " M", spyEmp.getFullName());
		
		System.out.println("Train employee to return " + THE_SPY + " when emp.getLastName() is called");
		when(spyEmp.getLastName()).thenReturn(THE_SPY);
		System.out.println("Full Name: " + spyEmp.getFullName());
		assertEquals(I_AM + " " + THE_SPY, spyEmp.getFullName());
	}	
}

In verifyEmployeeDetails(), we call getFullName() on the spied object. We know internally it calls getFirstName() and getLastName(). We can verify this and the order in which they are called using InOrder. The spy object is encapsulated in InOrder object and then we verify the order of interaction of getFirstName() and getLastName().

We can also verify the method invocations by their count. For example, verify(spyEmp, times(2)).getFullName().

In spyEmployeeName(), we do the partial mocking. We train the spied Employee bean to return “I am” when getFirstName() is called. Since getFullName() calls getFirstName() and getLastName(), we can see the change in its full name when getFullName() is called. It returns “I am M”.

Next, we train the getLastName() to return the spy. This again reflects in the full name and returns “I am the spy”.

Output:

PASSED: verifySpyEffectOnRealInstance

Train employee to return I am when emp.getFirstName() is called
Full Name: I am M
Train employee to return the Spy when emp.getLastName() is called
Full Name: I am the Spy
PASSED: spyEmployeeName

Full name:Joe M
Age:35
Verify emp.getFullName() calls getFirstName() and then getLastName()
Verify emp.getAge() is called
Verify emp.getFullName() called twice
PASSED: verifyEmployeeDetails

2. Spying on Interface

In the below example, I show you that one can even spy on an anonymous object. In the test case spyOnInterface, we create a spy object on EngineerAware implementation called Dev. We can see it returns us the expected enum Engineer.DEV. Next, we train the spy object to return Engineer.QA, which it does.

EngineerAware:

package com.javacodegeeks.mockito;

public interface EngineerAware {
	Engineer getDesignation();
	
	enum Engineer {
		DEV,QA
	}
}

SpyOnInterfaceExample:

package com.javacodegeeks.mockito;

import org.testng.annotations.Test;

import com.javacodegeeks.mockito.EngineerAware.Engineer;

import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

public class SpyOnInterfaceExample {
	@Test
	public void spyOnInterface() {
		EngineerAware engineerAware = spy(new Dev());
		assertEquals(Engineer.DEV, engineerAware.getDesignation());
		when(engineerAware.getDesignation()).thenReturn(Engineer.QA);
		assertEquals(Engineer.QA, engineerAware.getDesignation());
	}	
	private class Dev implements EngineerAware {
		@Override
		public Engineer getDesignation() {
			return Engineer.DEV;
		}		
	}
}

3. Stubbing a final method in a Spy Object

In this Mockito spy example, I demonstrate that we can’t train a final method. Method moveTo(), updates employee’s designation. We also have another method, finalMoveTo() which does the same as moveTo () but is a final method.
In our test cases, we will try to train the final method and see how it behaves.

Employee:

package com.javacodegeeks.mockito;

import java.util.ArrayList;
import java.util.List;

public class Employee {
	private String firstName;
	private String lastName;
	private int age;
	private EngineerAware engineerAware;
	private List skills;

	public Employee(String firstName, String lastName, int age) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.age = age;
		this.engineerAware = new Dev();
	}

	public int getAge() {
		return age;
	}

	public String getFullName() {
		return getFirstName() + " " + getLastName();
	}

	public String getLastName() {
		return lastName;
	}

	public String getFirstName() {
		return firstName;
	}	
	
	public void setAge(int age) {
		this.age = age;
	}

	public final void finalMoveTo(EngineerAware engineerAware) {
		System.out.println("Employee moves from " + this.engineerAware.getDesignation() + " to " + engineerAware.getDesignation());
		this.engineerAware = engineerAware;
	}
	
	public void moveTo(EngineerAware engineerAware) {
		System.out.println("Employee moves from " + this.engineerAware.getDesignation() + " to " + engineerAware.getDesignation());
		this.engineerAware = engineerAware;
	}
	
	private class Dev implements EngineerAware {
		@Override
		public Engineer getDesignation() {
			return Engineer.DEV;
		}		
	}
}
  1. In the test case, stubNonFinalMoveTo(), we train moveTo() method to throw a RuntimeException.
  2. In stubFinalMoveTo(), we do the same with the final method finalMoveTo().

Since finalMoveTo() is final, Mockito fails to train it and instead simply invokes the real object’s method, which is why it fails to throw RuntimeException.

StubOnFinalMethod:

package com.javacodegeeks.mockito;

import static org.mockito.Mockito.*;

import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class StubOnFinalMethod {
	private Employee emp;
	private static final String FIRST_NAME = "Joe";
	private static final String LAST_NAME = "M";
	private static final int AGE = 35;
	QA qa = new QA();
	
	@BeforeMethod
	public void buildSpy() {
		emp = spy(new Employee(FIRST_NAME, LAST_NAME, AGE));
	}
	
	@Test(expectedExceptions=RuntimeException.class)
	public void stubNonFinalMoveTo() {
		doThrow(new RuntimeException("Can't move to a different department")).when(emp).moveTo(qa);
		emp.moveTo(qa);
	}
	
	@Test(expectedExceptions=RuntimeException.class)
	public void stubFinalMoveTo() {
		doThrow(new RuntimeException("Can't move to a different department")).when(emp).finalMoveTo(qa);
		emp.finalMoveTo(qa);
	}
	
	private class QA implements EngineerAware {
		@Override
		public Engineer getDesignation() {
			return Engineer.QA;
		}		
	}
}

Output:

Employee moves from DEV to QA
Employee moves from QA to QA
FAILED: stubFinalMoveTo
org.testng.TestException: 
Expected exception java.lang.RuntimeException but got org.testng.TestException:

PASSED: stubNonFinalMoveTo

4. Using doReturn(Object) for stubbing spies

By using Mockito Spy, there are times when calling when(Object) for stubbing might be inappropriate. In such situations, we should consider using doReturn for stubbing.
In our Employee bean, I have added a couple of new methods to help us capture three employee skills. We access the skill using getSkill(index) which returns us the skill based on the index passed in.

Employee:

package com.javacodegeeks.mockito;

import java.util.ArrayList;
import java.util.List;

public class Employee {
	private String firstName;
	private String lastName;
	private int age;
	private EngineerAware engineerAware;
	private List skills;

	public Employee(String firstName, String lastName, int age) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.age = age;
		this.engineerAware = new Dev();
	}

	public int getAge() {
		return age;
	}

	public String getFullName() {
		return getFirstName() + " " + getLastName();
	}

	public String getLastName() {
		return lastName;
	}

	public String getFirstName() {
		return firstName;
	}	
	
	public void setAge(int age) {
		this.age = age;
	}

	public final void finalMoveTo(EngineerAware engineerAware) {
		System.out.println("Employee moves from " + this.engineerAware.getDesignation() + " to " + engineerAware.getDesignation());
		this.engineerAware = engineerAware;
	}
	
	public void moveTo(EngineerAware engineerAware) {
		System.out.println("Employee moves from " + this.engineerAware.getDesignation() + " to " + engineerAware.getDesignation());
		this.engineerAware = engineerAware;
	}
	
	private class Dev implements EngineerAware {
		@Override
		public Engineer getDesignation() {
			return Engineer.DEV;
		}		
	}
	
	public String getSkill(int index) {
		return skills.get(index);
	}
	
	public void addSkill(String skill1, String skill2, String skill3) {
		if (skills == null) {
			skills = new ArrayList(3);
		}
		skills.add(0, skill1);
		skills.add(1, skill2);
		skills.add(2, skill3);
	}
}

Suppose we want to train our spy object Employee bean to return us “SPY” when getSkill(0) is called, using when() API like below will throw NullPointerException. Note that spyEmp.getSkill(0) calls on the original method and since the List object is not yet initialized, it throws NullPointerException.

when(spyEmp.getSkill(0)).thenReturn("SPY");

This can be done differently using doReturn() for stubbing. For example, in the below style, we get around the NullPointerException.

doReturn("SPY").when(spyEmp).getSkill(0);

Both the cases are demonstrated in spySkillUsingWhenThenReturn and spySkillUsingDoWhen.

MockitoSpyExample:

package com.javacodegeeks.mockito;

import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

import org.mockito.InOrder;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class MockitoSpyExample {
	private Employee spyEmp;
	private Employee emp;
	private static final String FIRST_NAME = "Joe";
	private static final String LAST_NAME = "M";
	private static final int AGE = 35;
	
	@BeforeMethod
	public void buildSpy() {
		emp = new Employee(FIRST_NAME, LAST_NAME, AGE);
		spyEmp = spy(emp);
	}
	
	@Test
	public void verifySpyEffectOnRealInstance() {
		spyEmp.setAge(20);
		assertFalse(emp.getAge() == spyEmp.getAge());
	}
	
	@Test
	public void verifyEmployeeDetails() {
		System.out.println("Full name:" + spyEmp.getFullName());
		System.out.println("Age:" + spyEmp.getAge());
		
		InOrder inOrder = inOrder(spyEmp);
		
		System.out.println("Verify emp.getFullName() calls getFirstName() and then getLastName()");
		inOrder.verify(spyEmp).getFirstName();
		inOrder.verify(spyEmp).getLastName();
		
		System.out.println("Verify emp.getAge() is called");
		verify(spyEmp).getAge();
		
		assertEquals(spyEmp.getFirstName(), FIRST_NAME);
		assertEquals(spyEmp.getLastName(), LAST_NAME);
		assertEquals(spyEmp.getFullName(), FIRST_NAME + " " + LAST_NAME);
		assertEquals(spyEmp.getAge(), AGE);
		
		System.out.println("Verify emp.getFullName() called twice");
		verify(spyEmp, times(2)).getFullName();
	}
	
	@Test
	public void spyEmployeeName() {
		final String I_AM = "I am";
		final String THE_SPY = "the Spy";
		System.out.println("Train employee to return " + I_AM + " when emp.getFirstName() is called");
		when(spyEmp.getFirstName()).thenReturn(I_AM);
		System.out.println("Full Name: " + spyEmp.getFullName());
		assertEquals(I_AM + " M", spyEmp.getFullName());
		
		System.out.println("Train employee to return " + THE_SPY + " when emp.getLastName() is called");
		when(spyEmp.getLastName()).thenReturn(THE_SPY);
		System.out.println("Full Name: " + spyEmp.getFullName());
		assertEquals(I_AM + " " + THE_SPY, spyEmp.getFullName());
	}
	
	@Test
	public void spySkillUsingWhenThenReturn() {
		when(spyEmp.getSkill(0)).thenReturn("SPY");
		assertEquals("SPY", spyEmp.getSkill(0));
	}
	
	@Test
	public void spySkillUsingDoWhen() {
		doReturn("SPY").when(spyEmp).getSkill(0);
		assertEquals("SPY", spyEmp.getSkill(0));
	}
}

Output:

FAILED: spySkillUsingWhenThenReturn
java.lang.NullPointerException
	at com.javacodegeeks.mockito.Employee.getSkill(Employee.java:54)

PASSED: spySkillUsingDoWhen

5. Download the Eclipse Project

This was an example of Mockito Spy.

Download
You can download the full source code of this example here: mockitoSpy.zip

Ram Mokkapaty

Ram holds a master's degree in Machine Design from IT B.H.U. His expertise lies in test driven development and re-factoring. He is passionate about open source technologies and actively blogs on various java and open-source technologies like spring. He works as a principal Engineer in the logistics domain.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Test
Test
1 year ago

I think your example 1 output is not in the right order.

Back to top button