THE CHALLENGE:
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.
8257 interfacing 2 in microprocessor for btech students
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
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.
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.
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!
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.
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.
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.
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
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%.
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
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
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.