<= 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 interfa
ce 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.
Wow, that’s a lot of work!
How about using NUnitForms? You can write a test verifying that clicking a button opens a window, then filling a form and closing the window makes the record appear in the main form. Quickly make it pass, and then refactor it to the MVP pattern or whatever you find appropriate, adding tests as necessary. So, after you are sure it works, you have plenty of time to think about responsibilities and such, and if you don’t add too many tests, you’ll be able to redesign later.
In addition, NUnitForms makes it very simple to fire UI events. You can avoid subclassing your form, for example, and other hacks.
One reason why I usually avoid your approach is that you have to keep all your pieces in your head at once. Note that several times you have to make a mental note in order to return to it later. This way, if you forgot something, all your tests are green, but the thing can’t do anything useful. But that’s probably me, I don’t like making design decisions before coding.