Quantcast
Channel: technoChord » tech
Viewing all articles
Browse latest Browse all 15

Does Test Driven Development Really Work?

$
0
0

In an assembly of geeks, whenever someone suggests the possibility of adopting Test Driven Development(TDD), I can almost see the collective eye-roll! Although widely regarded as a great technique for developing modular testable code, it is still regarded as a mostly utopian concept. In other words…you don’t usually see the enthusiasm in the room to jump on the TDD bandwagon.

In this post, I will attempt to demonstrate to you using a concrete, but contrived example, how TDD encourages developing modular code. To be absolutely clear, we are going to be demonstrating the benefits of Test First Development.

I will use the popular JUnit framework and some mocking frameworks to demonstrate this.

Unit testing is an overloaded term. Depending on how you look at it, what is defined as a unit can vary from a single method call, to an entire class to an entire user story to, sometimes an entire epic!

JUnit provides the developer with a framework to write tests but does not have an opinion on what is defined as a unit.

The definition of a unit, or rather, the assumption of what a unit should be, is baked into the correct use of several mocking frameworks. [All except PowerMock,  which breaks the rules pretty blatantly].

Most such frameworks define a System Under Test (SUT) and collaborators with the SUT. The attempt is, that once the SUT is identified, all it’s collaborators may be stubbed out. What is left in the SUT, then, is the unit.

By that yardstick, a “badly” written class would be one where there are not many collaborators. All functionality, is therefore jammed into one class, even though, it may be spread into several method calls. So, in other words, you have several small methods but your code may still not be modular. To make your code modular, what you need is collaborators that are candidates for stubbing.

So why is it that methods in a SUT cannot be stubbed out but collaborators can. The reason is simple and consistent across all mocking frameworks: Mocks are created by extending classes or interfaces and the SUT is never mocked. (If it is, you’d be testing a mock, not the original class). Therefore a method in your SUT can never be mocked in an elegant manner (one can always use reflection, but that’s a slippery slope).

I have worked with teams that state that the very reason for breaking up a large class is clashes while checking into source control. That may very well be, but breaking up methods to allow re-use (where the same code is used in several places) will only get you so far. What will really make the code modular, and enforce Separation Of Concerns, is the adoption of two fundamental principles:

The Greedy App without TDD

We will start with a User Story:

User Story 1: As a club manager I would like to invite users to my club who have a net worth of more than a million dollars. The invited user should be persisted and a welcome email be sent to him.

Let us first build this application the conventional way, with no tests first.

Here is one possible way the UserService may be coded:

public class UserServiceImplWithoutTDD implements UserService {
@Inject
private UserRepository userRepository;
@Inject
private EmailService emailService;

@Override
public void inviteUser(User user) {
	if (user != null){
		if (user.getId() != null){
			//determine net worth
			if ((user.getStateOfResidence() == "CA" &&
				(user.getIncome() - user.getExpenses()) > 1000000.0) ||
				(user.getStateOfResidence() == "TX" &&
				(user.getIncome() - user.getExpenses() *1.19) > 1000000.0)
				){
			        //millionaire!
				userRepository.addUser(user);
				String welcomeEmail = "Hi " + user.getName()
                                        + ", welcome to the club!";
				emailService.sendMail(welcomeEmail);
			}
		}
	}
}
}

As we can see, business logic is checked to see if the user qualifies as per the story and then the user object is persisted and welcomed with an email message.

Since we are not using TDD, we have not written a test to begin with. Now using Mockito, here is how we can write a test for this class after the code is written:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceUnitTests {
	@Mock
	private UserRepository userRepository;
	@Mock
	private EmailService emailService;

	@InjectMocks
	private UserService userService;

	private User user ;
	@Before
	public void setup(){
                //given
                user = new User();
		user.setIncome(1200000.0);
		user.setExpenses(150000.0);
		user.setStateOfResidence("CA");
	}

	@Test
	public void testInviteUserWhenUserIsAMillionaire(){
		//when
		//Following stubs are not necessary because voids on mocks are noops anyway...but just for clarity
		doNothing().when(userRepository).addUser(any(User.class));
		doNothing().when(emailService).sendEmail(anyString());

		//call SUT
		userService.inviteUser(user);

		//then
		verify(userRepository, times(1)).addUser(any(User.class));
		verify(emailService, times(1)).sendEmail(anyString());

	}
}

So far, all looks good. You have managed to satisfy the user story and written a test for it. However, do make note of that painfully written “given” section in setup(). The developer has to scrutinize the complex business logic and engineer data so that the conditional is true so that the “then” section is satisfied.
Also.. the code doesn’t look clean. So, you decide to refactor a bit and extract the business logic into a method of it’s own:

@Override
public void inviteUser(User user) {
	if (user != null){
		if (user.getId() != null){
			if (this.determineNetWorth(user)){
				//millionaire!
				userRepository.addUser(user);
				String welcomeEmail = "Hi " + user.getName() + ", welcome to the club!";
				emailService.sendMail(welcomeEmail);
			}
		}
	}
}
}
private boolean determineNetWorth(User user){
	return ((user.getStateOfResidence() == "CA" &&
			(user.getIncome() - user.getExpenses()) > 1000000.0) ||
			(user.getStateOfResidence() == "TX" &&
			(user.getIncome() - user.getExpenses() *1.19) > 1000000.0)
			);
}

Looks better. The test still passes.
Till the customer decides to add another story:

User Story 2: As a club manager I would like to invite users to my club who have paid taxes in the past 10 years at least and have stayed in one state for more than 3 years.

Back to the drawing board. This time you come up with nicely refactored code:

@Override
public void inviteUser(User user) {
	if (user != null){
		if (user.getId() != null){
			//determine net worth
			if (this.determineNetWorth(user)){
				//millionaire!
				if (this.determineTaxQualification(user)){
					//User has paid taxes in the past 10 years while living in state for 3
					userRepository.addUser(user);

					String welcomeEmail = "Hi " + user.getName() + ", welcome to the club!";
					emailService.sendMail(welcomeEmail);
				}
			}
		}
	}
}

private boolean determineTaxQualification(User user){
	return user.getTaxesPaidInPastYearsMap() != null &&
			user.getTaxesPaidInPastYearsMap().size() >= 10 &&
			user.getNumberofYearsInCurrentState() > 3.0;
}
private boolean determineNetWorth(User user){
	return ((user.getStateOfResidence() == "CA" &&
			(user.getIncome() - user.getExpenses()) > 1000000.0) ||
			(user.getStateOfResidence() == "TX" &&
			(user.getIncome() - user.getExpenses() *1.19) > 1000000.0)
			);
}

Testing this however is a bit of a challenge because now you have to test the four combination of conditions, for each of the determineNetWorth and determineTaxQualification methods returning true or a false.
For doing  that, you will need to prepare the User object in the setup() with values that passes both the conditionals.
Maybe for this contrived example we can get away with engineering some data. But as you can see, this approach will soon go out of hand and will not scale for more realistic and complex cases.

Ideally, what should happen is that when we write the test for testing a user who has passed the taxQualification test, we should stub out the netWorth method so that we do not need to engineer data that *it* tests to return a certain condition. Likewise, when testing the netWorth conditional, we should be able to stub out the tax qualification method without dealing with the pain of engineering data for tax qualification. In that sense, two tests will need to be written, where the unit has changed for each.

However, since the two “business operations” are implemented as methods in the SUT, it is impossible for the mock frameworks to stub them out (because the SUT is not mocked, and only methods of a mocked class can be stubbed out).
So the next best thing to do is to place this functionality into classes of their own and make then collaborators of the SUT.

One way to do that is to do that manually when we reach such a point in our development. Another way is to use TDD.
I’ll show you now, how adopting TDD will naturally lead you down the path where you end up with classes amenable to testing using mocks and stubbing.

The Greedy App with TDD

In the spirit of TDD, write a test that fails:

The test:
@RunWith(MockitoJUnitRunner.class)
public class UserServiceWithTDDTests {
        @Mock
        private UserRepository userRepository;
        @Mock
        private EmailService emailService;
	@InjectMocks
	private UserService userService;
	private User user ;
	@Before
	public void setup(){
		user = new User("foo");
	}
	public void testInviteUserWhenAMillionaire() {
		userService.inviteUser(user);

		verify(userRepository, times(1)).addUser(any(User.class));
		verify(emailService, times(1)).sendEmail(anyString());
	}
}

And the corresponding service class:

public class UserServiceImplWithTDD implements UserService {
	@Inject
	private UserRepository userRepository;
	@Inject
	private EmailService emailService;

	@Override
	public void inviteUser(User user) {
		if (user != null){
			if (user.getId() != null){
				//determine net worth
				if (this.isNetWorthAcceptable(user)){
					userRepository.addUser(user);

					String welcomeEmail = "Hi " + user.getName() + ", welcome to the club!";
					emailService.sendMail(welcomeEmail);
				}
			}
		}
	}
	@Override
	public boolean isNetWorthAcceptable(User user) {
		return false;
	}
}

In the above scenario, the test fails because the calls to addUser and sendEmail were never made.

So, how can we fix this? A simple possibility is the have the isNetWorthAcceptable(User user) method return a true instead of a false. However this does not test the condition mentioned in the user story.

So let us implement the logic asked for by the user story:

public boolean isNetWorthAcceptable(User user) {
	return ((user.getStateOfResidence() == "CA" &&
			(user.getIncome() - user.getExpenses()) > 1000000.0) ||
			(user.getStateOfResidence() == "TX" &&
			(user.getIncome() - user.getExpenses() *1.19) > 1000000.0)
			);
}

This will fail too as the user object is not populated with the correct data.
Now I have two options: either I fill in the User object in the setup() to make this condition pass, or, to test if the userRepository and emailService calls are actually made, I have to change the conditions coded inside the getNetWorthAcceptable(…) method. The first option is unpalatable. To do the second, I will have to change code that I have already coded.
So I consider mocking out the isNetWorthAcceptable(…) call. But how do I mock out a method call in the System Under Test (SUT)?
The only way that can be done is if I put that method into a class of it’s own. To really make it generic, I should code an interface called NetWorthVerifier with a corresponding implementation.

So I write an interface and an implementation like so:

Interface:
public interface NetWorthVerifier {
	public boolean isNetWorthAcceptable(User user);
}

Implemenation:

public class NetWorthVerifierImpl implements NetWorthVerifier {

	@Override
	public boolean isNetWorthAcceptable(User user) {
		return ((user.getStateOfResidence() == "CA" &&
				(user.getIncome() - user.getExpenses()) > 1000000.0) ||
				(user.getStateOfResidence() == "TX" &&
				(user.getIncome() - user.getExpenses() *1.19) > 1000000.0)
				);
	}

}

Better…now I can mock out this class and only test out the userRepository and emailService calls.

@RunWith(MockitoJUnitRunner.class)
public class UserServiceWithTDDTests {
	@Mock
	private NetWorthVerifier netWorthVerifier;
	@InjectMocks
	private UserService userService;
	private User user ;
	@Before
	public void setup(){
		//Given
		user = new User("foo");
	}
	public void testInviteUserWhenAMillionaire() {
		//When
		when(netWorthVerifier.isNetWorthAcceptable()).thenReturn(true);

		userService.inviteUser(user);

		//Then
		verify(userRepository, times(1)).addUser(any(User.class));
		verify(emailService, times(1)).sendEmail(anyString());
	}
}

And voila! We have a successful test without having to engineer data.
But.. what is more important, is that I have encapsulated the checking of net worth in a class of it’s own, thereby ensuring a clean separation of concerns.

Wait… what about testing the actual logic to see if the user’s net worth is met or not? We have not tested that.
Correct… and the reason is that that’s another test! In that case the NetWorthVerifier will be the SUT and the User object will be the collaborator.
So not only do we have modular code, we have modular tests too. That’s the beauty of it!

Hopefully I have been able to show you a pretty neat way to build software where we are always considering SUTs and collaborators that don’t necessarily line up with layers in our architecture.

So the next time someone proposes TDD… please give it a chance.. it may not be such a bad idea after all!


Viewing all articles
Browse latest Browse all 15

Trending Articles