Implementing fake objects – simulators
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:
- The real object cannot be instantiated, such as when the constructor reads a file, performs a JNDI lookup, and so on.
- The real object has slow methods; for example, a class might have a
calculate ()
method that needs to be unit tested, but thecalculate()
method calls aload ()
method to retrieve data from the database. Theload()
method needs a real database, and it takes time to retrieve data, so we need to bypass theload()
method to unit test thecalculate
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:
- Launch Eclipse, open
<work_space>
, and go to the3605OS_TestDoubles
project. - Create a
com.packt.testdoubles.fake
package and create aJdbcSupport
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 either0
or1
. If the value returned is1
, it means that the database update is successful, and0
means there is no update. So, if we pass only oneStudent
object to update and if the update succeeds, then the array will contain only one integer as1
; however, if it fails, then the array will contain0
. - Create a
StudentDao
interface for theStudent
data access. The following is the interface snippet:public interface StudentDao { public void batchUpdate(List<Student> students); }
- Create an implementation of
StudentDao
. The following class represents theStudentDao
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 theStudent
list and populates theinsertList
andudpateList
methods, depending on theroleNumber
attribute. IfroleNumber
isNULL
, then this implies a new student. It creates a SQL parameter map for each student and calls theJdbcSupprt
class, and finally, checks the database update count. - We need to unit test the
batchUpdate
behavior, but theupdate
method creates a new instance ofJdbcSupport
and calls the database. So, we cannot directly unit test thebatchUpdate()
method; it will take forever to finish. Our problem is theupdate()
method; we'll separate the concern, extend theStudentDaoImpl
class, and override theupdate()
method. If we invokebatchUpdate()
on the new object, then it will route theupdate()
method call to the new overriddenupdate()
method.Create a
StudentDaoTest
unit test and aTestableStudentDao
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 index1
, set its value to0
, such asint[] val = {0}
, and set this array tovaluesToReturn
. - The following example demonstrates the failure scenario:
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); }
- Check whether
dao
is instantiated withTestableStudentDao
, then a new student object is created, and thevaluesToReturn
attribute of the fake object is set to{0}
. In turn, thebatchUpdate
method will call the update method ofTestableStudentDao
, and this will return a database update count of0
. ThebatchUpdate()
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 theStudent
object is created with a nullroleNumber
attribute. - The following example demonstrates the
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 theStudent
object is created with aroleNumber
attribute. - The following example unit tests the create and update scenarios together. We will pass two students: one to update and one to create. So,
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 theupdate
method's logic to conditionally return the count, but we cannot break the existing tests. So, we'll check whether thevaluesToReturn
array is not null and then returnvaluesToReturn
; 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
isnull
, theupdate
method creates an array of theparams
size and sets it as1
for each index. So, when the update will be called with two students, theupdate
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:
Note
Note that it took 0.041 seconds to execute four tests. This is interesting because it's something that you wouldn't easily get if you were using a real database.