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:
<work_space>
, and go to the 3605OS_TestDoubles
project.com.packt.testdoubles.stub
package and add a CreateStudentResponse
class. This Plain Old Java Object (POJO) contains a Student
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; } }
StudentDAO
interface and add a create()
method to persist a student's information. The create ()
method returns the roll number of the new student or throws an SQLException
error. The following is the interface definition:public interface StudentDAO { public String create(String name, String className) throws SQLException; }
create
API returns a CreateStudentResponse
. The response contains a Student
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 that the service implementation class delegates the Student
object's creation task to the StudentDAO
object. If anything goes wrong in the data access layer, then the DAO throws an SQLException
error. The implementation class catches the exceptions and sets the error message to the response object.
SQLException
condition? Create a stub object and throw an exception. Whenever the create
method is invoked on the stubbed DAO, the DAO throws an exception. The following ConnectionTimedOutStudentDAOStub
class implements the StudentDAO
interface and throws an SQLException
error from the create()
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.
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 an SQLException
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.