How unit tests can help interface segregation

Docler
Docler
Mar 20, 2020 · 9 min read

Written By João Marcos Bizarro Lopes

TDD (Test Driven Development) is not a new concept, but despite a lot of content on the Internet, there are also many complaints about its implementation, such as:

  • TDD is beautiful in theory, but in practice it doesn’t work;
  • TDD is expensive because it needs time to implement, but there is never time. The deadlines are too short;
  • The examples found on the internet about TDD are very basic and superficial, and therefore do not apply to reality;

While partially agreeing with the last point, the problem with these sentences is that they lead to hasty conclusions.

TDD is a skill, and as such, it should be thoroughly practised until you reach a level of professionalism.

I’m not a great programmer; I’m just a good programmer with great habits

Kent Beck, author of the book TDD By Example

But, like everything, there are advantages and disadvantages. No need to strictly follow TDD, writing unit tests for absolutely everything. It’s up to developers to understand trade offs and make the best decision possible.

However, in this article I show you how to avoid falling into a very common trap during software development, which is the indirect dependency created between unit tests that test different components but have the same dependency.

Context:
Joe is a software developer at Awesome Courses and Adam, Business Manager, asked him to create a software to manage students. The application must have the following features:

  • Student registration, containing name, email and birth date. The application must avoid an email to be registered more than once;
  • Edit student’s name and birth date;
  • Cancel student’s registration;

1° Approach: Without Interface Segregation

Joe decided to start by student registration, however he learned about TDD and this application is the perfect situation to put his new knowledge into practice, so he starts creating a test which verifies if there is already a student registered with the same email:

[TestFixture]
public class StudentRegistrationTest
{
[Test]
public void ItShouldNot_RegisterStudent_IfAlreadyRegistered()
{
var registerStudent = new RegisterStudent();

Assert.Throws<StudentAlreadyRegisteredException>(() =>
registerStudent.Execute(new RegisterStudentRequest(
"Joe", "joe@awesome.courses", new DateTime(1990, 1, 1))));
}
}

Since Joe doesn’t have any production code, the test fails (actually he has a compile error). He creates his empty classes then:

public class RegisterStudentRequest
{
public string Name { get; set; }
public string Email { get; set; }
public DateTime BirthDate { get; set; }
public RegisterStudentRequest(string name, string email, DateTime birthDate)
{
Name = name;
Email = email;
BirthDate = birthDate;
}
}public class RegisterStudent
{
public void Execute(RegisterStudentRequest request)
{

}

}

Now the code is compiling, but the test is still failing. Following TDD, Joe then starts to develop your feature:

public class RegisterStudent
{
public void Execute(RegisterStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student != null)
throw new StudentAlreadyRegisteredException();
}
}

Joe writes his production code and discovers that he’ll need a dependency to find student by email, so he develops a new interface called IStudentRepository:

public interface IStudentRepository
{
Student FindByEmail(string email);
}

And his Student class looks like this:

public class Student
{
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime BirthDate { get; private set; }
}

And after that Joe needs to inject the repository interface into RegisterStudent class:

public class RegisterStudent
{
private readonly IStudentRepository _repository;
public RegisterStudent(IStudentRepository repository)
{
_repository = repository;
}

public void Execute(RegisterStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student != null)
throw new StudentAlreadyRegisteredException();
}
}

After changing your production code, Joe notices he needs to change his unit test and add this dependency, so he creates a stub for students repository. Now his test class looks like this:

[TestFixture]
public class StudentRegistrationTest
{
[Test]
public void ItShouldNot_RegisterStudent_IfAlreadyRegistered()
{
var registerStudent = new RegisterStudent(new StudentRepositoryStub());

Assert.Throws<StudentAlreadyRegisteredException>(() =>
registerStudent.Execute(new RegisterStudentRequest(
"Joe", "joe@awesome.courses", new DateTime(1990, 1, 1))));
}

private class StudentRepositoryStub : IStudentRepository
{
public Student FindByEmail(string email)
{
return new Student();
}
}
}

Now the first Joe’s test is green and he’s feeling excited since all his knowledge is being applied and the result is great. Now he adds the student creation and he realizes that he will need another method in the repository interface to persist the student in database:

public interface IStudentRepository
{
Student FindByEmail(string email);
void Register(Student student);
}[TestFixture]
public class StudentRegistrationTest
{
[Test]
public void ItShouldNot_RegisterStudent_IfAlreadyRegistered()
{
var registerStudent = new RegisterStudent(new StudentRepositoryStub());

Assert.Throws<StudentAlreadyRegisteredException>(() =>
registerStudent.Execute(new RegisterStudentRequest(
"Joe", "joe@awesome.courses", new DateTime(1990, 1, 1))));
}

private class StudentRepositoryStub : IStudentRepository
{
public Student FindByEmail(string email)
{
return new Student(string.Empty, string.Empty, DateTime.Now);
}
public void Register(Student student)
{
}
}
}

Now he updates RegisterStudent class:

public class RegisterStudent
{
private readonly IStudentRepository _repository;
public RegisterStudent(IStudentRepository repository)
{
_repository = repository;
}

public void Execute(RegisterStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student != null)
throw new StudentAlreadyRegisteredException();

student = new Student(request.Name, request.Email, request.BirthDate);
this._repository.Register(student);
}
}

Awesome! Joe finishes his first feature and his code is tested. After that he starts to code the next feature: edit student. He starts the same way as he did on his first feature, coding his test first:

[TestFixture]
public class StudentEditionTest
{
[Test]
public void ItShouldNot_EditStudent_IfNotFound()
{
var editStudent = new EditStudent();
Assert.Throws<StudentNotFound>(() =>
editStudent.Execute(new EditStudentRequest(
"Maria", "maria@awesome.courses", new DateTime(1991, 10, 3))));
}
}

Joe creates the classes he needs to compile the test then:

public class EditStudentRequest
{
public string Email { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public EditStudentRequest(string email, string name, DateTime birthDate)
{
Email = email;
Name = name;
BirthDate = birthDate;
}
}public class EditStudent
{
public void Execute(EditStudentRequest request)
{

}
}

Ok, now Joe’s test compiles, but it fails since he didn’t write any production code, which is his next step:

public class EditStudent
{
public void Execute(EditStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student == null)
throw new StudentNotFound();
}
}

Joe have just discovered that he needs a repository dependency and he thinks: “Oh, I have the same method in IStudentRepository interface, so I’ll inject it in my class”. So he does it:

public class EditStudent
{
private readonly IStudentRepository _repository;
public EditStudent(IStudentRepository repository)
{
_repository = repository;
}

public void Execute(EditStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student == null)
throw new StudentNotFound();
}
}

Joe’s test class is now looking like this:

[TestFixture]
public class StudentEditionTest
{
[Test]
public void ItShouldNot_EditStudent_IfNotFound()
{
var editStudent = new EditStudent(new StudentRepositoryStub());
Assert.Throws<StudentNotFound>(() =>
editStudent.Execute(new EditStudentRequest(
"Maria", "maria@awesome.courses", new DateTime(1991, 10, 3))));
}

private class StudentRepositoryStub : IStudentRepository
{
public Student FindByEmail(string email)
{
return null;
}
public void Register(Student student)
{
throw new NotImplementedException();
}
}
}

Amazing! The test is passing and now Joe needs to finish this feature adding a new repository method to update the student:

public interface IStudentRepository
{
Student FindByEmail(string email);
void Register(Student student);
void Update(Student student);
}
public class Student
{
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime BirthDate { get; private set; }
public Student(string name, string email, DateTime birthDate)
{
Name = name;
Email = email;
BirthDate = birthDate;
}
public void UpdateName(string name) => this.Name = name; public void UpdateBirthDate(DateTime birthDate) => this.BirthDate = birthDate;}

Now his EditStudent class looks like:

public class EditStudent
{
private readonly IStudentRepository _repository;
public EditStudent(IStudentRepository repository)
{
_repository = repository;
}

public void Execute(EditStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student == null)
throw new StudentNotFound();
student.UpdateName(request.Name);
student.UpdateBirthDate(request.BirthDate);

this._repository.Update(student);
}
}

Ok, Joe finished his second feature, right? Wrong. After add a new method in repository interface Joe broke his tests. Which one? All of them. Why? Because now he needs to implement the repository interface in all tests. But shouldn’t unit tests be isolated? Yes, they should.

You’re maybe thinking now:

  1. If Joe used Mocks instead of Stubs he wouldn’t face this issue;
  2. If Joe had only one stub for both tests he wouldn’t have this problem;

Well, it’s true that Joe would not face these issues if he was using Mocks. Honestly this is one of mockists arguments to use mocks instead of stubs. The problem of mockist approach is that using mocks doesn’t naturally encourage developers to segregate interfaces. You need to think about it during development. However it would solve Joe’s problems and since you understand the trade offs, go for it.

Regarding the second approach, Joe would need to add conditionals in his stubs or he would need at least to create a spy stub or a fake repository, which is not technically wrong, but then Joe would create an indirect dependency between unit tests, which means that a change in one test can break other ones. This would break the isolation principle a unit test should have.

2° Approach: Interface Segregation

Fortunately Joe realised that he created a problem and he feels encouraged by your unit tests behaviour to segregate his repository interface.

As Joe remember, ISP (Interface Segregation Principle) states that a client shouldn’t be required to depend on method that doesn’t use. In Joe’s code both register and edit features have methods available from your dependencies that they’re not using. Why should EditStudent class need Register method?

Well, Joe then starts to refactor your code creating two different interfaces:

public interface IStudentRegistrationRepository
{
Student FindByEmail(string email);
void Register(Student student);

}



public interface IEditStudentRepository
{
Student FindByEmail(string email);
void Update(Student student);

}

Joe now updates his tests to use its new dependencies (interfaces):

[TestFixture]
public class StudentEditionTest
{
[Test]
public void ItShouldNot_EditStudent_IfNotFound()
{
var editStudent = new EditStudent(new StudentRepositoryStub());

Assert.Throws<StudentNotFound>(() =>
editStudent.Execute(new EditStudentRequest(
"Maria", "maria@awesome.courses", new DateTime(1991, 10, 3))));
}

private class StudentRepositoryStub : IEditStudentRepository
{
public Student FindByEmail(string email)
{
return null;
}

public void Update(Student student)
{
}
}

}


[TestFixture]
public class StudentRegistrationTest
{
[Test]
public void ItShouldNot_RegisterStudent_IfAlreadyRegistered()
{
var registerStudent = new RegisterStudent(new StudentRepositoryStub());

Assert.Throws<StudentAlreadyRegisteredException>(() =>
registerStudent.Execute(new RegisterStudentRequest(
"Joe", "joe@awesome.courses", new DateTime(1990, 1, 1))));
}

private class StudentRepositoryStub : IStudentRegistrationRepository
{
public Student FindByEmail(string email)
{
return new Student(string.Empty, string.Empty, DateTime.Now);
}

public void Register(Student student)
{
}
}

}

There is now a compilation error, which is quickly fixed by Joe changing the interface injected in his classes:

public class RegisterStudent
{
private readonly IStudentRegistrationRepository _repository;

public RegisterStudent(IStudentRegistrationRepository repository)
{
_repository = repository;
}

public void Execute(RegisterStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student != null)
throw new StudentAlreadyRegisteredException();

student = new Student(request.Name, request.Email, request.BirthDate);
this._repository.Register(student);
}

}



public class EditStudent
{
private readonly IEditStudentRepository _repository;

public EditStudent(IEditStudentRepository repository)
{
_repository = repository;
}

public void Execute(EditStudentRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student == null)
throw new StudentNotFound();

student.UpdateName(request.Name);
student.UpdateBirthDate(request.BirthDate);

this._repository.Update(student);
}

}

Wow, nice! Joe's tests are green again. And Joe's next step is to code his last feature: cancel student registration. He starts creating his test:

[TestFixture]
public class CancelStudentRegistrationTest
{
[Test]
public void StudentShouldNot_CancelRegistration_IfNotFound()
{
var cancelRegistration = new CancelStudentRegistration();

Assert.Throws<StudentNotFound>(() =>
cancelRegistration.Execute(new CancelStudentRegistrationRequest(
"maria@awesome.courses")));
}

}

To compile his test Joe writes his other classes:

public class CancelStudentRegistration
{
public void Execute(CancelStudentRegistrationRequest request)
{

}

}


public class CancelStudentRegistrationRequest
{
public string Email { get; protected set; }

public CancelStudentRegistrationRequest(string email)
{
this.Email = email;
}

}

The test is now compiling, but failing. Joe writes your production code to make his test green:

public class CancelStudentRegistration
{
public void Execute(CancelStudentRegistrationRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student == null)
throw new StudentNotFound();

this._repository.CancelRegistration(student);
}

}

Joe finds out that he needs a repository dependency so he fixes his test first:

[TestFixture]
public class CancelStudentRegistrationTest
{
[Test]
public void StudentShouldNot_CancelRegistration_IfNotFound()
{
var cancelRegistration = new CancelStudentRegistration(new CancelStudentRegistrationRepository());

Assert.Throws<StudentNotFound>(() =>
cancelRegistration.Execute(new CancelStudentRegistrationRequest(
"maria@awesome.courses")));
}

private class CancelStudentRegistrationRepository : ICancelStudentRegistrationRepository
{
public Student FindByEmail(string email)
{
return null;
}

public void CancelRegistration(Student student)
{
}
}
}

Joe knows that it’s his test responsability to say how his interfaces (abstractions) should look like, so he can create his interface now:

public interface ICancelStudentRegistrationRepository
{
Student FindByEmail(string email);
void CancelRegistration(Student student);
}

Finally his production class should like this:

public class CancelStudentRegistration
{
private readonly ICancelStudentRegistrationRepository _repository;
public CancelStudentRegistration(ICancelStudentRegistrationRepository repository)
{
_repository = repository;
}

public void Execute(CancelStudentRegistrationRequest request)
{
var student = this._repository.FindByEmail(request.Email);
if (student == null)
throw new StudentNotFound();
this._repository.CancelRegistration(student);
}
}

Joe executes all his tests and all of them are green! Wow! Not only he didn’t break any other test, but all tests are completely isolated. Beyond that, all his features are isolated, which means that if he wants to change anything in one feature only he’ll be able to do that without breaking non related changes. That’s great!

You may think Joe is repeating himself, because he could create a parent interface containing the method FindByEmail. This change would not bring only solutions, but problems also, which are not in the scope of this article.

Therefore I don’t intend to convince you to use stubs instead of mocks. Mocks are a great technique too. The goal of this article is to help you understand the importance of interface segregation, which is an essential element for software maintainability, testability and modularisation.

Any questions or suggestions, feel free to email me or comment on the article.

The Startup

Get smarter at building your thing. Join The Startup’s +800K followers.

Docler

Written by

Docler

Curious about the technologies powering the 30th most visited website in the world, with 45 million users, 2,000 servers, 4 data centers?

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +800K followers.

Docler

Written by

Docler

Curious about the technologies powering the 30th most visited website in the world, with 45 million users, 2,000 servers, 4 data centers?

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +800K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store