A fake object is a test double with real logic (unlike stubs) and is much more simplified or cheaper in some way. We do not mock or stub a unit that we test; rather, the external dependencies of the unit are mocked or stubbed so that the output of the dependent objects can be controlled or observed from the tests. The fake object replaces the functionality of the real code that we want to test. Fakes are also dependencies, and don't mock via subclassing (which is generally always a bad idea; use composition instead). Fakes aren't just stubbed return values; they use some real logic.
A classic example is to use a database stub that always returns a fixed value from the DB, or a DB fake, which is an entirely in-memory nonpersistent database that's otherwise fully functional.
What does this mean? Why should you test a behavior that is unreal? Fake objects are extensively used in legacy code. The following are the reasons behind using a fake object:
calculate ()
method that needs to be unit tested, but the calculate()
method calls a load ()
method to retrieve data from the database. The load()
method needs a real database, and it takes time to retrieve data, so we need to bypass the load()
method to unit test the calculate
behavior.Fake objects are working implementations. Mostly, the fake class extends the original class, but it usually performs hacking, which makes it unsuitable for production.
The following steps demonstrate the utility of a fake object. We'll build a program to persist a student's information into a database. A data access object class will take a list of students and loop through the student's objects; if roleNumber
is null
, then it will insert/create a student, otherwise it will update the existing student's information. We'll unit test the data access object's behavior:
<work_space>
, and go to the 3605OS_TestDoubles
project.com.packt.testdoubles.fake
package and create a JdbcSupport
class. This class is responsible for database access, such as acquiring a connection, building a statement object, querying the database, updating the table, and so on. We'll hide the JDBC code and just expose a method for the batch update. The following are the class details:public class JdbcSupport { public int[] batchUpdate(String sql, List<Map<String, Object>> params){ //original db access code is hidden return null; } }
Check whether the batchUpdate
method takes an SQL string and a list of objects to be persisted. It returns an array of integers. Each array index contains either 0
or 1
. If the value returned is 1
, it means that the database update is successful, and 0
means there is no update. So, if we pass only one Student
object to update and if the update succeeds, then the array will contain only one integer as 1
; however, if it fails, then the array will contain 0
.
StudentDao
interface for the Student
data access. The following is the interface snippet:public interface StudentDao {
public void batchUpdate(List<Student> students);
}
StudentDao
. The following class represents the StudentDao
implementation:public class StudentDaoImpl implements StudentDao { public StudentDaoImpl() { } @Override public void batchUpdate(List<Student> students) { List<Student> insertList = new ArrayList<>(); List<Student> updateList = new ArrayList<>(); for (Student student : students) { if (student.getRoleNumber() == null) { insertList.add(student); } else { updateList.add(student); } } int rowsInserted = 0; int rowsUpdated = 0; if (!insertList.isEmpty()) { List<Map<String, Object>> paramList = new ArrayList<>(); for (Student std : insertList) { Map<String, Object> param = new HashMap<>(); param.put("name", std.getName()); paramList.add(param); } int[] rowCount = update("insert", paramList); rowsInserted = sum(rowCount); } if (!updateList.isEmpty()) { List<Map<String, Object>> paramList = new ArrayList<>(); for (Student std : updateList) { Map<String, Object> param = new HashMap<>(); param.put("roleId", std.getRoleNumber()); param.put("name", std.getName()); paramList.add(param); } int[] rowCount = update("update", paramList); rowsUpdated = sum(rowCount); } if (students.size() != (rowsInserted + rowsUpdated)) { throw new IllegalStateException("Database update error, expected " + students.size() + " updates but actual " + (rowsInserted + rowsUpdated)); } } public int[] update(String sql, List<Map<String, Object>> params) { return new JdbcSupport().batchUpdate(sql, params); } private int sum(int[] rows) { int sum = 0; for (int val : rows) { sum += val; } return sum; } }
The batchUpdate
method creates two lists; one for the new students and the other for the existing students. It loops through the Student
list and populates the insertList
and udpateList
methods, depending on the roleNumber
attribute. If roleNumber
is NULL
, then this implies a new student. It creates a SQL parameter map for each student and calls the JdbcSupprt
class, and finally, checks the database update count.
batchUpdate
behavior, but the update
method creates a new instance of JdbcSupport
and calls the database. So, we cannot directly unit test the batchUpdate()
method; it will take forever to finish. Our problem is the update()
method; we'll separate the concern, extend the StudentDaoImpl
class, and override the update()
method. If we invoke batchUpdate()
on the new object, then it will route the update()
method call to the new overridden update()
method.Create a StudentDaoTest
unit test and a TestableStudentDao
subclass:
public class StudentDaoTest { class TestableStudentDao extends StudentDaoImpl{ int[] valuesToReturn; int[] update(String sql, List<Map<String, Object>> params) { Integer count = sqlCount.get(sql); if(count == null){ sqlCount.put(sql, params.size()); }else{ sqlCount.put(sql, count+params.size()); } if (valuesToReturn != null) { return valuesToReturn; } return valuesToReturn; } } }
Note that the update
method doesn't make a database call; it returns a hardcoded integer array instead. From the test, we can set the expected behavior. Suppose we want to test a database update's fail behavior; here, we need to create an integer array of index 1
, set its value to 0
, such as int[] val = {0}
, and set this array to valuesToReturn
.
public class StudentDaoTest { private TestableStudentDao dao; private Map<String, Integer> sqlCount = null; @Before public void setup() { dao = new TestableStudentDao(); sqlCount = new HashMap<String, Integer>(); } @Test(expected=IllegalStateException.class) public void when_row_count_does_not_match_then_rollbacks_tarnsaction(){ List<Student> students = new ArrayList<>(); students.add(new Student(null, "Gautam Kohli")); int[] expect_update_fails_count = {0}; dao.valuesToReturn = expect_update_fails_count; dao.batchUpdate(students); }
dao
is instantiated with TestableStudentDao
, then a new student object is created, and the valuesToReturn
attribute of the fake object is set to {0}
. In turn, the batchUpdate
method will call the update method of TestableStudentDao
, and this will return a database update count of 0
. The batchUpdate()
method will throw an exception for a count mismatch.The following example demonstrates the new Student
creation scenario:
@Test public void when_new_student_then_creates_student(){ List<Student> students = new ArrayList<>(); students.add(new Student(null, "Gautam Kohli")); int[] expect_update_success = {1}; dao.valuesToReturn = expect_update_success; dao.batchUpdate(students); int actualInsertCount = sqlCount.get("insert"); int expectedInsertCount = 1; assertEquals(expectedInsertCount, actualInsertCount); }
Note that the valuesToReturn
array is set to {1}
and the Student
object is created with a null roleNumber
attribute.
Student
information update scenario:@Test public void when_existing_student_then_updates_student_successfully(){ List<Student> students = new ArrayList<>(); students.add(new Student("001", "Mark Leo")); int[] expect_update_success = {1}; dao.valuesToReturn = expect_update_success; dao.batchUpdate(students); int actualUpdateCount = sqlCount.get("update"); int expectedUpdate = 1; assertEquals(expectedUpdate, actualUpdateCount); }
Note that the valuesToReturn
array is set to {1}
and the Student
object is created with a roleNumber
attribute.
update
should return {1,1}
for the existing students and {1}
for the new student.We cannot set this conditional value to the valuesToReturn
array. We need to change the update
method's logic to conditionally return the count, but we cannot break the existing tests. So, we'll check whether the valuesToReturn
array is not null and then return valuesToReturn
; otherwise, we will apply our new logic.
The following code snippet represents the conditional count logic:
class TestableStudentDao extends StudentDaoImpl { int[] valuesToReturn; int[] update(String sql, List<Map<String, Object>> params) { Integer count = sqlCount.get(sql); if(count == null){ sqlCount.put(sql, params.size()); }else{ sqlCount.put(sql, count+params.size()); } if (valuesToReturn != null) { return valuesToReturn; } int[] val = new int[params.size()]; for (int i = 0; i < params.size(); i++) { val[i] = 1; } return val; } }
When valuesToReturn
is null
, the update
method creates an array of the params
size and sets it as 1
for each index. So, when the update will be called with two students, the update
method will return {1,1}
.
The following test creates a student list of three students, two existing students with roleNumbers
and one new student.
@Test public void when_new_and_existing_students_then_creates_and_updates_students() { List<Student> students = new ArrayList<>(); students.add(new Student("001", "Mark Joffe")); students.add(new Student(null, "John Villare")); students.add(new Student("002", "Maria Rubinho")); dao.batchUpdate(students); }
The following screenshot shows the output of the JUnit execution: