67
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

Refactoring Legacy Web Forms for Test Automation

Embed Size (px)

Citation preview

REFACTORING LEGACY WEB FORMS FOR TEST AUTOMATION

Author: Stephen FuquaLast Revised: December 2014

All content by the author or from public domain sources unless otherwise noted

PART 1The Challenge

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.

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 ViewStateare 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.

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

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.

PART 2Toolkit

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

Test Isolation Flow Chart

http://www.safnet.com/writing/tech/2014/08/unit-test-isolation-for-legacy-net-code.html

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.

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.

Site Conditions Page

PART 3Refactoring to Adapters

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.

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.

Using the Session Adapter

• Adding the lazy-load to a base class.

• Note the use of HttpSessionStateWrapper, which turns old Session into HttpSessionStateBase.

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!

Unit Testing the Adapter, cont.

PART 4Refactoring to Model-View-Presenter (MVP)

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 Interfacesto facilitate testing the Presenter.

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.

Using the Presenter

•Add the Presenter to the concrete View.

• The view’s events make calls to the Presenter.

Class Diagrams After Refactor

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.

Evaluating the Presenter

• Green – UI layer logic

• Red – business logic

• Yellow – extract to methods with validation

• Business logic should return a modified SiteVisitobject after performing inserts.

• Just noticed –first line isn’t used! Remove GlobalMap from constructor.

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.

Refreshing the Class Diagram

SiteConditionsFacade

Refactored Presenter

• Accesses state.

• Retrieves and validates form values.

• Forwards values to the business layer.

• Is fully testable.

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 Sessionand Application, are 100% unit testable.

PART 5Unit Testing the View / Introducing Dependency Injection

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 IoCcontainers, but Unity is my current tool.

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’sconstructor introduces a circular dependency, thus preventing use of any IoC container.

• View depends on Presenter depends on View

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

PresenterFactory

Injecting View and State

• To set the instance-specific view and state values resolving the presenter type, use the ResolverOverride parameter argument.

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.

Modify The View

•Add the Factory as a constructor argument in the view / code behind file.

•Call the Factory to create the Presenter.

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.

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.

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%.

PART 6Interlude – Toward MVC

• 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

•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

• The lazy-loaded adapters were previously in a base class for ASPX pages.

•Move to a base class for Presenters.

• Leave out PageAdaptersbecause those values belong in the View / ViewModel.

Lazy Loading Adapters

•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

• The project uses validation controls in the ASPX file - need to rely on Page.IsValid for validation.

• For test automation, best to have the Presenterreact 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

• 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

PART 6Service Level Testing with SpecFlow

• 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

•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

•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

• 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

• 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

•Run the scenarios…

•Not surprisingly, the scenarios fails to run: there is no connection between the scenario and our application code.

Run the Scenario

•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

• 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 dataaccess (showing OrmLite here).

Sandbox Database

•Before the complete test run: setup database access.

•Before each individual test: clear out all of the tables that will be used.

Test Hooks

• 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

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.

• 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

• 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

•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

CONCLUSION

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.

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.