"An ASP.NET website has a button_click event handler which does the following:
a) calls to a legacy COM+ object
b) calls to a 3rd party licensed DLL
c) calls to a web service
d) calls some ADO.NET code to save to a database
The method is approximately 100 lines of code and is, in its current form, untestable."
How might you break this down, with some minimal refactoring but without actually changing any of the functionality of the 4 actions within the event handler, to make it testable?
Here is one possible approach:
Begin with Refactoring
1. Do Extract Method refactoring on the event handler to move the code into a seperate method.
2. Use Extract Class refactoring to move this method to a separate class.
3. For testability, this logic needs to be in a separate class library, not in the context of an ASP.NET website, so do the following:
a) create a new project in your solution, of type class library, and name it (eg. Web.Support)
b) move the new class you have created into this project.
c) fix any compiler errors by referencing the new class library and then adding a using statement to the code-behind class.
4. In the new class library, Add References to resolve any compiler errors within the class.
5. Most typically, this will be a reference to System.Web (plus any custom or 3rd-party references your page was using).
NOTE For broken references to Session, use: System.Web.HttpContext.Current.Session5. Finally, within this new external method, use Extract Method refactoring to break out the 4 unique pieces of functionality into 4 public methods that can be called separately.
When you are done, you should have something like this:
public class MyExtractedClass
{
public void MyExtractedButtonClickEventHandler()
{
// various parameters declared here
CallToLegacyComObject(); // each metod will have various parameters
CallToThirdPartyDLL();
CallToWebService();
CallToAdoNetDbSave();
}
}
You are now ready to begin creating your unit and integration tests against a coordinator class. This class will coordinate each of the pieces of above functionality, but in the form of interfaces. Each interface will represent a distinct part of the functionality that can be tested separately. Typically in an ASP.NET (WebForm) web site this coordination is achieved using Model-View-Presenter, where the Presenter is a class that will coordinate the various interfaces. These interfaces are made available to the presenter class by "injecting" each interface into the class as a parameter to the class constructor (hence the term: Dependency Injection.) In this case, you are going to have 5 interfaces injected into the presenter class: one for each unique piece of functionality, plus the view interface.
Creating the Unit Test
1. Create two new class library projects:
- Tests.Unit
- Tests.Integration
3. Class variables: you being by declaring repository interfaces for each of the functionality elements you need to test. Note that the names of these interfaces should not reflect the technology--for example, IComPlusObjectRepository is a bad name because at the interface level, one does not know whether COM+ will be used as an implementation. Instead, the names of the interfaces should reflect WHAT the functionality business logic does. Let's rewrite the above 4 method calls as pseudo-code, to define WHAT they actually do:
public class MyExtractedClass
{
public void MyExtractedButtonClickEventHandler()
{
// various parameters declared here
// 1. get gym membership fee structure
// 2. parse and write a PDF invoice
// 3. register details with national gym organization
// 4. save gym membership changes to db
}
}
4. Now that you have an idea of what they do, name your repository interfaces names such as:
- IGymMembershipFeeRepository
- IPdfInvoiceParserRepository
- INationalGymRegistrationRepository
- IGymMembershipRepository
6. In the Web.Support class library, you will now need new sub-namespaces for each of these interface or class types. To do this, create the following folders within the class library:
- Repository
- Presenter
- View
NOTE We create the unit test BEFORE the interfaces or classes exist, so we won't have Intellisense assistance as we type out these interface or class names as they do not exist yet. The compiler will flag these in Visual Studio by marking them in red. You can then implement these classes in multiple ways:
- manually
- make use of the Generate by Usage feature in Visual Studio 2010
- install a tool like Resharper to enable you to generate these classes more quickly
[TestFixture]
public class GymMembershipPresenterTests
{
private MockRepository _mockRepository;
private IGymMembershipFeeRepository _gymMembershipFeeRepository;
private IPdfInvoiceParserRepository _pdfInvoiceParserRepository;
private INationalGymRegistrationRepository _nationalGymRegistrationRepository;
private IGymMembershipRepository _gymMembershipRepository;
private IGymMembershipView _startTransactionView;
[SetUp]
public void SetUp()
{
_mockRepository = new MockRepository();
_gymMembershipFeeRepository = _mockRepository.StrictMock<IGymMembershipFeeRepository>();
_pdfInvoiceParserRepository = _mockRepository.StrictMock<IPdfInvoiceParserRepository>();
_nationalGymRegistrationRepository = _mockRepository.StrictMock<INationalGymRegistrationRepository>();
_gymMembershipRepository = _mockRepository.StrictMock<IGymMembershipRepository>();
_gymMembershipView = _mockRepository.StrictMock<IGymMembershipView>();
}
[TearDown]
public void TearDown()
{
_mockRepository.VerifyAll();
}
[Test]
public void Constructor_FiveRepositoryInputs_ConfiguresGymMembershipAndReturnsMessage()
{
const string name = "Sally Wong";
const decimal amount = 35.00M;
GymMembership gymMembership = new GymMembership { Name = name, Amount = amount };
Expect.Call(_gymMembershipFeeRepository.CreateMembershipFee(name)).Return(gymMembership);
InvoicePdf invoice = new InvoicePdf { GymMembership = gymMembership };
Expect.Call(pdfInvoiceParserRepository.CreatePdf(gymMembership)).Return(invoice);
NationalGymInfo nationalGymInfo = new NationalGymInfo { ResponseCode = "<out>some expected xml</out>" };
Expect.Call(nationalGymRegistrationRepository.RegisterDetails(gymMembership.Name, gymMembership.Amount)).Return(nationalGymInfo);
var gymMembership = new GymMembership
{
Name = name,
LockerNumber = 352,
Amount = amount,
NationalGymInfo = nationalGymInfo
};
_gymMembershipRepository.Save(gymMembership);
_gymMembershipView.Message = "Your membership has been processed.";
_mockRepository.ReplayAll();
var sut = new GymMembershipPresenter(_gymMembershipFeeRepository,
_pdfInvoiceParserRepository,
_nationalGymRegistrationRepository
_gymMembershipRepository,
_startTransactionView);
sut.CreateNewGymMembership(name, amount);
}
}
Explanation of the Unit Test with Mocks
For a complete explanation of mock objects, there are many good resources on the web. However, the MAIN purpose of a unit test that uses mock objects is for DESIGN. As you create this unit test, you are designing (experimenting with) possible interactions in the presenter class, using mock versions of the interfaces to speculate what those interface implementations might do, and what values they might return back.
Typically the unit test consists of the specifying mock behaviors (using the Expect() method of Rhino.Mocks for methods or properties that have a return type), following by the ReplayAll() command, followed by the actual presenter instantiation and method call (see above code snippet for exact details.) The [TearDown] method then calls the VerifyAll() command to validate the specification.
Once you start to run the unit tests, the mock object framework will validate the specification and point out where the REAL presenter class doesn't yet match the specification you have created with your mocks. This becomes a trial and error process where you keep adding code to the presenter until all of the expectations which you have set up in your unit test have been satisfied by the presenter.
Why is this important? Why go to all this work to specify the interface interactions within the presenter? Because one you have established a working unit test, you are now in a position to create integration tests where each element can be tested independently.
Integration Tests
You can now create an integration test that ONLY tests the behavior of the COM+ object; the rest of the interfaces can be implemented with a "fake" class whose only purpose is to pretend to succeed. This allows you to focus each integration test on the real behavior of a single interaction.
8. Add a new integration test class to the Tests.Integration class library, named GymMembershipPresenterTests.cs (it will show up under the integration test library so the name can be the same, or different.)
9. Note the code below. For integration tests, we don't have to use a mocking framework. Instead, we create fake classes, whose only purpose is to succeed happily. The first integration test will simply pass by calling all fake classes.
[TestFixture]
public class GymMembershipPresenterTests
{
private IGymMembershipFeeRepository _gymMembershipFeeRepository;
private IPdfInvoiceParserRepository _pdfInvoiceParserRepository;
private INationalGymRegistrationRepository _nationalGymRegistrationRepository;
private IGymMembershipRepository _gymMembershipRepository;
private IGymMembershipView _startTransactionView;
[SetUp]
public void SetUp()
{
_gymMembershipFeeRepository = FakeGymMembershipFeeRepository();
_pdfInvoiceParserRepository = FakePdfInvoiceParserRepository();
_nationalGymRegistrationRepository = FakeNationalGymRegistrationRepository();
_gymMembershipRepository = FakeGymMemberhipsRepository();
_gymMembershipView = FakeGymMembershipView();
}
[Test]
public void Constructor_AllFakeRepositoryInputs_ConfiguresGymMembershipAndReturnsMessage()
{
const string name = "Sally Wong";
const decimal amount = 35.00M;
var sut = new GymMembershipPresenter(_gymMembershipFeeRepository,
_pdfInvoiceParserRepository,
_nationalGymRegistrationRepository
_gymMembershipRepository,
_startTransactionView);
sut.CreateNewGymMembership(name, amount);
Assert.AreEqual("Your membership has been processed.", _gymMembershipView.Message);
}
}
10. The SECOND integration test will test ONLY the interaction with the COM+ object. It does this by replacing one of the fake interface implementations with a real interface implementation, in this case, ComPlusCallerGymMembershipFeeRepository class.
[Test]
public void Constructor_RealComPlusGymMembershipFeeAndFakeRepositoryInputs_ConfiguresGymMembershipAndReturnsMessage()
{
const string name = "Sally Wong";
const decimal amount = 35.00M;
_gymMembershipFeeRepository = new ComPlusCallerGymMembershipFeeRepository();
var sut = new GymMembershipPresenter(_gymMembershipFeeRepository,
_pdfInvoiceParserRepository,
_nationalGymRegistrationRepository
_gymMembershipRepository,
_startTransactionView);
sut.CreateNewGymMembership(name, amount);
Assert.AreEqual("Your membership has been processed.", _gymMembershipView.Message);
}
11. What goes into the class ComPlusCallerGymMembershipFeeRepository.cs? A call to the ORIGINAL logic which you worked so hard to extract out into an independent class and an independent method:
public class ComPlusCallerGymMembershipFeeRepository : IParkingLotRepository
{
public GymMembership CreateMembershipFee(string name)
{
var myExtractedClass = new MyExtractedClass();
double amountCharged = myExtractedClass.CallToLegacyComObject(name);
var gymMembership = new GymMembership { Name = name, Amount = amountCharged };
return gymMembership;
}
}
From here, you can proceed to create 3 more integration tests (with 3 more real implementation classes that call the original logic in the extracted class.) Each integration test will call only one real implementation, and the rest as fakes, allowing you to independently verify the behavior of each, separate action:
- against the COM+ legacy object
- against the 3rd party DLL
- against the web service
- against the ADONET db layer.
So let's review: you STARTED with a button_click event handler containing long procedural code that did 4 completely distinct actions, which you could not test.
You have ENDED with 4 decoupled interfaces, each representing only one of the actions, and you have the ability to test them independently by implementing each interface, either as mocks in a unit test (to check that your presenter coordinates correctly), as fakes in an integration test (to create pretend success classes for stuff you don't currently care about) or as real implementation classes in an integration test (which actually tests a single, specific action against the original functionality of your legacy code.)
NOTE As a final step, you would implement the IGymMembershipView interface on your original ASPX page, and have the button_click event either call directly to the method GymMembershpPresenter.CreateNewGymMembership(name, amount), or via a View event's event handler implementation within the presenter. This is an important final refactoring, so that both your integration tests, and your presentation layer, would be calling the same (tested) code.
No comments:
Post a Comment