Testing the User Interface - Using DotNetMock

Testing the User Interface - Using DotNetMock

When you start to write the user interface for your application, a number of different problems arise. Although you can create user interface classes that are loosely coupled with respect to other classes, a user interface class is by definition highly coupled to the user! So how can we create a automated unit test to test this? The answer is that we separate the logic of our user interface from the actual presentation of the view. Various patterns exist in the literature under a variety of different names: Model-View-Controller, Model-View-Presenter, Doc-View, etc. The creators of these patterns recognized that decoupling the logic of what the view does (i.e., controller) from the view is a Good Thing . So how do we use this? The technique I use comes from Michael Feathers' paper The Humble Dialog Box. The idea is to make the view class support a simple interface used for getting and setting the values displayed by the view. There is basically no code in the view except for code related to the painting of the screen. The event handlers in the view for each interactive user interface element (e.g., a button) contain nothing more than a pass-thru to a method in the controller. The best way to illustrate this concept is with an example. Assume our application needs a screen that asks the user for their name and social security number. Both fields are required, so we need to make sure that a name is entered and the SSN has the correct format. Since we are writing our unit tests first , we write the following test:

[TestFixture]
public class VitalsControllerTests
{
[Test]
public void TestSuccessful()
{
MockVitalsView view = new MockVitalsView();
VitalsController controller =
new VitalsController(view);

view.Name = "Peter Provost";
view.SSN = "123-45-6789";

Assertion.Assert( controller.OnOk() == true );
}

[Test]
public void TestFailed()
{
MockVitalsView view = new MockVitalsView();
VitalsController controller =
new VitalsController(view);

view.Name = "";
view.SSN = "123-45-6789";
view.SetExpectedErrorMessage(
controller.ERROR_MESSAGE_BAD_NAME );
Assertion.Assert( controller.OnOk() == false );
view.Verify();

view.Name = "Peter Provost";
view.SSN = "";
view.SetExpectedErrorMessage(
controller.ERROR_MESSAGE_BAD_SSN );
Assertion.Assert( controller.OnOk() == false );
view.Verify()
}
}

When we build this we receive a lot of compiler errors because we don't have either a MockVitalsView or a VitalsController . So let's write skeletons of those classes. Remember, we only want to write enough to make this code compile.

public class MockVitalsView
{
public string Name
{
get { return null; }
set { }
}

public string SSN
{
get { return null; }
set { }
}

public void SetExpectedErrorMessage( string message )
{
}

public void Verify()
{
throw new NotImplementedException();
}
}

public class VitalsController
{
public const string ERROR_MESSAGE_BAD_SSN
= "Bad SSN.";
public const string ERROR_MESSAGE_BAD_NAME
= "Bad name.";

public VitalsController( MockVitalsView view )
{
}

public bool OnOk()
{
return false;
}
}

Now our test assembly compiles and when we run the tests, the test runner reports two failures. The first occurs when TestSuccessful calls controller.OnOk , because the result is false rather than the expected true value. The second failure occurs when TestFailed calls view.Verify . Continuing on with our test-first paradigm, we now need to make these tests pass. It is relatively simple to make TestSuccessful pass, but to make TestFailed pass, we actually have to write some real code, such as:

public class MockVitalsView : MockObject
{
public string Name
{
get { return _name; }
set { _name = value; }
}

public string SSN
{
get { return _ssn; }
set { _ssn = value; }
}

public string ErrorMessage
{
get { return _expectedErrorMessage.Actual; }
set { _expectedErrorMessage.Actual = value; }
}

public void SetExpectedErrorMessage( string message )
{
_expectedErrorMessage.Expected = message;
}

private string _name;
private string _ssn;
private ExpectationString _expectedErrorMessage =
new ExpectationString("expected error message");
}

public class VitalsController
{
public const string ERROR_MESSAGE_BAD_SSN = "Bad SSN.";
public const string ERROR_MESSAGE_BAD_NAME = "Bad name.";

public VitalsController( MockVitalsView view )
{
_view = view;
}

public bool OnOk()
{
if( IsValidName() == false )
{
_view.ErrorMessage = ERROR_MESSAGE_BAD_NAME;
return false;
}

if( IsValidSSN() == false )
{
_view.ErrorMessage = ERROR_MESSAGE_BAD_SSN;
return false;
}

// All is well, do something...
return true;
}

private bool IsValidName()
{
return _view.Name.Length > 0;
}

private bool IsValidSSN()
{
string pattern = @"^/d{3}-/d{2}-/d{4}___FCKpd___2quot;;
return Regex.IsMatch( _view.SSN, pattern );
}

private MockVitalsView _view;
}

Let's briefly review this code before proceeding. The first thing to notice is that we haven't changed the tests at all (which is why I didn't even bother to show them). We did, however, make significant changes to both MockVitalsView and VitalsController . Let's begin with the MockVitalsView . In our previous example, MockVitalsView didn't derive from any base class. To make our lives easier, we changed it to derive from DotNetMock.MockObject . The MockObject class gives us a stock implementation of Verify that does all the work for us. It does this by using expectation classes through which we indicate what we expect to happen to our mock object. In this case our tests are expecting specific values for the ErrorMessage property. This property is a string, so we add an ExpectationString member to our mock object. Then we implement the SetExpectedErrorMessage method and the ErrorMessage property to use this object. When we call Verify in our test code, the MockObject base class will check this expectation and identify anything that doesn't happen as expected. Pretty cool, eh? The other class that changed was our VitalsController class. Because this is where all the working code resides, we expected there to be quite a few changes here. Basically, we implemented the core logic of the view in the OnOk method. We use the accessor methods defined in the view to read the input values, and if an error occurs, we use the ErrorMessage property to write out an appropriate message. So we're done, right? Not quite. At this point, all we have is a working test of a controller using a mock view. We don't have anything to show the customer! What we need to do is use this controller with a real implementation of a view. How do we do that? The first thing we need to do is extract an interface from MockVitalsView . A quick look at VitalsController and VitalsControllerTests shows us that the following interface will work.

public interface IVitalsView
{
string Name { get; set; }
string SSN { get; set; }
string ErrorMessage { get; set; }
}

After creating the new interface, we change all references to MockVitalsView to IVitalsView in the controller and we add IVitalsView to the inheritance chain of MockVitalsView . And, of course, after performing this refactoring job we run our tests again. Assuming everything is fine, we can create our new view. For this example I will be creating an ASP.NET page to act as the view, but you could just as easily create a Windows Form. Here is the .ASPX file:

<%@ Page language="c#" Codebehind="VitalsView.aspx.cs" 
  AutoEventWireup="false" 
  Inherits="UnitTestingExamples.VitalsView" %>
<!DOCTYPE HTML="GENERATOR" Content="Microsoft Visual Studio 7.0">
    <meta name="CODE_LANGUAGE" Content="C#">
    <meta name=vs_defaultClientScript content="JavaScript">
    <meta name=vs_targetSchema content="http://schemas.microsoft.com/intellisense/ie5">
  </head>
  <body MS_POSITIONING="GridLayout">
    <form id="VitalsView" method="post" runat="server">
      <table border="0">
        <tr>
          <td>Name:</td>
          <td><asp:Textbox runat=server id=nameTextbox /></td>
        </tr>
        <tr>
          <td>SSN:</td>
          <td><asp:Textbox runat=server id=ssnTextbox /></td>
        </tr>
        <tr>
          <td> </td>
          <td><asp:Label runat=server id=errorMessageLabel /></td>
        </tr>
        <tr>
          <td> </td>
          <td><asp:Button runat=server id=okButton Text="OK" /></td>
        </tr>
      </table>
    </form>
  </body>
</html>

And here is the code-behind file:

using System;
using System.Web.UI.WebControls;
using UnitTestingExamples.Library;

namespace UnitTestingExamples
{
/// <summary>
/// Summary description for VitalsView.
/// </summary>
public class VitalsView : System.Web.UI.Page, IVitalsView
{
protected TextBox nameTextbox;
protected TextBox ssnTextbox;
protected Label errorMessageLabel;
protected Button okButton;

private VitalsController _controller;

private void Page_Load(object sender, System.EventArgs e)
{
_controller = new VitalsController(this);
}

private void OkButton_Click( object sender, System.EventArgs e )
{
if( _controller.OnOk() == true )
Response.Redirect("ThankYou.aspx");
}

#region IVitalsView Implementation

public string Name
{
get { return nameTextbox.Text; }
set { nameTextbox.Text = value; }
}

public string SSN
{
get { return ssnTextbox.Text; }
set { ssnTextbox.Text = value; }
}

public string ErrorMessage
{
get { return errorMessageLabel.Text; }
set { errorMessageLabel.Text = value; }
}

#endregion

#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
okButton.Click += new System.EventHandler( this.OkButton_Click );
}
#endregion
}
}

As you can see, the only code in the view is code that ties the IVitalsView interface to the ASP.NET Web Controls and a couple of lines to create the controller and call its methods. Views like this are easy to implement. Also, because all of the real code is in the controller, we can feel confident that we are rigorously testing our code.

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Also create a ControllerCreate class that extends Controller.The create method takes as arguments the name of a new library user, a number of books (as a string), and an integer representing the role of user to create (where the integer 0 means a lender and the integer 1 means a borrower). The create method of the controller then transforms the book number from a string to an integer (using the Integer.parseInt static method), creates an object from the correct class (based on the role specified by the user input: lender or borrower) and calls the addUser method of the library to add the new user object to the library. • If no exception occurs then the create method of the controller returns the empty string. • If the constructor of the Borrower class throws a NotALenderException then the create method of the controller must catch this exception and return as result the error message from the exception object. • If the parseInt method of the Integer class throws a NumberFormatException (because the user typed something which is not an integer) then the create method of the controller must catch this exception and return as result the error message from the exception object. Modify the run method of the GUI class to add a ViewCreate view that uses a ControllerCreate controller and the same model as before (not a new model!) Do not delete the previous views. Note: if at the end of Question 7 you had manually added to your library (model object) some users for testing, then you must now remove those users from the run method of the anonymous class inside the GUI class. You do not need these test users anymore because you have now a graphical user interface to create new users! Run your GUI and check that you can correctly use the new view to create different users for your library, with different types of roles. • Check that, when you create a new user, the simple view is automatically correctly updated to show the new total number of books borrowed by all users. • Also use the “get book” view to check that the users are correctly created with the correct names and correct number of books. • Also check that trying to create a borrower with a negative number of books correctly shows an error message. Also check that trying to create a user with a number of books which is not an integer correctly shows an error message (do not worry about the content of the error message). After you created a new user, you can also check whether it is a lender or a borrower using the “more book” view to increase the number of books of the user by a big negative number: • if the new user you created is a lender, then increasing the number of books by a big negative value will work and the number of books borrowed by the user will just become a larger value (you can then check that using the “get book” view); • if the new user you created is a borrower, then increasing the number of books by a big negative value will fail with an error message and the number of books borrowed by the user will not change (you can then check that using the “get book” view). 完成符合以上要求的java代码
05-24

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值