Cory Foy

Thursday, November 29, 2007

Slides from the Tampa IASA Meeting

Last night I presented at the Tampa IASA meeting on a topic called "Getting Unstuck: Working with Legacy Code and Data". It was basically a combination of Working Effectively with Legacy Code and Refactoring Databases for Software Architects. The presentation went really well - especially because so many of the points hit close to home.

I got asked to post the slide deck, and so I did. You can download it below - though I converted it to the older PPT format (I have Office 2007) so hopefully no quirks in it.

Thanks again to Tom Fuller and all the guys and gals from the Tampa chapter - it was great fun!

Saturday, November 24, 2007

Legacy Data and BDUF

Next week I'm speaking at a Tampa IASA meeting about approaching legacy code as architects. In getting ready for it, I was rereading some articles by Scott Ambler when I came across the following quote:

An interesting observation is that when you take a big design up front (BDUF) approach to development where your database schema is created early in the life of your project you are effectively inflicting a legacy schema on yourself. Don’t do this.

Oftentimes we forget to take our databases into account with our agile processes. It is vital not to leave them out to avoid just pushing all of the problems you were looking to solve by using a more agile methodology to your data integration layer.

Wednesday, November 21, 2007

Happy Thanksgiving!

Gobble, gobbleHere in the US, Thursday is the day we celebrate Thanksgiving. It seems to be a great excuse to have the family over, overindulge in food, and then have good conversations (or listen to the football broadcaster's conversation). For us, we'll be having lots of family over, and it should be a good time. But whatever you are doing, Happy Thanksgiving to you!

Wednesday, November 14, 2007

TDD of a WinForm app - Part 4 - Finishing the Account Data UI

<= Part 3 | Series Home | Part 5 =>

11/14/07 - Updated to fix a problem where Windows Live Writer didn't upload the whole article

At the end of the last post, we had automated tests in place that showed our business logic correctly triggered a window to open, and that the UI implemented the logic correctly. However, that window didn't do much of anything - especially not add accounts to the Accounts collection. Let's get that fixed!

 

Reviewing Our Progress

Right now, our AccountPresenter class is doing everything it can to make sure AddAccount is correctly called and hooked up. So we can now move to implementing the business logic in AddAccountView. There are two pieces - validating the fields when a user submits the form, and correctly creating an Account instance and calling back to the AccountPresenter to add it. It looks something like:

So our next tests are going to be on the AddAccountView side. I create a new test class called AddAccountPresenterTests.cs in my ServiceTrackerPresenterTests project. Since we'll have a submit button on the form, we should make sure we are listening for it. Since the button will be used to make sure the Account information is valid, and then create the Account and pass it back to the caller, we'll call it AccountReadyForCreation perhaps? Any thoughts on better names?

 

Being A Good Listener

Copying over the similar test from AccountPresenter, we get:

[TestMethod]
public void PresenterRegistersAsListenerForAccountReadyForCreation()
{
  IAddAccountView view = new StubAddAccountView();
  Assert.AreEqual(0, ((StubAddAccountView)
    view).AccountReadyForCreationListenerCount);
  AddAccountPresenter presenter =
    new AddAccountPresenter(view);
  int expectedListenersCount = 1;
  int actualListenersCount =
    ((StubAddAccountView)view)
     .AccountReadyForCreationListenerCount;
  Assert.AreEqual(expectedListenersCount, actualListenersCount);
}

However, one of the feedback items I got from Brian Button was that I really have two test cases there - one for the zero case, and one for the listener case. We had a good discussion on the TDD list about it, and two solutions came from it:

  • Set the AccountReadyForCreationListenerCount in the test so that we know what it is
  • Use a method to make it clear what we are checking - something like SanityCheckStub()

However, I like the sanity check as is, especially since the get method is checking the invocation list. But it is something to keep in mind for later tests, and something I may move to SetUp if it gets too noisy.

I get the test compiling by adding the StubAddAccountView class to the AddAccountPresenterTests.cs file and having it implement IAddAccountView. I also give it a property called AccountReadyForCreationListenerCount which is just returning 0 for now. Finally, I create an AddAccountPresenter.cs in the ServiceTrackerPresenter project and give it a constructor which takes in an IAddAccountView. Running the tests gives us the predictable message that the count is 0 when we expected 1, so let's get that working. First, we'll add the AccountReadyForCreation event to the interface, which forces the add to the Stub class:

public delegate void AddAccountViewEventDelegate(object sender, IAccountView view);

public interface IAddAccountView
{
  event AddAccountViewEventDelegate AccountReadyForCreation;
}

class StubAddAccountView : IAddAccountView
{
  public event AddAccountViewEventDelegate AccountReadyForCreation;

  public int AccountReadyForCreationListenerCount
  {
    get { return 0; }
  }
}

Now we can modify our presenter to listen for the event:

public class AddAccountPresenter
{
  public AddAccountPresenter(IAddAccountView view)
  {
    view.AccountReadyForCreation += new AddAccountViewEventDelegate(view_AccountReadyForCreation);
  }

  void view_AccountReadyForCreation(object sender, IAccountView view)
  {
  }
}

And finally, modify our test to make sure it is checking the invocation list:

public int AccountReadyForCreationListenerCount
{
  get
  {
    if (AccountReadyForCreation != null)
    {
      return AccountReadyForCreation.GetInvocationList().Count();
    }
    return 0;
  }
}

Which gives us our green test. Now what we need to happen is that when the event is fired, the data is validated, a new Account object is created, and a callback method on the presenter passed to the AddAccount form needs to be called with this new Account so it can be added. However, we seem to be having a problem having a clear vision of the next test, which means we probably have a design flaw somewhere. So let's step back for a second.

 

Reviewing Our Design

In the scheme of things, the workflow for what we are doing is that the user pushes a button to add a new account, enters the account information, and clicks save. When they click save, the form should validate the information, and if it is valid, create an account object and pass it back to the AccountView presenter to add it to the Accounts collection, which will then update the UI. So our tests would be:

 

  • A valid account is added to the accounts collection
  • A user clicking submit with an invalid field should get a warning message (with tests for each field that could be invalid)

That helps clarify things, at least for me. We need our submit action to do two separate things - validate the information, and pass the account to the accounts collection. And maybe that's the rub - neither the presenter or the view seem like the best place to create the account and validate - that seems like the responsibility of the Account class. It should be able to create itself and validate that the fields are, well, valid. I'm envisioning a way to pass the AddAccount form to the Account class to have it create an Account object. Using this, Account can pull what it needs from the view to construct and validate the account data, and then the presenter is responsible for passing the form over, and handling the result. So now we'll need the following:

  • The presenter creates an Account class on submit by passing the view to a factory method in the Account class
  • The presenter passes the Account to the callback on successful validation
  • The presenter closes the form on succesful validation
  • The presenter leaves the form open on validation problems

So writing our first test gives us this mess:

[TestMethod]
public void PresenterPassesAccountObjectToCallbackWhenValid()
{
  Account fakeAccount = new Account("Justin Example", "813-555-1212");
  account.IsValid = true;
  StubAccountPresenter fakeAccountPresenter = new StubAccountPresenter();
  StubAddAccountView fakeView = new StubAddAccountView();
  fakeView.Presenter = fakeAccountPresenter;
  AddAccountPresenter presenter = new AddAccountPresenter(fakeView);
  presenter.ProcessAccount(fakeAccount);
  Assert.AreEqual(fakeAccount, fakeAccountPresenter.AccountToBeAdded);
}

Remember that in our last part, we ended by passing the AccountPresenter as a parameter to AddAccountView so that the Account could be added to the Accounts collection. That seems unwise now - we really should just have a DataSource that we pass around that also notifies interested parties when it has been updated. Let's delete the above test for now and fix that.

 

Passing Notes During Class Is Bad

The first thing we need to change is the PresenterRequestsNewWindowWithItselfWhenAddAccountRequested test in AccountPresenterTests. It should check to make sure the DataSource - the Accounts collection - was passed in. The problem is that we get Accounts from the LoadAccounts() method in AccountPresenter, and don't hold a reference to it. What we'll do is modify the Accounts collection to be able to load and hold the accounts in it, and use that within the form. The change moves the code from LoadAccounts into Accounts, and then calls that:

public class Accounts : List<Account>
{
  private static Accounts accountStore;
  public static Accounts AccountStore
  {
    get
    {
      if (accountStore == null)
      {
        accountStore = new Accounts();
        accountStore.Add(new Account("Test User", "813-555-1234"));
        accountStore.Add(new Account("Justin Example", "813-555-1212"));
      }
      return accountStore;
    }
  }
  //...
}

public AccountPresenter
{
  //...
  private Accounts LoadAccounts()
  {
    return Accounts.AccountStore;
  }
}

With that change made, let's run our tests. All green.

 

Updating The Grid

Next, we need to make sure the DataGrid gets updated when the Accounts collection is updated. First, let's make sure that our DataSource and AccountStore match:

[TestMethod]
public void DataGridSourceMatchesAccountsCollectionOnLoad()
{
  IAccountView view = new StubAccountView();
  AccountPresenter presenter = new AccountPresenter(view);
  ((StubAccountView)view).FireAccountViewLoadEvent();
  Assert.AreEqual(
    Accounts.AccountStore,
    view.AccountGrid.DataSource);
}

Green test. Now to make sure it is updated:

[TestMethod]
public void DataGridUpdatedWhenAccountsCollectionChanged()
{
  IAccountView view = new StubAccountView();
  AccountPresenter presenter = new AccountPresenter(view);
  ((StubAccountView)view).FireAccountViewLoadEvent();
  Accounts.AccountStore.Add(new Account("Faked Out", "813-111-2222"));
  Assert.AreEqual(Accounts.AccountStore, view.AccountGrid.DataSource);
}

And...another green test? Having worked with DataGrids before, I know it isn't that easy, and it probably a side effect of using a stub. I try the following test in our MainFormTests:

[TestMethod]
public void DataGridUpdatedWhenAccountsCollectionChanged()
{
  StubMainForm view = new StubMainForm();
  StubAccountPresenter presenter = new StubAccountPresenter(view);
  view.FireLoadEvent();
  Accounts.AccountStore.Add(new Account("Faked Out", "813-111-2222"));
  Assert.AreEqual(Accounts.AccountStore, view.AccountGrid.DataSource);
}

But it passes as well. Let's try one more test:

[TestMethod]
public void RowsDisplayedUpdatesWhenAccountsCollectionChanged()
{
  using (StubMainForm view = new StubMainForm())
  {
    view.Show();
    DataGridView realGrid = view.GetAccountsGrid();
    Assert.IsNotNull(realGrid, "realGrid");
    Assert.AreEqual(2, realGrid.RowCount);
    Accounts.AccountStore.Add(new Account("Faked out", "813-111-2222"));
    Assert.AreEqual(3, realGrid.RowCount);
    view.Close();
  }
}

Basically we are accessing the actual AccountsGrid object in the UI and checking to make sure the RowCount was updated when the DataSource changed. To make this compile, we had to widen the visibility of AccountsGrid in MainForm.cs to protected, and then added a method in the StubMainForm called GetAccountsGrid which returns it. This test fails that the row count was not updated. In other words, the DataSource instance gets updated, but that doesn't mean the UI gets repainted. To fix this, we'll need to do two things - one, have our Accounts have a way to notify us when it has been updated, and then have our presenter listening for this event and updating the datasource of the grid when this happens.

Since we'll need the former before we can have the latter (which is what the test above is showing), let's comment this test out and write a test which expresses what we need Accounts to do.

 

Just Wake Me If You Need Anything

In our AccountsTests in ServiceTrackerLogicTests, we add the following:

[TestMethod]
public void AccountsCollectionNotifiesListenersWhenAdded()
{
  Accounts accounts = initializedAccounts;
  bool updatedFired = false;
  accounts.Updated += delegate { updatedFired = true; };
  accounts.Add(new Account("Blah", "555-111-1212"));
  Assert.IsTrue(updatedFired);
}

Which doesn't compile because Updated isn't an event. So we add the following to Accounts:

public event EventHandler Updated;

And we have a compiled, red test. We make it pass by adding the following to Accounts:

public new void Add(Account item)
{
  base.Add(item);
  if(Updated != null)
    Updated(this, null);
}

But, looking at this, it isn't ideal. using the new operator is fine if you are always working with instances of that type. But what if someone uses Accounts as a List? Besides, we can't be the only ones who want an updatable list. Indeed, .NET 3.0 introduces an ObservableCollection. Let's see if we can just swap it out. We change the Accounts class to:

public class Accounts : ObservableCollection<Account>

To see ObservableCollection, we had to add a reference to Windowsbase.dll. We also had to change the constructors we were using in Accounts, and modify the FindAccounts method (since FindAll isn't in ObservableCollection). Once we got it compiling, it caused a cascade of compiler errors that we didn't have Windowsbase.dll in our other projects that used Accounts. Fixing that got us a clean compile, and running all of our tests shows we didn't break anything. If that doesn't show the power of TDD, I don't know what would.

We do have a test we have to fix. Our AccountsCollectionNotifiesListenersWhenAdded test relies on there being an Updated event, and using the shadowed Add method. Let's change that to use the CollectionChanged event, and comment out the add method we added:

[TestMethod]
public void AccountsCollectionNotifiesListenersWhenAdded()
{
  Accounts accounts = initializedAccounts;
  bool updatedFired = false;
  accounts.CollectionChanged += delegate { updatedFired = true; };
  accounts.Add(new Account("Blah", "555-111-1212"));
  Assert.IsTrue(updatedFired);
}

And it stays green. So now we have an ObservableCollection in place, we can go back to our rows displayed problem. Let's put our test back in place:

[TestMethod]
public void RowsDisplayedUpdatesWhenAccountsCollectionChanged()
{
  using (StubMainForm view = new StubMainForm())
  {
    view.Show();
    DataGridView realGrid = view.GetAccountsGrid();
    Assert.IsNotNull(realGrid, "realGrid");
    Assert.AreEqual(2, realGrid.RowCount);
    Accounts.AccountStore.Add(new Account("Faked out", "813-111-2222"));
    Assert.AreEqual(3, realGrid.RowCount);
    view.Close();
  }
}

Rerunning the test shows it is still failing. That's important because you never know if a change you made previously made this test pass. For example, it's hard to know if the GUI is wired to automatically listen for updates to the DataSource if the DataSource is an ObservableCollection. Since it isn't, we need to wire it up ourselves. So in our presenter, we can add the following line in our constructor:

Accounts.AccountStore.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(AccountStore_CollectionChanged);

However, we find that we aren't holding a reference to the view currently in the presenter - mainly because we've been working primarily on callbacks. So I add an instance variable for the view, and implement AccountStore_CollectionChanged as:

void AccountStore_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
  accountView.AccountGrid.DataSource = Accounts.AccountStore;
}

That should allow our test to pass, but it doesn't. Knowing some quirks about UI elements, let's try the following first:

void AccountStore_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
accountView.AccountGrid.DataSource = null;
accountView.AccountGrid.DataSource = Accounts.AccountStore;
}

Sure enough, that lets our test pass. Now, to the casual observer, it may not be clear why we are doing this. So we are going to add something you haven't seen up till now - a comment:

//Must set DataSource to null first or else
//the GUI won't recognize the change

This test is a great case where, even using the MVP pattern, it is important to be sure you have acceptance tests exercising the real GUI.

 

Seriously. Can We Add An Account Now?

Now lets run our full test suite - 30 out of 30 passed. Good to go! Let's get the Account add working. Remember that what led us down the path we just went was that we were passing a reference to the AccountPresenter to AddAccountView. Then we were going to have to find a way to have AddAccountPresenter callback to AccountPresenter to update the Account grid. However, now all we need to do is have AddAccountPresenter update the Accounts collection, and AccountPresenter will handle updating the GUI. Therefore this test in AccountPresenterTests:

[TestMethod]
public void PresenterRequestsNewWindowWithItselfWhenAddAccountRequested()
{
  IAccountView view = new StubAccountView();
  AccountPresenter presenter = new AccountPresenter(view);
  ((StubAccountView)view).FireAddAccountRequestedEvent();
  Assert.AreEqual(presenter, ((StubAccountView)view).PresenterForOpenAccountWindow);
}

Isn't correct. What we need was the test we originally had here which just opens the window as a dialog. The test from Part 3 was:

[TestMethod]
public void PresenterRequestsNewWindowFromViewWhenAddCustomerRequested()
{
  IAccountView view = new StubAccountView();
  AccountPresenter presenter = new AccountPresenter(view);
  ((StubAccountView)view).FireAddCustomerRequestedEvent();
  Assert.IsTrue(((StubAccountView)view).NewCustomerWindowRequested);
}

which was superseded by the PresenterRequestsNewWindowWithItselfWhenAddAccountRequested test. Let's swap those tests. To do that, we'll first put in the old test and make sure it passes. It doesn't compile, because we changed the name to FireAddAccountRequestEvent. NewCustomerWindowRequested (which should be NewAccountWindowRequested) was removed, so we add back in the instance variable and change the stub to set that when OpenAccountWindow is called (leaving in the old code for now). That lets the test compile, and pass.

Now we can remove the old test. The easiest thing it to remove the passing of the parameter in the OpenAccountWindow defintion in the interface, and then lean on the compiler to find the places we need to fix. So we change IAccountView's definition to:

void OpenAccountWindow();

And we clean up MainForm and StubAccountView, as well as modify AccountPresenter to clean all of that up. We also remove the PresenterForOpenAccountWindow property we exposed in StubAccountView. Finally, we remove the PresenterRequestsNewWindowWithItselfWhenAddAccountRequested test since it can't function anymore. Well, actually, then we finally run all of our tests to prove we haven't broken anything - and we haven't!

So, finally we can go back and get the AddAccountView adding accounts. Looking at our AddAccountPresenterTests, the only thing we have it doing is listening for the AccountReadyForCreation event. But now, instead of our presenter having to do all kinds of crazy callbacks, it just needs to create an Account from the information on the form, validate it, and add it to the Accounts collection. That's much better.

 

Letting The Account Deal With Itself

At the beginning of the article, we talked about Account having a factory method it could use to create Account instances from the form. The easiest way would be to create an interface - IAccount - which defines the contract for an Account, and then have the factory method spit out an Account from there. It would be nice if the AddAccountView could be an Account, and there certainly may be better ways. So let's drive out a factory method in the Account class:

[TestMethod]
public void FactoryMethodCreatesAccountWithCorrectInformation()
{
  IAccount account = new Account("My User", "813-111-2222");
  account.Address = "12345 Blah Circle"
  account.City = "Tampa"
  account.State = "FL"
  account.ZipCode = "12345"

  Account createdAccount = Account.CreateAccount(account);
  Assert.AreEqual(account as Account, createdAccount);
}

It doesn't compile because neither IAccount nor CreateAccount exist. We'll tackle IAccount first. In our ServiceTrackerLogic project we create:

public interface IAccount
{
  string Name { get; set; }
  string PhoneNumber { get; set; }
  string Address { get; set; }
  string City { get; set; }
  string State { get; set; }
  string ZipCode { get; set; }
}

Now we get an error that we can't cast IAccount to Account - presumably because Account doesn't implement IAccount. We fix that with:

public class Account : IAccount

Now we get that CreateAccount doesn't exist. We'll just create the static method and have it return a new Account(). Now we can run our test, see it fail, and fix it by changing CreateAccount to:

public static Account CreateAccount(IAccount account)
{
  Account a = new Account(account.Name, account.PhoneNumber);
  a.Address = account.Address;
  a.City = account.City;
  a.State = account.State;
  a.ZipCode = account.ZipCode;
  return a;
}

We could also use the new C# syntax to do:

Account a = new Account(account.Name, account.PhoneNumber) {
  Address = account.Address
  //...
};

But we'll leave it as is for now. Now our test passes, meaning we have the plumbing in place. We now have two options. We can either have AddAccountView implement IAccount - or have IAddAccountView inherit from it. I prefer the latter, which looks like:

public interface IAddAccountView : IAccount

Doing that causes a host of compiler errors since neither our AddAccountForm nor our StubAddAccountView expose those properties. Let's fix that. For the Stub, we'll just expose the properties to return empty strings:

public string Name
{
get { return String.Empty; }
set { }
}
//etc, etc

For our actual form, we'll add the fields to the form using the designer:

Each of the fields is named FieldNameText - for example, PhoneNumberText. This is because the interface requires us to implement PhoneNumber as a property returning/receiving a string. So to implement the interface, we now need to add the properties. However, something interesting happened. We clicked on the interface declaration in AddAccountForm and told Visual Studio to implement the interface. It added all of the fields except Name. Wondering why, we checked in IAccount - but it was there. Then it hit us - AddAccountForm is a Form which exposes a Name property. The only real option we have is to modify our Account class to call it AccountName. So we go into IAccount and do a Refactor->Rename to AccountName, and clean up the references. We also run our tests for good measure - everything's still green.

To implement the interface in the Form, the properties just map the interface property to the text property of the control. It looks like:

public string AccountName
{
  get { return NameText.Text; }
  set { NameText.Text = value; }
}

 

Passing Accounts In Class Is Good

With all of these changes in place (and our tests all green) we can now write the next test:

[TestMethod]
public void ValidAccountGetsAddedToAccountsOnSubmit()
{
  IAddAccountView view = new StubAddAccountView();
  view.AccountName = "Testing Account"
  view.PhoneNumber = "813-555-1212"
  AddAccountPresenter presenter = new AddAccountPresenter(view);
  ((StubAddAccountView)view).FireAccountReadyForCreationEvent();
  Assert.AreEqual(3, Accounts.AccountStore.Count);
  Assert.IsTrue(Accounts.AccountStore.Contains(new Account("Testing Account", "813-555-1212")));
}

We have to fix the compiler error that FireAccountReadyForCreationEvent is not implemented. In implementing the method, we're getting a strange error that the Stub form can't be cast to the interface. We can clearly see that the stub implements the interface. Looking at the delegate declaration we see we did:

public delegate void AddAccountViewEventDelegate(object sender, IAccountView view);

Instead of:

public delegate void AddAccountViewEventDelegate(object sender, IAddAccountView view);

Making that change makes everything better. Now our test compiles, and running it gives us that Accounts.AccountStore has 2 elements when we are expecting 3. To fix this, we modify the Presenter to act on the event:

void view_AccountReadyForCreation(object sender, IAddAccountView view)
{
Accounts.AccountStore.Add(Account.CreateAccount(view));
}

Now that's nice and easy. It doesn't take into account validation or anything yet, but I think it will make this test pass nicely. In fact, part of our test passes nicely. There are now 3 objects in the collection, but the Contains test is not passing. We'll change the test to use:

[TestMethod]
public void ValidAccountGetsAddedToAccountsOnSubmit()
{
  IAddAccountView view = new StubAddAccountView();
  view.AccountName = "Testing Account"
  view.PhoneNumber = "813-555-1212"
  AddAccountPresenter presenter = new AddAccountPresenter(view);
  ((StubAddAccountView)view).FireAccountReadyForCreationEvent();
  Assert.AreEqual(3, Accounts.AccountStore.Count);
  Account foundAccount =
Accounts.AccountStore.FindAccount("Testing Account")[0];
  Assert.IsNotNull(foundAccount);
}

Which also fails. Let's try something different:

Account foundAccount = Accounts.AccountStore[2];
Assert.AreEqual("Testing Account", foundAccount.AccountName);

Which fails with it expecting "Testing Account" but getting "". Of course! The stub class we're using didn't implement the properties as actual instance variables. They get changed to:

string accountName;
string phoneNumber;
//...

public string AccountName
{
  get { return accountName; }
  set { accountName = value; }
}

public string PhoneNumber
{
  get { return phoneNumber; }
  set { phoneNumber = value; }
}
//...

Our test passes now, so we switch it back to the original contains:

Assert.IsTrue(Accounts.AccountStore.Contains(new Account("Testing Account", "813-555-1212")));

which now passes.

 

Failure Is Not An Options

We run all of our tests and...failure? Looking at the test list, the test we just ran failed. It's saying it expected 3, but was 4. Aha! Our test has a side effect - it's modifying the collection, which is screwing everything else up. We can fix that by putting the following in the test:

Accounts accountsBeforeTest = Accounts.AccountStore;
//...
Accounts.AccountStore = accountsBeforeTest;

but AccountStore is read-only. This is a very important issue - tests should never leave behind cruft that could cause other tests to fail. The simplest thing would be to create a setter, and we'll run with that for now. This will probably come up again as we add persistance to Accounts, but right now we need to get past the red test. So we add the setter, and the code from above. However, we will watch closely and if we add another test which modifies the collection we'll move that code to the Setup/Teardown.

So we run our tests and...still red? Same error message too. So another test must be modifying the collection. In fact, the very first test in the list is DataGridUpdatedWhenAccountsCollectionChanged - I bet that's the culprit. And indeed, we are adding an account there. Let's add the same lines before, but something is just not smelling right with this. Turns out we are going to have to address it now - because that didn't let our test pass.

What we should be doing is working on a known collection. So instead of getting the value, setting it to what we expect. Another way would be just to set AccountStore to null at the end of the test since we lazy load it back up on the get method. However, we need to find all of the tests that Add something to the collection. We do a search for "AccountStore.Add" and find three tests that add something to the collection. Adding the Accounts.AccountStore = null line to all three locations lets the tests pass.

Since this is something that needs to be done at the end of the test, I'll add TearDown methods for both test classes that had the problem - MainFormTests and AccountPresenterTests. It looks like:

[TestCleanup]
public void TearDown()
{
  //Clean up AccountStore
  Accounts.AccountStore = null;
}

With that in place, we rerun the tests, and they are all still passing. So we remove out setting the AccountStore to null in each of the tests and rerun. Still green, so it seems safe to move on. It does make me start to wonder if perhaps a different way of organizing the tests might have made that easier. But we'll save that for later.

 

Wiring Up the UI

Ok, so the last test we wrote showed that on Submit the Presenter will create an Account using the factory method, passing the view in, and add that to the Accounts collection. Let's get that working on the UI itself. We add a new test class called AddAccountFormTests.cs to the ServiceTrackerTests project. The first thing we need to check is that the form is calling AccountReadyForCreation when the AddAccount button is clicked:

[TestMethod]
public void AccountReadyForCreationFiredOnAddAccountClick()
{
  using (AddAccountForm addAccountForm = new AddAccountForm())
  {
    bool accountReadyForCreationFired = false;
    addAccountForm.AccountReadyForCreation += delegate { accountReadyForCreationFired = true; };
    addAccountForm.Show();
    addAccountForm.AddAccount.PerformClick();
    Assert.IsTrue(accountReadyForCreationFired);
  }
}

We have to go make AddAccount public for this to compile, which we do. Running the test fails, so we need to go implement this. So we add the following to AddAccountForm:

private void AddAccount_Click(object sender, EventArgs e)
{
  if (AccountReadyForCreation != null)
    AccountReadyForCreation(this, this);
}

Which passes our test. Next, let's make sure the Account is making it into the collection when we fire AccountReadyForCreation:

[TestMethod]
public void ValidAccountAddedToCollectionOnAddAccountClick()
{
  using (AddAccountForm addAccountForm = new AddAccountForm())
  {
    addAccountForm.Show();
    addAccountForm.AccountName = "Form Testing"
    addAccountForm.PhoneNumber = "555-112-3344"
    addAccountForm.AddAccount.PerformClick();
  }
  Assert.IsTrue(Accounts.AccountStore.Contains(new Account("Form Testing", "555-112-3344")));
}

This test tells us two things. First, it doesn't work. Second, we'll be modifying the Accounts collection, so we need our TearDown method. So what would cause this not to work? If the presenter isn't aware of the view. And sure enough, looking at the code behind, nowhere do we hook the two together. So we add the following to AddAccountForm:

private AddAccountPresenter presenter;

public AddAccountForm()
{
  InitializeComponent();
  presenter = new AddAccountPresenter(this);
}

And now our test passes! Let's run them all to make sure no crud bugs are around...and all green.

 

You Don't Have To Go Home (But You Can't Stay Here)

So we've got one more test to write. If the Account is valid (which they all are right now) then the form should close. So we need to test that the presenter calls close on the view. We go over to AddAccountPresenterTests and see that we didn't add the TearDown here. We do that, clean up the test, and rerun all tests. Good to go. Marching on, we add the following test:

[TestMethod]
public void PresenterCallsCloseAfterValidAccountAdded()
{
  IAddAccountView view = new StubAddAccountView();
  view.AccountName = "Testing Account"
  view.PhoneNumber = "813-555-1212"
  AddAccountPresenter presenter = new AddAccountPresenter(view);
  ((StubAddAccountView)view).FireAccountReadyForCreationEvent();
  Assert.IsTrue(((StubAddAccountView)view).CloseWasCalled);
}

Now we have to add CloseWasCalled to our stub. Adding the instance variable was easy enough, but what is missing is that our interface doesn't have a close method. So we add that. We then implement Close in our stub by having it set our instance variable. Note that we don't need to add anything to AddAccountForm - it has a Close method already. Now that our test compiles we run it - and it fails. So let's fix that by changing the presenter:

void view_AccountReadyForCreation(object sender, IAddAccountView view)
{
  Accounts.AccountStore.Add(Account.CreateAccount(view));
  view.Close();
}

And sure enough, our close method is called. Finally, let's make sure our form is being closed in our AddAccountFormTests:

[TestMethod]
public void FormClosedAfterValidAccountAddedFromClick()
{
  using (AddAccountForm addAccountForm = new AddAccountForm())
  {
    addAccountForm.Show();
    addAccountForm.AccountName = "Form Testing"
    addAccountForm.PhoneNumber = "555-112-3344"
    addAccountForm.AddAccount.PerformClick();
    Assert.IsFalse(addAccountForm.Visible);
}
}

And a green test it is. Just to be picky, I add an Assert.IsTrue(addAccountForm.Visible) right after I call Show, and the test still passes, so I pull it back out. We run all of our tests, and we've got a green light to go.

We've done a lot of coding and testing, but are they really giving us a true view to what is going on? If so, we should be able to start up the application, hit the Add Account button, have a window pop up, enter some information, hit the Save button, see the window close, and see our entry in the DataGrid. Without further ado..

After F5:

image

Hitting the Add Account button brings up the form:

image

And entering in some information and hitting Add Account closes the window and gives us:

image

So yes, we really can trust the tests. That's welcome news!

We still have some validation left, since Account name and Phone number are required, but this seems like a good stopping point for now.

To help aid in the process of following along, I'm going to be including the code downloads at the bottom of each part. I'm using MSTest and VS2008 if you are following along at home.

Any feedback is most welcome!

Friday, November 02, 2007

TDD of a WinForm app - Part 3 - Adding Account Data

<= Part 2 | Series Home | Part 4 =>

So we demo'd the application for our customer, and he liked the direction so far. It wasn't much functionality, but it did confirm that it was what he wanted. He would like to see the Account screens finished so that he can try out entering accounts with all of their data. So that's what we'll work on next.

To do this, let's list what needs to be done:

  • Finish out the Account class to include all of the user data
  • Provide a way to add customers
  • Provide a way to edit existing customers
  • Provide a way to delete customers

Notice that we don't necessarily have to hook up to a database or data store yet - we are focusing on functionality from the front end, but will need to tackle that story eventually. So let's get started.

 

Finishing out the Account form

The first item is to finish out the Account class. In addition to Name and Phone Number, he also wants to capture Street, City, State and Zip. None of these require functionality yet, so we'll just add fields for each one in the Account.cs class and rerun our tests. All green, so one story off the list.

Next to add customers. In talking with our customer, he wants a button on the screen that says Add Account. When you click it, it opens a window to capture the Account information, and when you click save it shows up in the list of accounts. From a backend it would also be stored in our data store. So our first test is to make sure we are capturing the add customer event:

[TestMethod]
public void PresenterRegistersAsListenerForAddCustomerRequestedEvent()
{
  IAccountView view = new StubAccountView();
  Assert.AreEqual(0, ((StubAccountView)view)
    .AddCustomerRequestedListenerCount);
  AccountPresenter presenter = new AccountPresenter(view);
  int expectedListenersCount = 1;
  int actualListenersCount = ((StubAccountView)view)
      .AddCustomerRequestedListenerCount;
  Assert.AreEqual(expectedListenersCount, actualListenersCount);
}

Our compiler complains that the AddCustomerRequestedListenerCount doesn't exist in our stub class, so we add the event to our interface and add the Count property to our stub:

public event AccountViewEventDelegate AddCustomerRequested;

public int AddCustomerRequestedListenerCount
{
  get
  {
    if (AddCustomerRequested != null)
    {
      return AddCustomerRequested.GetInvocationList().Count();
    }
    return 0;
  }
}

Our compiler is also complaining that the event doesn't exist in our MainForm. We'll go ahead and add it, but I feel a bit uneasy knowing that the error we see now is currently the only automated way we'll know about the MainForm implementing the interface events. In other words, by adding the event to MainForm, we know it declares it, but we don't currently have tests that show it actually hooks it up correctly. Let's mark that and revisit it shortly.

Ok, so with our compiler errors resolved, we run our tests, and our test fails like we expected. So let's modify our presenter to fix this:

public AccountPresenter(IAccountView view)
{
  view.AccountViewLoad +=
    new AccountViewEventDelegate(view_AccountViewLoad);
  view.SearchCustomersRequested += 
    new AccountViewEventDelegate   
      (view_SearchCustomersRequested);
  view.AddCustomerRequested += new AccountViewEventDelegate
    (view_AddCustomerRequested);
}

void view_AddCustomerRequested(
  object sender, IAccountView view)
{
}

Ok, green test. But what should happen when that event gets fired?

 

Let's open some windows

According to our customer, a new window should open up with the customer details. So where should the responsibility for opening a new window lie? The way I see it, the presenter has the responsibility for determining a new window needs to be opened, but it is up to the view to figure out what that means from a UI context. Following this thought, the next test should be:

[TestMethod]
public void PresenterRequestsNewWindowFromViewWhenAddCustomerRequested()
{
  IAccountView view = new StubAccountView();
  AccountPresenter presenter = new AccountPresenter(view);
  ((StubAccountView)view).FireAddCustomerRequestedEvent();
  Assert.IsTrue(
    ((StubAccountView)view).NewCustomerWindowRequested);
}

In our stub, we'll define a stub property called NewCustomerWindowRequested, and implement the OpenCustomerWindow method to set this property to be true in the stub:

public interface IAccountView
{
  //...
  void OpenCustomerWindow();
}

class StubAccountView
{
  //...
  private bool newCustomerWindowRequested = false;

  public bool NewCustomerWindowRequested
  {
    get { return newCustomerWindowRequested; }
    set { newCustomerWindowRequested = value; }
  }
  public void OpenCustomerWindow()
  { 
    newCustomerWindowRequested = true;
  }

  public void FireAddCustomerRequestedEvent()
  {
    if (AddCustomerRequested != null)
      AddCustomerRequested(this, this);
  }
}

Again, we are now left with the compiler error that our MainForm doesn't implement the OpenCustomerWindow method. We'll stick it in there with an empty body, but we still need to revisit this.

Ok, now our test is failing. We need to modify our Presenter:

void view_AddCustomerRequested(object sender, IAccountView view)
{
  view.OpenCustomerWindow();
}

Which passes our test, but leaves us with a question. How should the Customer Window communicate the new Account to us? And why are we using Customer here when we called it Account before?

 

Account? Or AstroCustomer?

Let's fix the latter first. We change our event to AddAccountRequested and the method to OpenAccountWindow, and then lean on the compiler to clean up the other places we used Customer. All better now.

 

Pull forward to the first window

Ok, back to the communication question. The view is responsible for opening the new window, but it shouldn't be concerned with what to do when that new window is opened or finished. The simplest thing would be to have the current form pass a reference to the presenter to the new form, and as we build it, we can write tests to drive it to call back to the presenter when it is done (which will update the main view). Not knowing if that will or will not reak havoc later down the road, we modify IAccountView to turn OpenAccountWindow() to OpenAccountWindow(AccountPresenter presenter). This means that instead of just checking that the method was called with the bool in our stub, we can make sure the presenter is passing itself in. In fact, when we made that change, our compiler told us that OpenAccountWindow() in our presenter was broken. Without thinking, I changed it to pass in (this), but I've gone back and changed it to null until we get a test in place that drives the need for it. That test looks like:

[TestMethod]
public void PresenterRequestsNewWindowWithItselfWhenAddAccountRequested()
{
  IAccountView view = new StubAccountView();
  AccountPresenter presenter = new AccountPresenter(view);
  ((StubAccountView)view).FireAddAccountRequestedEvent();
  Assert.AreEqual(presenter, ((StubAccountView)view)
    .PresenterForOpenAccountWindow);
}

And we change our stub to look like:

private AccountPresenter presenterForOpenAccountWindow;

public AccountPresenter PresenterForOpenAccountWindow
{
  get { return presenterForOpenAccountWindow; }
  set { presenterForOpenAccountWindow = value; }
}

public void OpenAccountWindow(AccountPresenter presenter)
{
  presenterForOpenAccountWindow = presenter;
  newCustomerWindowRequested = true;
}

Our test fails that Presenter was null, so we modify AccountPresenter to pass itself:

void view_AddAccountRequested(object sender, IAccountView view)
{
  view.OpenAccountWindow(this);
}

Which passes our test. With this test in place, we can remove the newCustomerWindowRequested test.

So let's get a checkpoint here. Right now we know that our interface exposes an event for users to request an account to be added, and when that event fires, our presenter calls a method on our view to open a new window with a reference to itself. We also know that we've been adding these things to our MainForm, but we don't have an automated way to make sure that everything happens as it should. Since the next steps would be to move to implementing the UI, let's see how we can do that in a Test-Driven fashion.

 

TDDing the WinForm (or, what the heck have we been doing?)

First, we want to make sure that we are wiring up everything correctly. Since the view is nothing more than a wrapper for the presenter to work on, the only logic it has is to map the domain events to UI events. It seems like this would be something that would be easy enough to test, so let's see what we can do. We create a new Test project called ServiceTracker tests and create a Test class in it called MainFormTests.cs. The first thing we want to verify is that the domain event AccountViewLoad is called when MainForm's Load event is fired. To do that, we'll subclass MainForm (so we can call OnLoad) and switch in a subclassed Presenter to see if the correct method is getting called. It all looks like this:

[TestMethod]
public void MainFormCallsAccountViewLoadOnLoad()
{
  StubMainForm stubForm = new StubMainForm();
  StubAccountPresenter presenter =
    new StubAccountPresenter(stubForm);
  stubForm.SetPresenter(presenter);
  stubForm.FireLoadEvent();
  Assert.IsTrue(presenter.AccountViewLoadEventFired);
}

And our stubs which live in our MainFormTests.cs class:

class StubAccountPresenter : AccountPresenter
{
  private IAccountView view;
  public bool AccountViewLoadEventFired = false;

  public StubAccountPresenter(IAccountView view)
    : base(view)
  {
    this.view = view;
    view.AccountViewLoad +=
      new AccountViewEventDelegate(view_AccountViewLoad);
  }

  void view_AccountViewLoad(object sender, IAccountView view)
  {
    AccountViewLoadEventFired = true;
  }
}

class StubMainForm : MainForm
{
  public void SetPresenter(AccountPresenter presenter)
  {
    base.presenter = presenter;
  }

  public void FireLoadEvent()
  {
    this.OnLoad(null);
  }
}

Running our test passes it (because MainForm actually does do the right thing), so just to make sure it isn't a fluke, I go into MainForm and comment out the call to the domain event. Running our test fails, so we are on the right track. I uncomment the event call, and we have a green test again.

With this in place, we can go ahead and get tests around the other calls our MainForm does - SearchCustomersRequested and our two new ones AddAccountRequested and OpenAccountWindow. SearchCustomersRequested looks like:

[TestMethod]
public void MainFormCallsSearchCustomersRequestedOnClick()
{
  StubMainForm stubForm = new StubMainForm();
  StubAccountPresenter presenter =
    new StubAccountPresenter(stubForm);
  stubForm.SetPresenter(presenter);
  stubForm.FireSearchClickEvent();
  Assert.IsTrue(presenter.SearchCustomersRequestedEventFired);
}

In StubAccountPresenter we add the field and handle the SearchCustomersRequested event by setting the field to true. However, we run into a problem in StubMainForm. Our FireSearchClickEvent method looks like:

public void FireSearchClickEvent()
{
  base.SubmitSearch.OnClick();
}

But the compiler isn't happy because it can't find SubmitSearch - even though that's the correct control name according to the designer. The problem is that the definition of SubmitSearch doesn't happen in MainForm.cs - it's a partial class. It happens in MainForm.Designer.cs, which is also a partial class. I try adding the definition to MainForm.cs, which gives me the following amusing compiler error list:

Then it hits me. SubmitSearch is there - as a private member. So I remove the definition from MainForm, and change the one in MainForm.Designer.cs to be protected, and that gets me past the above error, but to an error that says OnClick can't be called from Button. After some digging, that's because to cause the click event on a button, you call PerformClick(). So now our StubMainForm has:

public void FireSearchClickEvent()
{
  SubmitSearch.PerformClick();
}

And the compiler is happy.

 

Show me the goods!

But are our tests? They aren't. Stepping through the code with the debugger, we are calling PerformClick, but that isn't causing our event to fire in MainForm. So we try something a little drastic:

[TestMethod]
public void MainFormCallsSearchCustomersRequestedOnClick()
{
  StubMainForm stubForm = new StubMainForm();
  StubAccountPresenter presenter =
    new StubAccountPresenter(stubForm);
  stubForm.SetPresenter(presenter);
  stubForm.Show();
  stubForm.FireSearchClickEvent();
  Assert.IsTrue(presenter.SearchCustomersRequestedEventFired);
}

This causes our test to pass, meaning the right magic happens when we call Show. But calling Show also causes the window to actually pop up - not what we want. Unfortunately I can't figure out a way to not do this - Show()ing the window then immediately Hide()ing it still causes the event not to fire. We'll leave it be for now.

So now all we have left is to use TDD to implement our AddAccountRequested event and OpenAccountWindow method. AddAccountRequested should come when the user clicks the Add Account button, so our test will be similar to the last one:

[TestMethod]
public void MainFormCallsAddAccountRequestedOnClick()
{
  StubMainForm stubForm = new StubMainForm();
  StubAccountPresenter presenter =
    new StubAccountPresenter(stubForm);
  stubForm.SetPresenter(presenter);
  stubForm.Show();
  stubForm.FireAddAccountClickEvent();
  Assert.IsTrue(presenter.AddCustomerRequestedEventFired);
}

class StubAccountPresenter : AccountPresenter
{
  //...
  public bool AddCustomerRequestedEventFired = false;

  public StubAccountPresenter(IAccountView view)
    : base(view)
  {
    //...
    view.AddAccountRequested += 
      new AccountViewEventDelegate
        (view_AddAccountRequested);
  }

  void view_AddAccountRequested(
    object sender, IAccountView view)
  {
    AddCustomerRequestedEventFired = true;
  }
}

To add the method to our StubView, we need the button to actually be added to the form. So we pop open the form in the designer and add our Add Account button:

We also go into the properties when we add the button and set the modifiers to be protected:

Now we should be able to finish out our stub method:

class StubForm : MainForm
{
  //...
  public void FireAddAccountClickEvent()
  {
    base.AddAccount.PerformClick();
  }
}

Which compiles, and gives us a failing test. To make it green, we need to modify our MainForm.cs event handler:

private void AddAccount_Click(object sender, EventArgs e)
{
  if (AddAccountRequested != null)
    AddAccountRequested(this, this);
}

And we have a green test! I was a little concerned having two tests running Show(), but running all the tests gives us all green. Good to go!

 

Opening the right window

So let's knock out our last test - the OpenAccountWindow method. This method should create a new AddAccountForm.cs instance and call the ShowDialog() method on it. Our first stab at the test looks like:

[TestMethod]
public void OpenAccountWindowOpensRightWindowUsingShowDialog()
{
  StubMainForm stubForm = new StubMainForm();
  StubAccountPresenter presenter =
    new StubAccountPresenter(stubForm);
  stubForm.SetPresenter(presenter);
  stubForm.Show();
  stubForm.FireAddAccountClickEvent();
  Assert.IsTrue(stubForm.AddAccountFormRequested);
  Assert.IsTrue(
    stubForm.AddAccountFormOpenedWithShowDialog);
}

The thought being that MainForm.cs will request the correct window via a method we can override in our subclass with a sensing object to make sure ShowDialog was called. So first we'll create our production AddAccountForm.cs so we have the right class to override, but we won't put anything in it yet. Next, we'll create a stub class in our MainFormTests.cs which subclasses AddAccountForm:

class StubAddAccountForm : AddAccountForm
{
  public bool ShowDialogCalled = false;

  public new System.Windows.Forms.DialogResult ShowDialog()
  {
    ShowDialogCalled = true;
    return System.Windows.Forms.DialogResult.OK;
  }
}

Now we modify our StubMainForm to setup how our MainForm should act:

private StubAddAccountForm stubAddAccountForm;

public AddAccountForm AddAccountForm
{
  get
  {
    AddAccountFormRequested = true;
    stubAddAccountForm = new StubAddAccountForm();
    return stubAddAccountForm;
  }
}

public bool AddAccountFormOpenedWithShowDialog
{
  get { return stubAddAccountForm.ShowDialogCalled; }
}

So we give back a StubAddAccountForm which can then tell us if ShowDialog was called. Running our tests shows we have a red test that AddAccountForm was not requested. So let's fix that in MainForm.cs:

public void OpenAccountWindow(AccountPresenter presenter)
{
  AddAccountForm addAccountForm = this.AddAccountForm;
}

protected AddAccountForm AddAccountForm
{
  get { return new AddAccountForm(); }
}

However, our test still fails that AddAccountForm wasn't called. That's because we need to modify the property in MainForm.cs to mark it virtual, and then mark the property in StubMainForm to override it. There, now our test is failing that ShowDialog wasn't called. We can fix that by doing:

public void OpenAccountWindow(AccountPresenter presenter)
{
  this.AddAccountForm.ShowDialog();
}

Running the test shows it appearing to hang. However, that's because it had opened up the form and shown the AddAccountForm.cs form, and was waiting on us to do something. Not what we expected. So the problem is that while we are returning StubAddAccountForm.cs, ShowDialog, which we were hiding by using new, is getting called on the base class. We scrolled through the events hoping to find a BeforeShow, but the closes we get is Shown, so we modify our StubAddAccountForm to:

class StubAddAccountForm : AddAccountForm
{
  public bool ShowDialogCalled = false;

  public StubAddAccountForm()
    : base()
  {
    this.Shown +=
      new EventHandler(StubAddAccountForm_Shown);
  }

  void StubAddAccountForm_Shown(object sender, EventArgs e)
  {
    ShowDialogCalled = this.Modal;
    this.Close();
  }
}

We listen for the Shown event, and when it happens, we see if we are Modal or not. Since we can only be Modal if ShowDialog was called, this should be fine. Then we immediately Close ourselves. And sure enough, we have a green test!

However, we have one more test. The last test shows that our MainForm calls the AddAccountForm property correctly, and acts on the object returned appropriately. But we aren't guaranteeing that the AddAccountForm property is correct in MainForm.cs. So let's fix that:

[TestMethod]
public void AddAccountFormPropertyReturnsCorrectClass()
{
  MainForm form = new MainForm();
  AddAccountForm addAccountForm =
    form.AddAccountForm as AddAccountForm;
  Assert.IsNotNull(addAccountForm);
}

Which passes. However, it brings up a good point. For our MainForm, we've been working primarily with IAccountViews - shouldn't we be doing the same for our AddAccountForm? Let's fix that by creating an IAddAccountView and having AddAccountForm implement that:

public partial class AddAccountForm : Form, IAddAccountView

Ok, all green. IAddAccountView is just an empty interface at this point, so I didn't expect anything to break. Now let's clean up. To start, our AddAccountForm property is in StubMainForm and MainForm, but not in our interface. So in our IAccountView interface we add the following:

IAddAccountView AddAccountForm { get; }

Our compiler now complains, so we fix the property in our MainForm and StubMainForm. Now we get a complaint that IAddAccountView doesn't have a ShowDialog method from the call in MainForm. We could add the method to our interface, but then we'd have to pull the System.Windows.Forms dll into that project since ShowDialog returns a DialogResult. Instead, we'll just cast it in MainForm:

public void OpenAccountWindow(AccountPresenter presenter)
{
  ((AddAccountForm)this.AddAccountForm).ShowDialog();
}

And all of our tests are green! We don't have the data being saved, but this seems like a good stopping point to look over what we've done. We'll finish up the rest of Account Data next time.