Developers were given server controls to quickly and effectively arrange views, and code-behind classes to serve incoming requests and produce appropriate responses. A code-behind class is a piece of code that is tightly bound to the ASP.NET infrastructure. Any code placed in a code-behind class has direct access to the HTTP context and can read and write cookies, posted data, query string parameters, session state, HTTP headers, and so forth. Code-behind classes serve two main purposes: For HTTP GET requests, they set up the page for display; for HTTP POST requests, they grab posted data, prepare a call to the application's back end, receive a response, and prepare the next view for the user.
Sounds like a simple workflow, right? But in fact, it is not necessarily simple. The real complexity of such a workflow is determined by the inherent complexity of the call made to the application's back end. It is one thing to invoke a stored procedure from the code-behind class and display its results; it is quite another to invoke a stored procedure that is only one step in a far more complex piece of business logic. As an example, consider what it means for an e-commerce application to place an order. There might be extremely simple scenarios in which all that is required is adding a few records in a couple of tables; say, one record in the
OrderDetails table for each item ordered, and one record for the order itself in the
Orders table. In such a case, it might be acceptable that developers place all the code a single call to the Data Access Layer right in the code-behind class.
What if, instead, placing an order involves several (sequential and conditional) steps such as checking the credit status of the customer, checking the availability of goods, refilling stock by placing orders to suppliers, interacting with the back end systems of the shipping company, updating the company's accounting systems, and maybe more? You might still need to add a bunch of records to the
OrderDetails tables, but there's now a whole workflow to be orchestrated, which significantly increases the level of complexity of the code you need to trigger from the user interface.
Can this orchestration code be managed from within code-behind classes?
Over the past few years, scenarios where the code required to serve a Web request is fairly complex have become ubiquitous. As a result, having fat methods in code-behind classes shifted from being an acceptable practice to being a bad practice best avoided for the sake of maintainability and testability.
More complexity in the logic expressed via software inevitably means a stricter need to test the code to ensure that changes and new development don't break existing features.
How Easy Is It To Test Code Written for the ASP.NET Framework?
Design for Testability is a methodology aimed at making software easier to test automatically through other software. Design for Testability is centered on three pillars: visibility, control, and simplicity. Code that is easy to test is code that exposes the most relevant parts of the internal state so that appropriate assertions can be written and checked. Likewise, for tests to be relevant and reliable, testers should be able to force ad hoc input values on methods and exercise control over their invocation. Finally, the simpler the code, the more reliable response you get from the test.
The main impact of testability on software development comes through unit tests and integration tests. Unit tests are automatic tests done for the most part in isolation on code that has no dependencies whatsoever on the surrounding environment. Integration tests instead check whether interconnected parts of the system work well together.
When you try to apply Design for Testability to ASP.NET Web Forms, you find that you can gain a good level of visibility over the state of the infrastructure (that is, ASP.NET intrinsic objects), but you can hardly force test values without spinning up the entire ASP.NET runtime in the test environment. In fact, in ASP.NET Web Forms, you can't just simulate a fake HTTP context with test values in session state, a response stream that saves to memory, or an ad hoc query string. Because code-behind classes are tightly bound to the ASP.NET HTTP runtime, and the ASP.NET HTTP runtime doesn't allow mocking, covering ASP.NET Web Forms applications with unit tests is pretty hard. Integration tests then seem to be the only affordable option you have for testing ASP.NET Web Forms pages. But integration tests, by nature, are slower to run, thus developers tend to run them less often.
What Can You Do About It?
Unit testing is quite problematic in ASP.NET Web Forms if developers place all of the use-case logic into code-behind classes. An alternative option would be introducing a new layer of code that bridges code-behind classes (still part of the presentation layer) to the back end of the system. Such intermediate classes would receive HTTP context-specific data as an argument and operate in total isolation from the surrounding HTTP context. As a result, you extract all code that really expresses application logic from the presentation layer. The resulting design is therefore cleaner and much more testable.
So the bottom line is that while ASP.NET Web Forms certainly was not laid out with testability in mind (it was designed at a time when testability was not a primary concern for most developers), by intensively applying good principles of software design (Separation of Concerns and layers) you can easily write ASP.NET code that is unit testable.
On a final note, let's consider the twin ASP.NET framework the increasingly popular ASP.NET MVC. Being a newer framework, ASP.NET MVC was designed for helping developers to write more testable code. And those good old principles of software design applied to ASP.NET MVC can make not just your code but also your unit tests far simpler.