How To Use Interfaces To Make the Code More Testable | by Cullen Sun | Nov, 2022

Great Interface – Part 3

image from pixabay

Program to interfaces rather than implementations. This can bring a lot of benefits to single rule programming. This article will show how to make code more testable through interface refactoring. When we use an interface, we can replace the real object with a mock object during testing, and we can mock it in any way we want to meet software testing purposes.

This is the last part of my articles about great interfaces. Please see below links to the previous two articles:

Part 1: It covers the basics of the interface.
part 2: It mainly talks about using interfaces to achieve some general design principles and design patterns.

Please see a smaller room booking app below. It allows users to book a room by taking their email and expected date. If the booking is successful the system will update the database and send an email to the user.

DataStoreService.java

It is a class that connects to the database and updates the data. Real-world database access would be much more complex and could be asynchronous. However, I simplified it to demonstrate the concept of the interface.

package com.great.refactor.before;

public class DataStoreService
public DataStoreService()
System.out.println("set up database connection");

public boolean markDateAsBooked(String date)
System.out.printf("write into database to book the date %s\n", date);
return true;

email service.java

This email service will help us to send email with given subject and content to a user.

package com.great.refactor.before;

public class EmailService
public EmailService()
System.out.println("configure and setup up email connection");

public boolean sendEmail(String receiver, String subject, String content)
System.out.printf("send email to %s \nsubject: %s \ncontent: %s\n", receiver, subject, content);
return true;

booking.java

This is the actual booking application itself. One of its main methods is called processBooking which depends on an example DataStoreService and an example of EmailService to do part of the work.

package com.great.refactor.before;
import java.time.LocalDate;

public class Booking
public enum Result
FAILURE,
PARTIAL_SUCCESS,
SUCCESS

private DataStoreService dataStore = new DataStoreService();
private EmailService emailService = new EmailService();

private String getEmailSubject()
return "You have successfully booked the function room";

private String getEmailContent()
return "Thank you. See you soon.";

public Result processBooking(String userEmail, LocalDate date)
String dateStr = date.toString();
boolean bookingSuccess = dataStore.markDateAsBooked(dateStr);
if (!bookingSuccess)
return Result.FAILURE;

// Just simple illustration here. It might be some complicated logics in real application.
String emailSubject = getEmailSubject();
String emailContent = getEmailContent();
boolean sendingEmailSuccess = emailService.sendEmail(userEmail, emailSubject, emailContent);
if (!sendingEmailSuccess)
System.out.println("Email server down. Need to alert user.");
return Result.PARTIAL_SUCCESS;

return Result.SUCCESS;

public static void main(String[] args)
Booking booking = new Booking();
booking.processBooking("xyz@abc.com", LocalDate.now());

testing

we want to unit-test Booking Class. Unfortunately, the current implementation makes testing impossible because the dependencies are too tightly coupled. On the other hand, during the test environment, the actual database is usually not accessible, and actual email sending is not expected during unit tests. Let’s continue to see how we can refactor the code to make it testable.

DatastoreService.java

now we have DataStoreService as an interface, and we have a class called DataStoreImplementation,

package com.great.refactor.after;

public interface DataStoreService
public boolean markDateAsBooked(String date);

class DataStoreImplementation implements DataStoreService
public DataStoreImplementation()
System.out.println("set up database connection");

@Override
public boolean markDateAsBooked(String date)
System.out.printf("write into database to book the date %s\n", date);
return true;

email service.java

Similarly, we also have an interface and implementation EmailService,

package com.great.refactor.after;

public interface EmailService
public boolean sendEmail(String receiver, String subject, String content);

class EmailImplementation implements EmailService
public EmailImplementation()
System.out.println("configure and setup up email connection");

@Override
public boolean sendEmail(String receiver, String subject, String content)
System.out.printf("send email to %s \nsubject: %s \ncontent: %s\n", receiver, subject, content);
return true;

booking.java

“Bookings” now depend on interfaces instead of concrete classes, and dependencies can be supplied (injected) with the constructor.

package com.great.refactor.after;
import java.time.LocalDate;

public class Booking
public enum Result
FAILURE,
PARTIAL_SUCCESS,
SUCCESS

private DataStoreService dataStore;
private EmailService emailService;

public Booking(DataStoreService dataStore, EmailService emailService)
this.dataStore = dataStore;
this.emailService = emailService;

private String getEmailSubject()
return "You have successfully booked the function room";

private String getEmailContent()
return "Thank you. See you soon.";

public Result processBooking(String userEmail, LocalDate date)
String dateStr = date.toString();
boolean bookingSuccess = dataStore.markDateAsBooked(dateStr);
if (!bookingSuccess)
return Result.FAILURE;

// Just simple illustration here. It might be some complicated logics in real application.
String emailSubject = getEmailSubject();
String emailContent = getEmailContent();
boolean sendingEmailSuccess = emailService.sendEmail(userEmail, emailSubject, emailContent);
if (!sendingEmailSuccess)
System.out.println("Email server down. Need to alert user.");
return Result.PARTIAL_SUCCESS;

return Result.SUCCESS;

public static void main(String[] args)
Booking booking = new Booking(new DataStoreImplementation(), new EmailImplementation());
booking.processBooking("xyz@abc.com", LocalDate.now());

Checking just got easier. We can create mock classes as given below. Please note that we can manipulate the behavior of mocked classes in any way as long as they meet the interface. Take a look at the variable names inside the mocked class. Boolean variables (eg. shallBookSuccess) prefixed with “shall” would be used to manipulate the mock behavior. Whereas a variable with prefix “update” will be used to hold the calling parameters for validation.

class MockDataStoreImplementation implements DataStoreService 
String updatedDate;

boolean shallBookSucceed;

@Override
public boolean markDateAsBooked(String date)
updatedDate = date;
return shallBookSucceed;

class MockEmailImplementation implements EmailService
String updatedReceiver;
String updatedSubject;
String updatedContent;

boolean shallEmailSucceed;

@Override
public boolean sendEmail(String receiver, String subject, String content)
updatedReceiver = receiver;
updatedSubject = subject;
updatedContent = content;
return shallEmailSucceed;

Please take a look at the following tests. We test each logic path of the program to make sure the behavior is as expected.

For example, in the first test, booking_dataStoreFail, we set it to fail over to the datastore. we check dataStoreService is actually called checking updatedDateand we confirm emailService Those updated variables are never called by checking they are null. Finally, we also check that the result will be FAILURE,

class MockEmailImplementation implements EmailService 
String updatedReceiver;
String updatedSubject;
String updatedContent;

boolean shallEmailSucceed;

@Override
public boolean sendEmail(String receiver, String subject, String content)
updatedReceiver = receiver;
updatedSubject = subject;
updatedContent = content;
return shallEmailSucceed;

public class BookingTest
LocalDate getTestingDate()
return LocalDate.of(2022, 11, 14);

@Test
public void booking_dataStoreFail()
// Given
MockDataStoreImplementation dataStoreService = new MockDataStoreImplementation();
dataStoreService.shallBookSucceed = false;
MockEmailImplementation emailService = new MockEmailImplementation();
emailService.shallEmailSucceed = true;
Booking booking = new Booking(dataStoreService, emailService);

// When
Booking.Result bookingResult = booking.processBooking("xyz@abc.com", getTestingDate());

// Then
assertEquals(dataStoreService.updatedDate, "2022-11-14");
assertEquals(emailService.updatedReceiver, null);
assertEquals(emailService.updatedSubject, null);
assertEquals(emailService.updatedContent, null);
assertEquals(bookingResult, Booking.Result.FAILURE);

@Test
public void booking_dataStoreSuccess_emailFail()
// Given
MockDataStoreImplementation dataStoreService = new MockDataStoreImplementation();
dataStoreService.shallBookSucceed = true;
MockEmailImplementation emailService = new MockEmailImplementation();
emailService.shallEmailSucceed = false;
Booking booking = new Booking(dataStoreService, emailService);

// When
Booking.Result bookingResult = booking.processBooking("xyz@abc.com", getTestingDate());

// Then
assertEquals(dataStoreService.updatedDate, "2022-11-14");
assertEquals(emailService.updatedReceiver, "xyz@abc.com");
assertEquals(emailService.updatedSubject, "You have successfully booked the function room");
assertEquals(emailService.updatedContent, "Thank you. See you soon.");
assertEquals(bookingResult, Booking.Result.PARTIAL_SUCCESS);

@Test
public void booking_dataStoreSuccess_emailSuccess()
// Given
MockDataStoreImplementation dataStoreService = new MockDataStoreImplementation();
dataStoreService.shallBookSucceed = true;
MockEmailImplementation emailService = new MockEmailImplementation();
emailService.shallEmailSucceed = true;
Booking booking = new Booking(dataStoreService, emailService);

// When
Booking.Result bookingResult = booking.processBooking("xyz@abc.com", getTestingDate());

// Then
assertEquals(dataStoreService.updatedDate, "2022-11-14");
assertEquals(emailService.updatedReceiver, "xyz@abc.com");
assertEquals(emailService.updatedSubject, "You have successfully booked the function room");
assertEquals(emailService.updatedContent, "Thank you. See you soon.");
assertEquals(bookingResult, Booking.Result.SUCCESS);

If we program to interface then it becomes so much easier to test. The refactoring process shown above made non-testable software fully testable. I also mentioned concepts like Mocking and Dependency Injection, which are essential for testing.

I hope it is useful for you. You can find its full source code Project on GitHub.

Leave a Reply