<= Part 0 | Series Home | Part 2 =>
As I stated in the introduction, we’re building an app for my father-in-law who runs a small appliance repair business. His biggest pain point is that he captures all of his customer data on paper invoices, and so has no easy way to look up customer information. So his first need was a way to track the customers that he went to, and provide a way to search them.
Working with Accounts
Providing a searchable list of customers seemed like a good place to start. So I fired up Visual Studio and created a new WinForm app called ServiceTracker. I then added a class library called ServiceTrackerLogic and a Test application called ServiceTrackerLogicTests (I’m using MSTest for the tests). I knew we would capture the customer information as an Account, so it seemed logical to work on Accounts. I created a class in my Tests called AccountsTests.cs and wrote the first test:
[TestMethod]
public void AddingAccountToAccountsIncrementsListCount()
{
Account account = new Account("Test User", "813-555-1234");
Accounts accounts = new Accounts();
accounts.Add(account);
Assert.AreEqual(1, accounts.Count);
}
I added Account.cs and Accounts.cs classes to my Logic project, and added a constructor for Account.cs to take in two strings. For Accounts.cs I added a property called Count which returned 1 and an Add method which takes an Account. All green. I then added a second test:
[TestMethod]
public void AddingAccountToAccountsAddsAccountToList()
{
Account account = new Account("Test User", "813-555-1234");
Accounts accounts = new Accounts();
accounts.Add(account);
Assert.AreEqual(account, accounts[0]);
}
This fails that the indexer can’t be used. Since I plan on working with a basic list anyway, I have Accounts.cs subclass List
public class Accounts : List
I then delete the Add method and Count propery, since they are provided by the base List, and run the tests. All green.
Searching Accounts
The next thing I need to do is search the list for a name or phone number. Ideally I want one method which can do either, since the User Interface won’t care. That leads to our next test:
[TestMethod]
public void SearchingByFullNameReturnsMatchingAccount()
{
Account account = new Account("Test User", "813-555-1234");
Accounts accounts = new Accounts();
accounts.Add(account);
Accounts foundAccounts = accounts.FindAccount("Test User");
Assert.AreEqual(1, foundAccounts.Count);
Assert.AreEqual(account, foundAccounts[0]);
}
To compile, I add a FindAccount method to Accounts which takes in a string and returns Accounts. We now have a failing test, so let’s make it pass:
public Accounts FindAccount(string searchCriteria)
{
return this;
}
It’s sad that I can be just as annoying of a pair with myself. Fine, green test. So let’s get a better one:
[TestMethod]
public void SearchingByFullNameWhenMultipleAccountsReturnsOnlyMatchingAccount()
{
Account account = new Account("Test User", "813-555-1234");
Account account2 =
new Account("Justin Example", "813-555-1212");
Accounts accounts = new Accounts();
accounts.Add(account);
accounts.Add(account2);
Accounts foundAccounts = accounts.FindAccount("Test User");
Assert.AreEqual(1, foundAccounts.Count);
Assert.AreEqual(account, foundAccounts[0]);
}
Aha! Failing test. Now we have to do something about it. While we could loop through all of the members in Accounts, the List
public Accounts FindAccount(string searchCriteria)
{
Accounts foundAccounts = this.FindAll(MatchesFullName);
return foundAccounts;
}
Well, at least we want to do that. Turns out FindAll returns a List
public Accounts FindAccount(string searchCriteria)
{
Accounts foundAccounts =
new Accounts(this.FindAll(MatchesFullName));
return foundAccounts;
}
MatchesFullName is the other method which we added. It takes in an Account and returns true if that account matches the criteria. Of course, it doesn’t show how to get the criteria to the search method, but that’s Ok, because we can just do:
private bool MatchesFullName(Account account)
{
return account.Name.Contains("Test User");
}
Which doesn’t compile because we need to add accessors for Name and PhoneNumber to our Account.cs class and modify the Account constructor to set them. Ok, with that done, we now have green tests. Looking at our classes, our production code doesn’t quite need any refactoring, but our tests could. Let’s pull up some of the commonly used variables:
private Account testAccountTestUser;
private Accounts initializedAccounts;
[TestInitialize]
public void Setup()
{
testAccountTestUser =
new Account(“Test User”, “813-555-1234”);
initializedAccounts = new Accounts();
initializedAccounts.Add(testAccountTestUser);
}
Ok, now we said early we want one method to search both name and phone number. So let’s write a test which expresses that:
[TestMethod]
public void SearchingByFullPhoneNumberReturnsMatchingAccount()
{
Accounts foundAccounts =
initializedAccounts.FindAccount("813-555-1234");
Assert.AreEqual(1, foundAccounts.Count);
Assert.AreEqual(testAccountTestUser, foundAccounts[0]);
}
Well, this passes. Looks like I have to break out a deeper test:
[TestMethod]
public void SearchingByFullPhoneWhenMultipleAccountsReturnsOnlyMatchingAccount()
{
Account account2 =
new Account("Justin Example", "813-555-1212");
initializedAccounts.Add(account2);
Accounts foundAccounts =
initializedAccounts.FindAccount("813-555-1234");
Assert.AreEqual(1, foundAccounts.Count);
Assert.AreEqual(testAccountTestUser, foundAccounts[0]);
}
There. A failing test. So now we have to tackle how to pass a parameter to our search method. The easiest way seems to be to have an instance variable we set with the search criteria, and then use that in the search method. So we modify our Accounts.cs class to:
private string searchCriteria;
public Accounts FindAccounts(string criteria)
{
searchCriteria = criteria;
Accounts foundAccounts = new Accounts(
this.FindAll(MatchesFullNameOrPhone));
searchCriteria = String.Empty;
}
My internal pair chides me for the extra code, but I can’t bring myself not to put it in there. We can now modify our old MatchesFullName to:
private bool MatchesFullNameOrPhone(Account account)
{
return account.Name == searchCriteria
|| account.PhoneNumber == searchCriteria;
}
Which passes our tests.
Searching partial matches
Ok, almost there. The last thing we need to write behavior logic for is searching partial names and phone numbers:
[TestMethod]
public void SearchingByCharactersAtBeginningOfNameFindsAccount()
{
Accounts foundAccounts =
initializedAccounts.FindAccount("Tes");
Assert.AreEqual(1, foundAccounts.Count);
Assert.AreEqual(testAccountTestUser, foundAccounts[0]);
}
Which fails. So we modify our Matches to add an or condition for account.Name.StartsWith(searchCriteria). Green test. We then write similar tests for text at the end of the name (passing with EndsWith) and for text in the middle of the name (passing with Contains). We then do the same thing for Phone Number, and after a little refactoring we end up with:
private bool MatchesFullNameOrPhone(Account account)
{
return account.Name.Contains(searchCriteria)
|| account.PhoneNumber.Contains(searchCriteria);
}
Nice and clean. And all of our tests are passing now too. So our first requirement is out of the way – we can add Accounts and search for them based on name or phone number. In the next part, we’ll actually create a user interface for searching and displaying the Accounts so our customer can start to see real value from the application.
Hi Cory – Nice post, I enjoyed reading it.
2 points to notice:
1. You might be able to get rid of the searchCriteria member by using inline anonymous delegates, like this:
public Accounts FindAccounts(string criteria)
{
Accounts foundAccounts = new Accounts(this.FindAll(delegate(Account account)
{
return MatchesFullNameOrPhone(account, criteria);
}
));
}
private bool MatchesFullNameOrPhone(Account account, string searchCriteria)
{
return account.Name.Contains(searchCriteria)
|| account.PhoneNumber.Contains(searchCriteria);
}
2. I’ve found a nice source-code formatter for blogger – you might want to use it (no offense intended :-)
http://formatmysourcecode.blogspot.com/
All the best,
– Avi