Working with stubs
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.