"I never make stupid mistakes. Only very, very clever ones."
–John Peel
It is very difficult to find stupid mistakes, but it's even more daunting when you are trying to figure out the clever ones. Debugging an application to know how to fix a problem is very expensive and time-consuming. Automated unit tests provide an extremely effective mechanism for catching regressions, especially when combined with test-driven development; it creates a test safety net for the developers.
This chapter covers the concepts of unit testing, quality of unit tests, external dependencies, and test doubles.
The Working with unit tests section introduces you to test automation and describes the characteristics of a good unit test.
The Understanding test doubles section explores the concept of external dependency and provides examples of test doubles. The following test doubles are explored:
Dummy objects
Stubs
Spies
Mock objects
Fake objects
A common understanding of unit testing is the testing of the smallest possible part of software, such as a single method, a small set of related methods, or a class.
In reality, we do not test methods; we test a logical unit and its behavior instead. Logical units can extend to a single method, to an entire class, or a collaboration of multiple classes.
For example, a standard calculator program can have an add method for adding two numbers. We can verify the add behavior by invoking the add method, or we can design the calculator program to have a simple calculate API, which can take two numbers and an operation (add, subtract, divide, and so on). Depending on the operand type (integer, double, and so on), the calculator may delegate the calculation to a collaborator class, such as a double calculator or a long calculator. We can still unit test the add behavior, but multiple classes (units) are involved now.
A unit test verifies an assumption about the behavior of the system. Unit tests should be automated to create a safety net so that the assumptions are verified continuously and a quick feedback can be provided if anything goes wrong.
The following are the benefits of test automation:
Behavior is continually verified: We refactor code (change the internal structure of the code without affecting the behavior of the system) to improve the code's quality, such as maintainability, readability, or extensibility. We can refactor code with confidence if automated unit tests are running and giving feedback.
The side effects of code changes are detected immediately: This is useful for a fragile, tightly-coupled system, where a change in one module breaks another module.
Saves time; no need for immediate regression testing: Suppose that you are adding a scientific computational behavior to an existing calculator program and modifying the code; after every piece of change, you do a regression testing to verify the integrity of the system. Manual regression testing is tedious and time-consuming, but if you have an automated unit test suite, then you can delay the regression testing until the functionality is done. This is because the automated suite will inform you at every stage if you break an existing feature.
A unit test should exhibit the following characteristics:
It should be automated, as explained in the preceding section.
It should have a fast test execution. To be precise, a test should not take more than a few milliseconds to finish execution (they should be fast; the faster, the better). A system can have thousands of unit tests. If they take time to execute, then the overall test execution time will go up; as a result, no one will be interested in running the tests. It will impact the feedback cycle.
A test should not depend on the result of another test or rather test execution order. Unit test frameworks can execute tests in any order. So, if a test depends on another test, then the test may fail any time and provide wrong feedback. You want tests to be standalone so that you can look at them and quickly see what they're actually testing, without having to understand the rest of the test code.
A test should not depend on database access, file access, or any long running task. Rather, an appropriate test double should isolate the external dependencies.
A test result should be consistent and time-and-location transparent. A test should not fail if it is executed at midnight, or it should not fail if it is executed in a different time zone.
Tests should be meaningful. A class can have getter and setter methods; you should not write tests for the getters and setters because they should be tested in the process of other more meaningful tests. If they're not, then either you're not testing the functionality or your getters and setters aren't being used at all; so, they're pointless.
Tests are system documentation. Tests should be readable and expressive; for example, a test that verifies the unauthorized access could be written as
testUnauthorizedAccess()
or ratherwhen_an_unauthorized_user_accesses_the_system_then_raises_secuirty_error()
. The latter is more readable and expresses the intent of the test.Tests should be short and tests should not be treated as second-class citizens. Code is refactored to improve the quality; similarly, unit tests should be refactored to improve the quality. A test class of 300 lines is not maintainable; we can rather create new test classes, move the tests to the new classes, and create a maintainable suite.
As per the preceding best practices, a test should be executed as fast as possible. Then what should you do if you need to test data access logic or file download code? Simple, do not include the tests in an automated test suite. Consider such tests as slow tests or integration tests. Otherwise, your continuous integration cycle will run for hours. Slow tests should still be automated. However, they may not run all the time, or rather they should be run out of the continuous integration feedback loop.
You cannot automate a unit test if your API class depends on slow external entities, such as data access objects or JNDI lookup. Then, you need test doubles to isolate the external dependencies and automate the unit test.
The next section covers test doubles.
We all know about stunt doubles in movies. A stunt double or dummy is a trained replacement used for dangerous action sequences in movies, such as a fight sequence on the top of a burning train, jumping from an airplane, and so on, mainly fight scenes. Stunt doubles are used to protect the real actors, are used when the actor is not available, or when the actor has a contract to not get involved in stunts.
Similarly, sometimes it is not possible to unit test the code because of the unavailability of the collaborator objects, or the cost of interaction and instantiation of collaborators. For instance, when the code is dependent on database access, it is not possible to unit test the code unless the database is available, or when a piece of code needs to send information to a printer and the machine is not connected to a LAN. The primary reason for using doubles is to isolate the unit you are testing from the external dependencies.
Test doubles act as stunt doubles. They are a skilled replacement of the collaborator objects and allow you to unit test code in isolation from the original collaborator.
Gerard Meszaros coined the term test doubles in his book xUNIT TEST PATTERNS, Addison-Wesley—this book explores the various test doubles and sets the foundation for Mockito.
Test doubles can be created to impersonate collaborators and can be categorized into the types, as shown in the following diagram:

In movies, sometimes a double doesn't perform anything; they just appear on the screen. One such instance would be standing in a crowded place where the real actor cannot go, such as watching a soccer match or tennis match. It will be very risky for the real actor to go to a full house, but the movie's script needs it.
Likewise, a dummy object is passed as a mandatory parameter object. A dummy object is not directly used in the test or code under test, but it is required for the creation of another object required in the code under test. Dummy objects are analogous to null objects, but a dummy object is not used by the code under test. Null objects (as in the pattern) are used in the code under test and are actively interacted with, but they just produce zero behavior. If they weren't used, you'd just use an actual null value. The following steps describe the usage of dummy objects:
Note
In this book, we will write the code and JUnit tests in the Eclipse editor. You can download Eclipse from the following URL:
Launch Eclipse and create a workspace,
\PacktPub\Mockito_3605OS\
; we'll refer to it as<work_space>
in the next steps/chapters.We'll create an examination grade system. The program will analyze the aggregate of all the subjects and determine the grade of a student. Create a Java project named
3605OS_TestDoubles
. Add anenum Grades
field to represent a student's grades:package com.packt.testdoubles.dummy; public enum Grades { Excellent, VeryGood, Good, Average, Poor; }
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
We'll use
src
as our source code's source folder andtest
as our test code's source folder. All Java files for this example will be created under thecom.packt.testdoubles.dummy
package.Create a
Student
class to uniquely identify a student:public class Student { private final String roleNumber; private final String name; public Student(String roleNumber, String name) { this.roleNumber = roleNumber; this.name = name; } //setters are ignored }
Create a
Marks
class to represent the marks of a student:public class Marks { private final Student student; private final String subjectId; private final BigDecimal marks; public Marks(Student student, String subjectId, BigDecimal marks) { this.student = student; this.subjectId = subjectId; this.marks = marks; } //getters methods go here }
Note that the
Marks
constructor accepts aStudent
object to represent the marks of a student. So, aStudent
object is needed to create aMarks
object.Create a
Teacher
class to generate a student's grades:public class Teacher { public Grades generateGrade(List<Marks> marksList) { BigDecimal aggregate = BigDecimal.ZERO; for (Marks mark : marksList) { aggregate = aggregate.add(mark.getMarks()); } BigDecimal percentage = calculatePercent(aggregate, marksList.size()); if (percentage.compareTo(new BigDecimal("90.00")) > 0) { return Grades.Excellent; } if (percentage.compareTo(new BigDecimal("75.00")) > 0) { return Grades.VeryGood; } if (percentage.compareTo(new BigDecimal("60.00")) > 0) { return Grades.Good; } if (percentage.compareTo(new BigDecimal("40.00")) > 0) { return Grades.Average; } return Grades.Poor; } private BigDecimal calculatePercent(BigDecimal aggregate,int numberOfSubjects) { BigDecimal percent = new BigDecimal(aggregate.doubleValue()/ numberOfSubjects); return percent; }
Create a
DummyStudent
class and extend theStudent
class. This is the dummy object. A dummy object will be the one that is not the real implementation and provides zero functionality or values. TheDummyStudent
class throws a runtime exception from all the methods. The following is the body of theDummyStudent
class:public class DummyStudent extends Student { protected DummyStudent() { super(null, null); } public String getRoleNumber() { throw new RuntimeException("Dummy student"); } public String getName() { throw new RuntimeException("Dummy student"); } }
Note that the constructor passes
NULL
to the super constructor and throws a runtime exception from thegetRoleNumber()
andgetName()
methods.Create a JUnit test to verify our assumption that when a student gets more than 75 percent (but less than 90 percent) in aggregate, then the teacher generates the grade as
VeryGood
, creates aDummyStudent
object, and passes it asStudent
to theMarks
constructor:public class TeacherTest { @Test public void when_marks_above_seventy_five_percent_returns_very_good() { DummyStudent dummyStudent = new DummyStudent(); Marks inEnglish = new Marks(dummyStudent, "English002", new BigDecimal("81.00")); Marks inMath = new Marks(dummyStudent, "Math005", new BigDecimal("97.00")); Marks inHistory = new Marks(dummyStudent, "History007, new BigDecimal("79.00")); List<Marks> marks = Arrays.asList(inHistory, inMaths, inEnglish); Grades grade = new Teacher().generateGrade(marks); assertEquals(Grades.VeryGood, grade); } }
Note that a
DummyStudent
object is created and passed to all the threeMarks
objects, as theMarks
constructor needs aStudent
object. ThisdummyStudent
object is not used in theTeacher
class or test method, but it is necessary for theMarks
object. ThedummyStudent
object shown in the preceding example is a dummy object.
A stub delivers indirect inputs to the caller when the stub's methods are called. Stubs are programmed only for the test scope. Stubs may record other information such as how many times they are invoked and so on.
Unit testing a happy path is relatively easier than testing an alternate path. For instance, suppose that you need to simulate a hardware failure or transaction timeout scenario in your unit test, or you need to replicate a concurrent money withdrawal for a joint account use case—these scenarios are not easy to imitate. Stubs help us to simulate these conditions. Stubs can also be programmed to return a hardcoded result; for example, a stubbed bank account object can return the account balance as $100.00.
The following steps demonstrate stubbing:
Launch Eclipse, open
<work_space>
, and go to the3605OS_TestDoubles
project.Create a
com.packt.testdoubles.stub
package and add aCreateStudentResponse
class. This Plain Old Java Object (POJO) contains aStudent
object and an error message:public class CreateStudentResponse { private final String errorMessage; private final Student student; public CreateStudentResponse(String errorMessage, Student student) { this.errorMessage = errorMessage; this.student = student; } public boolean isSuccess(){ return null == errorMessage; } public String getErrorMessage() { return errorMessage; } public Student getStudent() { return student; } }
Create a
StudentDAO
interface and add acreate()
method to persist a student's information. Thecreate ()
method returns the roll number of the new student or throws anSQLException
error. The following is the interface definition:public interface StudentDAO { public String create(String name, String className) throws SQLException; }
Create an interface and implementation for the student's registration. The following service interface accepts a student's name and a class identifier and registers the student to a class. The
create
API returns aCreateStudentResponse
. The response contains aStudent
object or an error message:public interface StudentService { CreateStudentResponse create(String name, String studentOfclass); }
The following is the service implementation:
public class StudentServiceImpl implements StudentService { private final StudentDAO studentDAO; public StudentServiceImpl(StudentDAO studentDAO) { this.studentDAO = studentDAO; } @Override public CreateStudentResponse create(String name, String studentOfclass) { CreateStudentResponse response = null; try{ String roleNum= studentDAO.create (name, studentOfclass); response = new CreateStudentResponse(null, new Student(roleNum, name)); }catch(SQLException e) {){ response = new CreateStudentResponse ("SQLException"+e.getMessage(), null); }catch (Exception e) { response = new CreateStudentResponse(e.getMessage(), null); } return response; } }
Note
Note that the service implementation class delegates the
Student
object's creation task to theStudentDAO
object. If anything goes wrong in the data access layer, then the DAO throws anSQLException
error. The implementation class catches the exceptions and sets the error message to the response object.How can you test the
SQLException
condition? Create a stub object and throw an exception. Whenever thecreate
method is invoked on the stubbed DAO, the DAO throws an exception. The followingConnectionTimedOutStudentDAOStub
class implements theStudentDAO
interface and throws anSQLException
error from thecreate()
method:package com.packt.testdoubles.stub; import java.sql.SQLException; public class ConnectionTimedOutStudentDAOStub implements StudentDAO { public String create(String name, String className) throws SQLException { throw new SQLException("DB connection timed out"); } }
This class should be created under the
test
source folder since the class is only used in tests.Test the
SQLException
condition. Create a test class and pass the stubbed DAO to the service implementation. The following is the test code snippet:public class StudentServiceTest { private StudentService studentService; @Test public void when_connection_times_out_then_the_student_is_not_saved() { studentService = new StudentServiceImpl(new ConnectionTimedOutStudentDAOStub()); String classNine = "IX"; String johnSmith = "john Smith"; CreateStudentResponse resp = studentService.create(johnSmith, classNine); assertFalse(resp.isSuccss()); } }
The error condition is stubbed and passed into the service implementation object. When the service implementation invokes the
create()
method on the stubbed DAO, it throws anSQLException
error.
Stubs are very handy to impersonate error conditions and external dependencies (you can achieve the same thing with a mock; this is just one approach). Suppose you need to test a code that looks up a JNDI resource and asks the resource to return some value. You cannot look up a JNDI resource from a JUnit test; you can stub the JNDI lookup code and return a stubbed object that will give you a hardcoded value.
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)); } }
A mock object is a combination of a spy and a stub. It acts as an indirect output for a code under test, such as a spy, and can also stub methods to return values or throw exceptions, like a stub. A mock object fails a test if an expected method is not invoked or if the parameters of the method don't match.
The following steps demonstrate the test failure scenario:
Launch Eclipse, open
<work_space>
, and go to the3605OS_TestDoubles
project.Create a
com.packt.testdoubles.mock
package and 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); } }
Copy the
StudentServiceSpy
class and rename it asStudentServiceMockObject
. Add a new method to verify the method invocations:public void verify(String methodName, int numberOfInvocation){ int actual = invocation(methodName); if(actual != numberOfInvocation){ throw new IllegalStateException(methodName+" was expected ["+numberOfInvocation+"] times but actuallyactaully invoked["+actual+"] times"); } }
Modify the
StudentService
code to set the mock object, as we did in the spy example:private StudentServiceMockObject mock; public void setMock(StudentServiceMockObject mock) { this.mock = mock; } public void enrollToCourse(String courseName,Student student){ MethodInvocation invocation = new MethodInvocation(); invocation.addParam(courseName).addParam(student).setMethod("enrollToCourse"); mock.registerCall(invocation); …//existing code }
Create a test to verify the method invocation:
public class StudentServiceTest { StudentService service = new StudentService(); StudentServiceMockObject mockObject = new StudentServiceMockObject(); @Test public void enrolls_students() throws Exception { //create 2 students Student bob = new Student("001", "Robert Anthony"); Student roy = new Student("002", "Roy Noon"); //set mock/spy service.setMock(mockObject); //invoke method twice service.enrollToCourse("english", bob); service.enrollToCourse("history", roy); //assert that the method was invoked twice assertEquals(2, mockObject.invocation("enrollToCourse")); //verify wrong information, that enrollToCourse was //invoked once, but actually it is invoked twice mockObject.verify("enrollToCourse", 1); } }
Run the test; it will fail, and you will get a verification error. The following screenshot shows the JUnit failure output:
The Mockito framework provides an API for mocking objects. It uses proxy objects to verify the invocation and stub calls.
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:
This chapter covered the concept of automated unit tests, the characteristics of a good unit test, and explored tests doubles. It provided the examples of dummy objects, fake objects, stubs, mock objects, and spies.
By now, you will be able to identify the different test doubles and write unit tests using test doubles.
The next chapter, Socializing with Mockito, will focus on getting the reader quickly started with the Mockito framework.