One of the major flaws in an un-testable design is the very tight coupling between the UI and the actual domain and/or presentation logic. Our typical ASP.NET applications are difficult to test, because much of the logic is contained within the codebehind files, which derive from Web.UI.Page, which needs an HttpContext, which is difficult to mock. Furthermore, the output of the methods in the codebehind is often not easily-testable, because it's a side-effect (such as calling DataBind() on a GridView).
This same problem exists in windows forms: How do you test the logic inside the form when you are unable to control the input & output of the UI? Let’s examine the following code sample.
1: public partial class LoginForm : Form
2: {
3: const string AUTHNETICATION_FAILED = "Authentication failed!";
4: public LoginForm()
5: {
6: InitializeComponent();
7: }
8:
9: private void LoginButton_Click(object sender, EventArgs e)
10: {
11: var membershipService = new MembershipService();
12:
13: if (membershipService.Authenticate(
14: UserNameText.Text,
15: PasswordText.Text
16: )
17: )
18: {
19: MainForm form = new MainForm();
20: this.Hide();
21: form.Show();
22: }
23: else
24: {
25: MessageLabel.Text = AUTHNETICATION_FAILED;
26: }
27: }
28: }
Apart from being a pretty naïve implementation of a login form, this code is also hard to test. It’s design contains several smells of un-testable design. First of all it instantiates his Services directly, making it impossible to inject his dependencies through test doubles. Another problem is how can we test that; when the authentication was successful, the MainForm is shown?
By applying what we learned earlier we could already make our SUT more testable:
1: public partial class LoginForm : Form
2: {
3: const string AUTHENTICATION_FAILED = "Authentication failed!";
4: private IMembershipService _membershipService;
5: private INavigationService _navigationService;
6:
7: public LoginForm()
8: {
9: InitializeComponent();
10: _membershipService = new MembershipService();
11: _navigationService = new NavigationService(this);
12: }
13:
14: public LoginForm(
15: IMembershipService membershipService,
16: INavigationService navigationService
17: ) //This constructor is only for testing purposes
18: {
19: _membershipService = membershipService;
20: _navigationService = navigationService;
21: }
22:
23: public void LoginButton_Click(object sender, EventArgs e)
24: {
25: if (_membershipService.Authenticate(
26: UserNameText.Text,
27: PasswordText.Text)
28: )
29: {
30: _navigationService.NavigateTo(new MainForm());
31: }
32: else
33: {
34: MessageLabel.Text = AUTHENTICATION_FAILED;
35: }
36: }
37: }
38:
39: public interface INavigationService
40: {
41: void NavigateTo(Form focusForm);
42: }
43:
44: public class NavigationService : INavigationService
45: {
46: private Form _form;
47: public NavigationService(Form form)
48: {
49: _form = form;
50: }
51:
52: public void NavigateTo(Form focusForm)
53: {
54: _form.Hide();
55: focusForm.Show();
56: }
57: }
Here we encapsulated the Navigation logic into a Service and we’ve created a specific constructor that can be used by our tests to inject a test double making possible to write the following test:
1: [Test]
2: public void LoginButton_Click_OnSuccessfullLogin_ShouldNavigateToMainForm()
3: {
4: //Arrange
5: var mocks = new MockRepository();
6: var membershipServiceMock = mocks.StrictMock<IMembershipService>();
7: var navigationServiceMock = mocks.StrictMock<INavigationService>();
8: var form = new LoginForm(membershipServiceMock,navigationServiceMock);
9:
10: Expect.Call(membershipServiceMock.Authenticate(null, null)).Return(true);
11: navigationServiceMock.NavigateTo(null);
12: LastCall.IgnoreArguments();
13: mocks.ReplayAll();
14:
15: //Act
16: form.LoginButton_Click(null,null);
17:
18: //Assert
19: mocks.VerifyAll();
20: }
By applying one of the variants of the MVC pattern namely the PresentationModel we can completely decouple the presentation logic from infrastructure and test it in isolation:
1: public interface ILoginFormView
2: {
3: string UserName { get; set; }
4: string Password { get; set; }
5: string Message { get; set; }
6:
7: void NavigateTo(Form focusForm);
8: }
9:
10: public interface IMembershipService
11: {
12: bool Authenticate(string userName, string password);
13: }
14:
15: public class LoginFormPresenter
16: {
17: const string AuthneticationFailedMessage = "Authentication failed!";
18: private readonly ILoginFormView _view;
19: private readonly IMembershipService _membershipService;
20:
21: public LoginFormPresenter(ILoginFormView view, IMembershipService membershipService)
22: {
23: _view = view;
24: _membershipService = membershipService;
25: }
26:
27: public void LoginButtonClick()
28: {
29:
30: if (_membershipService.Authenticate(
31: _view.UserName,
32: _view.Password
33: )
34: )
35: {
36: _view.NavigateTo(new MainForm());
37: }
38: else
39: {
40: _view.Message = AuthneticationFailedMessage;
41: }
42: }
43:
44: }
45:
46: public partial class LoginForm : Form, ILoginFormView
47: {
48: private LoginFormPresenter _presenter;
49:
50: public string UserName
51: {
52: get
53: {
54: return UserNameTextBox.Text;
55: }
56: set
57: {
58: UserNameTextBox.Text = value;
59: }
60: }
61:
62: public string Password
63: {
64: get
65: {
66: return PasswordTextBox.Text;
67: }
68: set
69: {
70: PasswordTextBox.Text = value;
71: }
72: }
73:
74: public string Message
75: {
76: get
77: {
78: return MessageLabel.Text;
79: }
80: set
81: {
82: MessageLabel.Text = value;
83: }
84: }
85:
86: public LoginForm()
87: {
88: InitializeComponent();
89: _presenter = new LoginFormPresenter(this, new MembershipService());
90: }
91:
92: public void LoginButton_Click(object sender, EventArgs e)
93: {
94: _presenter.LoginButtonClick();
95: }
96:
97: public void NavigateTo(Form focussedForm)
98: {
99: Hide();
100: focussedForm.Show();
101: }
102: }
We are now able to test the presentation logic:
1: [TestFixture]
2: public class LoginFormPresenterTest
3: {
4: const string UserName = "myUserName";
5: const string Password = "myPassword";
6:
7: [Test]
8: public void LoginButtonClick_AuthenticationSuccess_ViewShouldNavigateToMainForm()
9: {
10: //Arrange
11: var viewMock = CreateLoginFormViewStub();
12: var membershipServiceMock = CreateMembershipServiceMock(true);
13: var subject = new LoginFormPresenter(viewMock, membershipServiceMock);
14:
15: //Act
16: subject.LoginButtonClick();
17:
18: //Assert
19: viewMock.AssertWasCalled(
20: s => s.NavigateTo(null),
21: options => options.IgnoreArguments()
22: );
23: }
24:
25: [Test]
26: public void LoginButtonClick_AuthenticationFailed_ViewMessageIsAuthenticationfailed()
27: {
28: //Arrange
29: var viewMock = CreateLoginFormViewStub();
30: var membershipServiceMock = CreateMembershipServiceMock(false);
31: var subject = new LoginFormPresenter(
32: viewMock,
33: membershipServiceMock
34: );
35:
36: //Act
37: subject.LoginButtonClick();
38:
39: //Assert
40: Assert.AreEqual("Authentication failed!", viewMock.Message);
41: }
42:
43: private IMembershipService CreateMembershipServiceMock(bool expectedResult)
44: {
45: var membershipServiceMock = MockRepository.GenerateStub<IMembershipService>();
46: membershipServiceMock.Stub(m => m.Authenticate(UserName, Password)).Return(expectedResult);
47: return membershipServiceMock;
48: }
49:
50: private ILoginFormView CreateLoginFormViewStub()
51: {
52: var viewMock = MockRepository.GenerateStub<ILoginFormView>();
53: viewMock.UserName = UserName;
54: viewMock.Password = Password;
55: return viewMock;
56: }
57: }
When designing UI’s you should make your choice between patterns like MVC/MVP/MVVM and use the most appropriate pattern and/or framework for your project. With the appropriate design a lot of the presentation logic encapsulated into the UI can be extracted and tested. In this example we extracted all the presentation logic encapsulated in our form and transferred it into a presenter. This presenter is completely decoupled from any infrastructure and we are able to easily obtain 100% test coverage with it. The LoginForm itself remains un-testable but it does not contain any logic anymore. His only purpose is to act as a sort of proxy between the windows forms infrastructure and our presentation logic.