📜 ⬆️ ⬇️

Unit tests in ASP.NET WebForms

About unit tests


Unit tests are known to be a software developer's headache pill. If used correctly and according to the instructions, then a healthy complexion and shine in the eyes is provided without stimulating agents. About unit tests speak differently: with aspiration, those who read about them in MSDN magazine, with admiration those who have already plunged into the world of the beautiful, and with regret those who, by the will of fate, have to work in an environment totally unacceptable to this heavenly mana. I can only wish decisiveness first, applauding the second, but this article is addressed to the third.

Who is it for? About what?


This article is about the same as me, the developers of corporate sites in C # / ASP.NET WebForms. People who, like me, have grown to the realization that ASP.NET WebForms is cool, but not quite. Not really, because web forms do not support unit tests, and therefore TDD. And the only practical way to write strong code is to read it thoughtfully. So I suffered the last few years developing existing asp.net projects, until I finally saw the light. About insight and will be discussed.


A bit of history


At the time of formation, WebForms was intended for desktop VB programmers who had already mastered the CommandButton, ComboBox and Click event (do not rush, I started it that way too!), Heard about the unprecedented heyday of the Internet (everyone remembers the dotcom boom?) And decided to invest in this business. For these, WebForms was invented, which most fully transferred the concept of RAD (rapid application development) to the web: form controls, event handlers, and an optional heap of whistles.
')

What is the problem?


And everything would be great, but then suddenly, it turned out that in this form, the developer is very easy to mix business logic with the presentation and get stuffing that can not be tested.
To whom the above mentioned affected the depths of programmer indignation, it is clear that the problem is in the environment. Not only is it difficult to design a reliable, expandable system, as well ASP.NET rests on all the horns and hooves! Here's how to test ascx control if everything in it is hung up on events? Hands all, you will not test.

How to be?


It is here that I come smoothly and gently to the topic of the article.

First, some foreplay

As I said above, one and the fundamental problems is mixing business logic and presentation in one pile. It is necessary to dilute them in the corners. Here it is appropriate to use one of the varieties of MVC, MVP. Because in MVP, View controls the appearance, as well as serves user requests. The role of Presenter is to prepare data for the view, as well as communication with the data repository for example. Model is a View Model, aka container containing data for displaying the view. So, MVP ideally fits into the ASP.NET operation scheme: view is all the same user control, presenter, this is a class containing business logic, and a model is also a class containing data.

Laugh work is as follows: view takes the model from the presenter, but when it needs it, and also notifies it of the user's input if it exists. Presenter, in turn, listens to the view of other presenters and creates a model on demand, taking into account all the incoming data.

Begin to pull in the side of the bedroom

Having dealt with the basic structure, you can start writing tests. It is clear that writing a unit test covering absolutely everything is impractical because such a test will be 2-3 times longer than the class it tests.
A little thought, I wrote a list of possible interactions of components:

And accordingly the following types of tests


In this article I will talk about the most difficult, about the unit test view. In the following articles I will describe less complex tests.

Go!


So, as already mentioned, view is the usual ascx control. Such control should be hosted on the aspx page in the asp.net runtime. To do this, you can use the method
System.Web.Hosting.ApplicationHost.CreateApplicationHost(Type hostType, string virtualDir, string physicalDir) 


where hostType must be the type inheriting MarshalByRefObject, and virtualDir and physicalDir must specify the virtual path and physical path, respectively. CreateApplicationHost returns a host to which HTTP requests can be sent.

Now let's think about what we want to write in a unit test? Since this test covers the interaction between the view and the presenter, it is necessary to write a list of possible scenarios and run the view on them in parallel, tracking what and how the presenter was transmitted. That is, from our unit test you need to be able to send requests, create a presenter, receive answers, as well as control what happened during the processing of requests.

If it is not difficult in principle with requests (for example, to simulate a postback, you just need to correctly compose the POST request parameters), then controlling the presenter is by no means trivial, because it lives on the other side of the AppDomain.

AppDomain is something like a process in process. In other words, the .NET environment allows you to split executable code in the system process into logical subprocesses that are isolated from each other. In our case, CreateApplicationHost creates a new host just in another AppDomain, and therefore we can’t directly address objects from the unit test, and accordingly we have no access to the presenter on the other side of the barrier.

Fortunately, there are proxy classes that can send requests on both sides using messages. You can read about proxies in Google (request of type .net remoting proxy). It is important for us to know that proxies can call methods.
 public class Proxy<T> : MarshalByRefObject { private readonly T _object; public Proxy(T obj) { _object = obj; } public TResult Invoke<TResult>(Func<T, TResult> selector) { return selector(_object); } public void Invoke(Action<T> action) { action(_object); } } 


In addition, the question arises, in what specific way will we track the interaction of the view-presenter. For simplicity, we can use any mocking framework that can track class calls. In this case, I chose Moq .

And the last is a container for data transfer. We have a lot to transmit: data on the mock-presenter, data on the environment, data on the request, and so on. Without further ado, we create the TestData class:
 [Serializable] public class TestData { public Type MockPresenterType { get; set; } public Delegate MockPresenterInitializer { get; private set; } public void SetPresenterMockInitializer<TPresenter>(Action<Mock<TPresenter>> action) where TPresenter : class { this.MockPresenterInitializer = action; this.MockPresenterType = typeof(TPresenter); } } 


In light of the above, the unit test algorithm looks like this:

On the part of the unit test:
  1. Create and configure a host
  2. Create and configure an instance of the TestData class
  3. Send request with the necessary control and testData
  4. Process response
  5. Check installation mock-presenter

From the host side:
  1. In Default.aspx OnPreInit load the required control
  2. Create the required mock-presenter
  3. Apply mock-presenter settings


And the approximate type of unit test:

 [TestMethod] public void UnitTestView { string physicalPath = Environment.CurrentDirectory.Substring(0, Environment.CurrentDirectory.IndexOf("bin")); TestHost host = (TestHost)ApplicationHost.CreateApplicationHost(typeof(TestHost), "/Testing", physicalDir); TestData testData = new TestData(); testData.SetPresenterMockInitializer<Presenter>( mock => { mock.Setup(m => ...); }); host.AddQueryParameter("ControlToTest", "~/OurView.ascx"); string response = host.ProcessRequest("Default.aspx", testData, null); XDocument doc = XDocument.Parse(response); host.GetPresenterMock<Presenter>().Invoke(mock => mock.Verify(m => m.SomeMethodRan(), Times.Once())); } 


From the host side, the Default.aspx.cs code also looks very simple:
 public class Default : Page, IRequiresSessionState { private readonly PlaceHolder _phContent = new PlaceHolder(); protected override void FrameworkInitialize() { this.Controls.Add(new LiteralControl(@"<!DOCTYPE html><html><body>")); var form = new HtmlForm(); this.Controls.Add(form); form.Controls.Add(_phContent); this.Controls.Add(new LiteralControl(@"</body></html>")); } protected override void OnPreInit(EventArgs e) { base.OnPreInit(e); var controlToTest = this.Request.QueryString["ControlToTest"]; var testData = (TestData)this.Context.Items["TestData"]; if (controlToTest != null) { Control c = this.LoadControl(controlToTest); Type viewType = get view type from that control if (viewType != null) { object presenter; if (testData.MockPresenterType != null) { var mockType = typeof(Mock<>).MakeGenericType(testData.MockPresenterType); object mock = Activator.CreateInstance(mockType); AppDomain.CurrentDomain.SetData("PresenterMock", mock); presenter = mockType.GetProperty("Object", testData.MockPresenterType).GetValue(mock, null); testData.MockPresenterInitializer.DynamicInvoke(mock); viewType.GetProperty("Presenter").SetValue(c, presenter, null); AppDomain.CurrentDomain.SetData("Presenter", presenter); _phContent.Controls.Add(c); } } 


Here I intend to omit the details of the viewType search for it is purely individual (besides, I did not describe how I implemented the MVP pattern). The key points are


The companion class TestHost is shown below:
 public class TestHost : MarshalByRefObject { private readonly Dictionary<string, string> _query = new Dictionary<string, string>(); public void AddQueryParameter(string key, string value) { _query.Add(key, HttpUtility.UrlEncode(value)); } private string GetQueryParameters() { StringBuilder sb = new StringBuilder(); foreach (var parameter in _query) { sb.AppendFormat("{0}={1}&", parameter.Key, parameter.Value); } return sb.ToString(); } public string ProcessRequest(string page, TestData testData, string postData) { StringWriter writer = new StringWriter(); TestRequest swr = new TestRequest(page, GetQueryParameters(), writer, testData, postData); HttpRuntime.ProcessRequest(swr); writer.Close(); return writer.ToString(); } public Proxy<Mock<TPresenter>> GetPresenterMock<TPresenter>() where TPresenter : class { object mock = AppDomain.CurrentDomain.GetData("PresenterMock"); return new Proxy<Mock<TPresenter>>((Mock<TPresenter>)mock); } } public class TestRequest : SimpleWorkerRequest { private readonly TestData _testData; private readonly byte[] _postData; private const string PostContentType = "application/x-www-form-urlencoded"; public TestRequest(string page, string query, TextWriter output, TestData testData, string postData) : base(page, query, output) { _testData = testData; if (!string.IsNullOrEmpty(postData)) _postData = Encoding.Default.GetBytes(postData); } public override void SetEndOfSendNotification(EndOfSendNotification callback, object extraData) { base.SetEndOfSendNotification(callback, extraData); HttpContext context = extraData as HttpContext; if (context != null) { context.Items.Add("TestData", _testData); } } public override string GetHttpVerbName() { if (_postData == null) return base.GetHttpVerbName(); return "POST"; } public override string GetKnownRequestHeader(int index) { if (index == HeaderContentLength) { if (_postData != null) return _postData.Length.ToString(); } else if (index == HeaderContentType) { if (_postData != null) return PostContentType; } return base.GetKnownRequestHeader (index); } public override byte[] GetPreloadedEntityBody() { if (_postData != null) return _postData; return base.GetPreloadedEntityBody(); } } 


Smoke break



That's all, at this stage you already have the opportunity to write elegant unit tests (as far as possible) for asp.net webforms. Of course, if you have the opportunity to use another more test-friendly environment, by all means, as my American colleagues say. But if it is not there, and I want to write more stable code, now it is possible.

By the way, I gave only the most necessary code for the proof-of-concept. I omitted for example the process of creating a postback and transferring test data, as well as preparing test controls for the test. If there is interest, I can write about it, as well as about my implementation of MVP, and about the other 2 types of tests I mentioned above.

Thanks for attention.

Source: https://habr.com/ru/post/118409/


All Articles