Anti-Pattern 5: Fat UI's

by geoffrey 27. May 2010 11:52

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.

 

kick it on DotNetKicks.com

Tags: , ,

Unit Testing

blog comments powered by Disqus

About the author

Geoffrey Vandiest

contact

View Geoffrey Vandiest's profile on LinkedIn

Follow me on twitter

Geoffrey Vandiest is a technical fellow who learned the art of programming at the age of 10 on a Philips MSX computer. He's skilled in the architecture and development on the Microsoft platform and started experimenting with the Microsoft .Net framework as from the Beta 1 in 2001. Since nearly a decade Geoffrey coaches development teams and base his management style on Agile principles.

 

Month List