Advertisement
Advertisement

More Related Content

Advertisement

Refactoring Legacy Web Forms for Test Automation

  1. REFACTORING LEGACY WEB FORMS FOR TEST AUTOMATION Author: Stephen Fuqua Last Revised: December 2014 All content by the author or from public domain sources unless otherwise noted
  2. PART 1 The Challenge
  3. Taking Over Legacy Code • Given you understand the value of test automation • Given you are handed a legacy application to maintain and enhance • Given the application is in ASP.Net Web Forms • When you try to add tests • Then you find that test-driven development is literally impossible.
  4. Web Forms Challenge • Testing ASP.Net Web Forms is problematic: • Tutorials show poor design, leading many developers to mix UI, business, and data access logic into a single class (the code behind). • ASP.Net functionality such as Session and ViewState are difficult to manipulate in an automated test. • Likewise, Web Forms architecture makes it difficult to access and manipulate form controls in tests. • Temptation: automate tests on the UI itself.
  5. Test Pyramid • In Succeeding with Agile, Mike Cohn describes a pyramid of automated tests: • Dominated by unit tests, and • Featuring service (system) tests that functionally integrate the units, and • Including just a few UI tests, to confirm that form fields connect to the services. UI Service Unit http://www.mountaingoatsoftware.com/blog/ the-forgotten-layer-of-the-test-automation- pyramid
  6. Right-siding the Pyramid • UI tests are brittle, expensive to write, and time consuming, to paraphrase Cohn. • With judicious refactoring, it is possible to continue using Web Forms and still achieve the goals of test automation and test-driven development • To overcome this challenge… • Use the Model-View-Presenter pattern, and • Introduce test isolation techniques.
  7. PART 2 Toolkit
  8. Model-View-Presenter • Model-View-Presenter, or MVP, is a specialized version of Model-View-Controller (MVC). • Split the traditional code behind into View and Presenter. View Presenter Model Code- Behind Business Logic
  9. Test Isolation Flow Chart http://www.safnet.com/writing/tech/2014/08/unit-test- isolation-for-legacy-net-code.html
  10. Refactoring • Start refactoring the code, carefully introducing isolation techniques in moving to MVP. • Sprouting – the code behind sprouts into model, view, and presenter. AKA Extract Method. • Adapters – write adapters for ASP.Net functionality that cannot be manipulated in unit tests. • Stubs & mocks – use interfaces and dependency injection properly, then apply stubs and mocks in the new unit test code.
  11. The Straw Man • To illustrate these techniques, I resurrected the code for www.ibamonitoring.org. • It is already split into web project and “back end” library for business logic and data access. • Contains unit and integration tests for the library, but none for the web project. • Originally used Microsoft Moles (now Fakes) to isolate some of the code for unit testing. • The app is sound, but patterns are used inconsistently.
  12. Site Conditions Page
  13. PART 3 Refactoring to Adapters
  14. Adapters for Caching • Introduce adapters that wrap Session, Application, etc. • Side benefit: centralizes magic strings and casting from object to appropriate types. • Use lazy-loading for Property Injection, combined with Test Specific Subclasses, to allow production code to access real objects and tests to access fake objects.
  15. Example: An Adapter for Session • Original code already contained this UserStateManager in order to centralize magic strings. • It has now been refactored to an instance class with an interface that can be mocked.
  16. Using the Session Adapter • Adding the lazy-load to a base class. • Note the use of HttpSessionStateWrapper, which turns old Session into HttpSessionStateBase.
  17. Unit Testing the Adapter • Even an adapter can be unit tested… you’ll need a fake Session for that. One that doesn’t start the ASP.Net engine. • In other words, a Test Specific Subclass. • But Session is sealed. • Hence the use of HttpSessionStateBase, which is not sealed!
  18. Unit Testing the Adapter, cont.
  19. PART 4 Refactoring to Model-View-Presenter (MVP)
  20. The MVP Pattern • Model contains business logic or accesses business logic in services. • View contains properties and methods for accessing form controls and changing the display. • Presenter connects the two; all “UI logic” moves from View to Presenter. • Use Separation of Interfaces to facilitate testing the Presenter.
  21. Deconstructing the View • Move use of dependencies to the Presenter. • Create a property for each control that needs to be accessed by the UI logic in the Presenter. • And methods for behavior changes that the Presenter should invoke in the UI.
  22. Using the Presenter • Add the Presenter to the concrete View. • The view’s events make calls to the Presenter.
  23. Class Diagrams After Refactor
  24. Discussion • View’s interface and Presenter are still in the web project. • This example does not show behavior changes in the view. • This app’s Model is not well-constructed: • Presenter calls static methods that can’t be mocked. • Presenter is invoking business logic, not just UI logic – extract that into the Model.
  25. Evaluating the Presenter • Green – UI layer logic • Red – business logic • Yellow – extract to methods with validation • Business logic should return a modified SiteVisit object after performing inserts. • Just noticed – first line isn’t used! Remove GlobalMap from constructor.
  26. Business Logic – the Facade • For business logic, I prefer to create a Façade class that takes just a few arguments and hides the complexity of data access and manipulation. • The Façade itself can be injected into the Presenter’s constructor.
  27. Refreshing the Class Diagram
  28. SiteConditionsFacade
  29. Refactored Presenter • Accesses state. • Retrieves and validates form values. • Forwards values to the business layer. • Is fully testable.
  30. Result • Original code behind was impossible to test, netting 40 lines of untested code. • Now, code behind has: • Some untested properties – but low risk of defects. • Event handler with one new line of untested code. • A new constructor with one line of untested code. • The presenter, and the wrappers for Session and Application, are 100% unit testable.
  31. PART 5 Unit Testing the View / Introducing Dependency Injection
  32. Web Forms and Dependency Injection • Without dependency injection, I cannot test the View’s constructor or events. • There is a means available for setting up full dependency in Web Forms: an HttpModule. • … and an open source solution to setup Unity as an Inversion of Control (IoC) container: https://bitbucket.org/KyleK/unity.webforms • Likely there are similar packages for other IoC containers, but Unity is my current tool.
  33. Evaluating the View’s Constructor • Here is the updated constructor for the View, injecting the new Façade into the Presenter. • The presence of the View in the Presenter’s constructor introduces a circular dependency, thus preventing use of any IoC container. • View depends on Presenter depends on View
  34. Solution: Abstract Factory • A solution to this conundrum is a Factory class with methods to build the presenters. • The Factory can wrap the IoC container. • It can access session and app state from HttpContext.Current. • In order to unit test the factory, we’ll want to access state variables through lazy-loaded properties. • Be sure to keep the abstract factory in the web project. Discussion: http://odetocode.com/Articles/112.aspx
  35. PresenterFactory
  36. Injecting View and State • To set the instance-specific view and state values resolving the presenter type, use the ResolverOverride parameter argument.
  37. Setup Dependency Injection • The installed package created class UnityWebFormsStart in the web project – add dependencies to this class. • Use Registration by Convention to auto-map the classes in the web project and library.
  38. Modify The View • Add the Factory as a constructor argument in the view / code behind file. • Call the Factory to create the Presenter.
  39. Unit Testing the View • We should be able to unit test the view’s constructor quite easily now • What about the event that calls the presenter? It has two more dependencies to isolate: • Page.IsValid – create adapter an lazy load. • Response.Redirect – lazy load an instance of HttpResponseBase, and create a TSS class. • Best to move these calls to the Presenter – skipping that for time’s sake right now.
  40. PageAdapter • Three commonly used properties to start with, can be expanded as needed. • Temporarily violating YAGNI principle, but it is a trivial and likely useful violation. • Page is not sealed and thus could be sub-classed for testing, but it simply isn’t worth it for 4 lines of code.
  41. Code Coverage • The entire web project is up to 7% coverage. • 25% uncovered in the Factory from lazy loading. • GlobalMap and UserStateManager are legacy – they can be tested now, but are not fully yet. • The View has 5.5% coverage, Presenter 100%.
  42. PART 6 Interlude – Toward MVC
  43. • The goal for this project is to automate tests, not to migrate the framework, but… • … in retrospection, the Presenter we’ve developed is definitely starting to look like a Controller from an ASP.Net MVC project. • The next step in test automation is to address service level testing – and that will be made cleaner by refactoring the Presenter to be very close to an MVC Controller. Refactoring the Presenter Interface
  44. • A Controller has direct access to HttpContext: • The Presenter is in the web project - we can use HttpContext.Current to access these values. • Controller actions accept form data, either as a set of variables or using model binding. • Use the View as the “model” (View Model) and pass to the action instead of to the constructor. • Validation should stay with the View Model. Gap Analysis Session Response Application Request
  45. • The lazy-loaded adapters were previously in a base class for ASPX pages. • Move to a base class for Presenters. • Leave out PageAdapters because those values belong in the View / ViewModel. Lazy Loading Adapters
  46. • Normally an MVC view model would be a concrete class, not an interface. • In this case, convenient to leave as an interface – if changing from Web Forms to MVC, then it will be trivial to change the interface to a concrete POCO. • Updated signatures: Convert to ViewModel
  47. • The project uses validation controls in the ASPX file - need to rely on Page.IsValid for validation. • For test automation, best to have the Presenter react to validation problems • Create an IsValid property in the view interface, and utilize it in the presenter. • Limitation – can’t test the validation details, only the Presenter’s response to invalid data. • Might not need PageAdapter at all now… View Model Validation
  48. • The code behind in the View has become much simpler – call the factory, then call the SaveConditions “action”, passing the View itself as the View Model. • What about the exception handling? In this case, it is ASPX specific and I will leave it alone. Updated View
  49. PART 6 Service Level Testing with SpecFlow
  50. • SpecFlow is a Visual Studio extension for writing business requirements / acceptance tests using the Gherkin language. • Using SpecFlow, we can add service-level tests that connect to the Presenter classes. • … and, when we’re ready to enhance the application, we can write new acceptance tests in a Behavior Driven Development mode. SpecFlow
  51. • As an IBA observer, I want to record the conditions for a site visit so that I can submit point count observations. • Try entering realistic data in all the fields – expect to go to the Point count page. • Try using end time less than start time – expect error. • Try leaving the form blank – expect error. User Story and Brief Confirmations
  52. • Assuming SpecFlow is installed, and you have a test project configured for MSTest*... • Create a Feature file called SiteConditions. • Modify the user story and scenario name. • I will remove the tag and customize the steps in the following slides. Add a Feature * Or leave with NUnit if you prefer
  53. • Since this test is driving a UI, input values include the available options for dropdowns controls. • We could initialize these through a Test Hook, or make the initialization transparent by including them the test definition. Happy Path – Setup Dropdowns
  54. • Fill in valid values. • Simulate pressing the Next button. • Confirm the expected page redirect. • And the unstated expectation that the submitted data will be saved into a database. Happy Path – Fill in Form, Submit It
  55. • Run the scenarios… • Not surprisingly, the scenarios fails to run: there is no connection between the scenario and our application code. Run the Scenario
  56. • Need to right-click and choose Generate Step Definitions. • Creates a step definition file that provides a template for linking the data and actions to the application code. Generate Step Definitions
  57. • The metadata values need to be inserted into the database – which brings us to… • As with any database-integrated tests, you’ll want to use a sandbox database. I will use the same LocalDB instance that I already created for stored procedure testing. • Make sure the test project’s app.config file is properly setup for data access. • Use an ORM for quick-and-easy backdoor data access (showing OrmLite here). Sandbox Database
  58. • Before the complete test run: setup database access. • Before each individual test: clear out all of the tables that will be used. Test Hooks
  59. • Now, edit the generated step definition template, reading the data from SpecFlow and writing into the database. • For convenience, cache a local copy of the objects and their Id values. Insert the Metadata
  60. Setup the View Model • The step “I have entered these values into the form” contains the View Model / form data. • Create a stub implementation of the View, and populate it using SpecFlow’s Assist Helpers. • Store the view model in a static variable for use in the Given step.
  61. • In order to call the Presenter without using ASP.Net, create stub implementations of IUserStateManager and HttpResponseBase. • Instantiate the Presenter using Unity and inject the stub objects. Call the Presenter
  62. • First validate the redirect. • Then use OrmLite again to validate that the actual data stored in the database matches the View Model. Evaluate the Results
  63. • Now we have a fully automated regression test of the “happy path” scenario for saving site conditions – using the entire system except for the ASPX page itself. • Each additional confirmation can be written as a new scenario in the same feature file. • When you re-use a Given, When, or Then phrase, you will have instant C# code re-use. • Note that the feature file is essentially a business requirements document. Service Test Wrap-Up
  64. CONCLUSION
  65. Keys to Success • Split code behind into Model-View-Presenter. • Introduced adapters for ASP.Net classes. • Session • Application • Response • Introduced interfaces and a Factory class. • Added Unity for Web Forms to achieve DI. • Utilized SpecFlow for service-level tests.
  66. • The entire solution is available under the MIT license, hosted on GitHub. • Files of particular interest: • SiteConditions.aspx.cs • SiteConditionsPresenter.cs • PresenterFactory.cs • UserState.cs • SiteConditionsFacade.cs • SiteConditions.feature • SiteConditionsSteps.cs • TestHooks.cs Source Code
  67. Preview: Moving to MVC • Should be able to do something like this… 1. Create an MVC project. 2. Run the original application. Save the generated web pages as .cshtml pages. 3. Change the Id values, e.g. ctl00_contentBody_SiteVisitedInput to SiteVisitedInput (find and replace “ctl00_contentBody” with “”). 4. Move the Presenters to MVC and rename as XyzController. 5. Change View interfaces to concrete ViewModel classes. 6. Update the validation, e.g. with Fluent Validator.
Advertisement