Test Driven Design: Part 2
In the first part of this writing I showed the basics about test driven design. Of course, it was my understanding and experience of doing TDD, but I still think that many of you learned something new for themselves.
In the previous part we finished with development of domain resolver, which tells us list of domains and registered user names in these domains. The design was driven by tests, created before the actual code. The tests allowed us to see the picture from outside and define minimal usable interface for domain resolver component. This time, we will create a dialog box component which will be talking to domain resolver implementation to read data for its drop-down lists without actually knowing the source. The dialog will also do basic validation of entry and displaying of warning messages.
Books on the subject:
- JUnit in Action by Ted Husted, Vincent Massol
- Pragmatic Unit Testing in Java with JUnit by Andy Hunt, Dave Thomas
So where do we go now? Again, we need a plan. We have the use-cases for our dialog box and we need to cover them in tests. Here are these use-cases repeated:
- Select domain. When user selects domain, the list of user names available in this domain changes. At the moment of domain change there can be some user name selected and some password entered. If the same user name is present in the newly selected domain it and entered password should be preserve. Otherwise, the first available user name should be selected and the password cleared.
- Select user name. When user selects some other user name, the password should be cleared.
- Enter password.
- Confirm the entry. This action calls the validation which is checking that user name selected (it may come that some domain still has no users) and password entered (empty passwords aren’t allowed). If all of these conditions are true then we continue with closing the dialog, and otherwise we display some warning and abort closing the dialog.
- Cancel the login operation.
We know what the user will wish from our dialog; now let’s look at our dialog from the site of application. For application, our dialog is simply a source of data. A decision making object – a black box, which produces some output when given some input. The input is our domain resolver and output is domain-name-password tripple. Application calls the dialog with some input and gets the results back when processing is finished. Simple? Yes, it is.
We start with defining main login dialog class:
public class LoginDialog { }
Then we continue with tests. First, we define ins and outs.
public class TestLoginDialog extends TestCase
{
private LoginDialog dialog;
private IDomainsResolver domainsResolver;
protected void setUp() throws Exception
{
super.setUp();
dialog = new LoginDialog();
domainsResolver = new ConstantDomainResolver();
}
/** Defines the general use scenario. */
private void defineInitialQuery()
{
UserCredentials credentials =
dialog.askForCredentials(domainsResolver);
if (credentials != null)
{
String domain = credentials.getDomain();
String userName = credentials.getUserName();
String password = credentials.getPassword();
}
}
}
Please understand why the method signature doesn’t says “public void test…”. It’s not an actual test and it can’t be executed, once we have our class implemented. If executed it will show the dialog during testing which is not what we actually wish. This scenario is simple usage scenario, necessary to put ourselves on the place of dialog user (not a human, but user in terms of code) before we start diving into coding. It helps us understand what is required and observe the picture as a whole.
So, what we see is that we need UserCredentials
class to hold user information and method askForCredentials()
to ask user about it. We also made an assumption on how we would like to interpret the results and if they are going to be sufficient. You can see that we can interpret NULL
as “Cancel” and how we can fetch all data we need. It’s time to add new holder class and put a new method in LoginDialog
.
public final class UserCredentials
{
private String domain;
private String userName;
private String password;
/**
* Creates credentials object.
*
* @param domain domain name.
* @param userName user name.
* @param password user password.
*/
public UserCredentials(String domain, String userName,
String password)
{
this.domain = domain;
this.userName = userName;
this.password = password;
}
/**
* Returns domain name.
*
* @return domain name.
*/
public String getDomain()
{
return domain;
}
/**
* Returns user name.
*
* @return user name.
*/
public String getUserName()
{
return userName;
}
/**
* Returns user password.
*
* @return user password.
*/
public String getPassword()
{
return password;
}
}
Please note that we define only what is really necessary at the moment. There are no setters, no hashCode()/equals()/toString()
implementations – nothing that we don’t need right now. We might not need it, so why build it.
public class LoginDialog
{
/**
* Questions user for credentials.
*
* @param domainsResolver resolver of domains
* related information.
*
* @return user credentials or <code>NULL</code> if user
* denies to answer.
*/
public UserCredentials askForCredentials(
IDomainsResolver domainsResolver)
{
return null;
}
}
Our login dialog now has first method which has clear goals – to show the dialog and return user-entered information. By the way, this design allows you to think about other authorization enhancements in a future. You can wrap this implementation with some cache to automatically provide credentials if they are required, but user has already entered them (imagine two independent functions which require user information and once entered in one place this information will not be questioned in the other).
At this moment we have already decided that we build usual dialog with three controls (domain drop-down, user names drop-down and password field). Let’s look what happens when user makes selections and enters password. User uses controls to affect the internal model of our dialog. We usually affect the models in the same way by calling their methods. There’s no difference between user selecting something in drop-down and our tests calling methods on dialog class doing the same things. What happens is that Swing/AWT/SWT delivers events which are converted into method calls. Often developers put the event handling code into the listeners. Yes, it’s valid, but it doesn’t allow us to build tests and decouple UI from actual logic. Later, we might decide to change the UI part and put text field instead of user names drop-down; we will need to rewrite whole listener and handler code. Why do that? So let’s create tests for our use-cases. We will be using use-cases list at the top:
public class TestLoginDialog extends TestCase
{
...
/**
* Verifies that selection of domain changes the
* list of shown user names.
*/
public void testSelectingDomainChangesUsersList()
{
String[] registeredDomains =
domainsResolver.getRegisteredDomains();
assertTrue(&qout;Not enough domains to complete this test.",
registeredDomains.length > 1);
selectDomainAndVerifyUserNames(registeredDomains[0]);
selectDomainAndVerifyUserNames(registeredDomains[1]);
}
/**
* Selects domain in the dialog as if user did that and verifies the list
* of shown user names.
*
* @param domain domain to select.
*/
private void selectDomainAndVerifyUserNames(String domain)
{
String[] userNamesInDomain =
domainsResolver.getUserNamesInDomain(domain);
dialog.onSelectDomain(domain);
String[] shownUserNames = dialog.getShownUserNames();
assertTrue("Shown user names do not " +
"match these in domain.",
Arrays.equals(shownUserNames, userNamesInDomain));
}
}
Here we need two new methods from our dialog:
onSelectDomain(String)
- method which should be called in order to select diferent domain.getShownUserNames()
- method to report what names are currently shown to the user.
The first use-case sounds really simple – to change list of user names when domain changes – and this test case covers it. Moving to the next part – preserve or clear user/password depending on presence of selected user name in new domain.
public class TestLoginDialog extends TestCase
{
...
/**
* Verifies that user name and password are
* preserved when selected user name
* exists in both domains.
*/
public void testSelectingDomainPreserveUserAndPassword()
{
String[] registeredDomains =
domainsResolver.getRegisteredDomains();
assertTrue("Not enough domains to complete this test.",
registeredDomains.length > 1);
String domain0 = registeredDomains[0];
String domain1 = registeredDomains[1];
String someTestPassword = "some password";
String commonUserName =
findCommonUserName(domain0, domain1);
assertNotNull("There's no common user name in " +
"first two domains.", commonUserName);
UserCredentials userCredentials;
dialog.onSelectDomain(domain0);
dialog.onSelectUserName(commonUserName);
dialog.onPasswordChange(someTestPassword);
userCredentials = dialog.getCredentials();
assertEquals("Domain isn't selected.",
domain0, userCredentials.getDomain());
assertEquals("User name isn't set.",
commonUserName, userCredentials.getUserName());
assertEquals("Password isn't set.",
someTestPassword, userCredentials.getPassword());
dialog.onSelectDomain(domain1);
userCredentials = dialog.getCredentials();
assertEquals(domain1, userCredentials.getDomain());
assertEquals("User name should be preserved.",
commonUserName, userCredentials.getUserName());
assertEquals("Password should be preserved.",
someTestPassword, userCredentials.getPassword());
}
/**
* Finds common user name in both domains.
*
* @param firstDomain first domain.
* @param secondDomain second domain.
*
* @return common user name or <code>NULL</code>
* if name isn't there.
*/
private String findCommonUserName(String firstDomain,
String secondDomain)
{
List firstDomainNames = Arrays.asList(
domainsResolver.getUserNamesInDomain(firstDomain));
List secondDomainNames = Arrays.asList(
domainsResolver.getUserNamesInDomain(secondDomain));
String commonName = null;
for (int i = 0; commonName == null &&
i < firstDomainNames.size(); i++)
{
String name = (String)firstDomainNames.get(i);
if (secondDomainNames.contains(name)) commonName = name;
}
return commonName;
}
}
In this test we find common user name in two first domain (note that we know it is there because we created specific domain resolver in the first part), select first domain and this common user name, and enter password. Then we change to the other domain and verify that our user selection and password remain unchanged. This requirement is also covered, but we also need several new methods here:
onSelectUserName(String)
- selects user name and registers it in credentials.onPasswordChange(String)
- registers change of the password.getCredentials()
- returns current version of credentials.
Please note that all these methods we add are going to be useful and tested building blocks which we will use when building dialog implementation. For now they can be empty.
In the same way we continue to create tests to cover all other use cases with selection of domain (clearing user name/password is left), selection of user name and entering password. When it comes to validation of entry, you already know what to do. We simply create method isValid()
and it tells us whether current credentials are valid or no.
By the moment when you finish creation of tests for all use-cases, you will have a solid structure of your dialog class. Further step will be to create GUI and connect event listeners to your methods. This task is very well known to most of us. We are all fond of creating GUI’s, right?
In conclusion, I would like to note that it isn’t necessary to test everything and that’s why I didn’t pay significant attention to unit testing. The goal of this writing was to show you how one can use tests to drive the design of some module and how these tests can help to come out with better design solutions. Also it’s quite obvious that this particular example may seem to be too simple to pay that much attention to its testing. Maybe you are right, but who knows for sure. I know projects which have no tests and I know the projects with lots of them. Personally, I would like to find myself in the second team where everyone confident in their creations, you?
As you could see, nothing was complex in what we did and what’s important it borrowed some time, but also promised to return that time back when it will come to deployment. You will definitely have much less errors having that big tests base.
Let’s have it tested and keep coding!