- Notifications
You must be signed in to change notification settings - Fork0
Don't Develop GUI Tests, Teach Your App To Test Itself!
License
toniarnold/aspnettest
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Most recent development:Blazor Server withSpecFlow,LinkedIn-Artikelserie (inGerman)
Don't Develop GUI Tests, Teach Your App To Test Itself! - in ASP.NET/NUnit/C#
While reading above article on Dr. Dobb's, I immediately knew:"This is it!" - even more so on the ASP.NET stack. Quote from the article:
Basically, the premise for this development was:
- What are the issues with GUI tests? Control detection and ease of development and maintenance.
- How can we fix them? Well, let's avoid having to "find" the controls, and instead make them always available. Then, let's use our favorite language and environment to write the test code.
Running the GUI tests in the application process is just one of three orthogonalconcepts. Having a persistent monolithic core .NET application detached from theconcrete GUI framework is the second one. Whether it is persisted only acrossrequests (ViewState in WebForms terms, the DOM in SPA terms), the browsersession or a database entry referenced by a persistent browser cookie is aninterchangeable configuration detail (and can even be changed at runtime).For-free transparent persistence allows programming just like a desktopapplication where the class withstatic void Main(string[] args)
naturallyprovides static state memory that gets manipulated by GUI events.
The React/Redux combo implements a similar idea solely in Frontend-JS in a muchmore elaborated, but also more "opinionated" way: It provides a centralmonolithic state store (optionally persisted with the redux-localstorage npmpackage) which gets manipulated by the state-changing "Reducer" in response to"Actions" emitted by the GUI. The GUI updates itself reactively according toglobal state changes. The core difference: Redux conceives global state asimmutable (the Reducer creates new state instances) while here, state isconceived asmutable:
Persistence across requests is the precondition for implementing the applicationas an SMC state machine as the third idea where button clicks perform statetransitions (mutating the object) and distinct states correspond to pages orparts displayed in the browser. The SMC part just decorates the application andimposes no further restrictions on it - calling formal state transitions fromGUI events instead of class methods directly is just a matter of convention.Automatically generating state diagrams as self-documentation is not the leastvirtue provided by the SMC compiler. Concrete states can be viewed as shortnames for a given set of (serialized) field values of the Main/state object.
When GUI events execute state transitions on a persistent SMC class staticallyaccessible to the testing class, test case assertions can observe the expectedmodel state directly and explicitly instead of guessing correctness indirectly(and thus roughly) by observing the GUI state change triggered by the modelstate change. In short:
- In-process web application GUI test...
- ...of a persistent, monolithic...
- ...state machine...
...decoupled from the concrete web GUI framework (ancient WebForms, SSR MVCcore, WebSharper SPA (C# or F#), Blazor Server) makes the aspnettest way ofdoing things (to my knowledge) unique - and blatantly ordinary at the same time,simply based on the model of classic native desktop applications.
And even further: With WebForms (usingUpdatePanel
), WebSharper and BlazorServer, nothing stands in the way of compositionally building up very complexweb applications made out of arbitrary many loosely coupled SMC monoliths, asdemonstrated with the respective triptych examples with three components noteven sharing the persistence mechanism (DOM, session and database).
The architecture presented has been implemented successively and isomorphically in theseframeworks in chronological order:
- ASP.NET WebForms
- ASP.NET Core MVC (Controllers and Actions)
- WebSharper
- Blazor Server
WebForms runs on .NET Framework and not on .NET Core like the other threearchitectures.WebSharper seems to have been discontinuednow, and MVC Controllers and Actions never caught up to the old WebForms withrespect to an abstract in-memory representation of the page structure(ultimately the DOM). Finally, Blazor Server arrived there where WebForms oncewas:
The fully compositional Components (Blazor) roughly correspond to the UserControls (WebForms). Both are statically accessed by the running tests and canobtain the framework-generated unique ClientID resp. the (undocumented) ID HTMLattribute to unambiguously access DOM elements from the Browser.
In WebForms (and Core), synchronization is simple, as server round-trips alwaysentail a full HTTP request. WebSharper SPAs are completely different: There isno synchronization, the tests have to wait and poll for changes to happen(AssertPoll
). Blazor Server changed the game again: TheOnAfterRender
resp.OnAfterRenderAsync
overrides allow a tight synchronization, as they're calledon the server after the JS client in the browser has finished rendering.
The demo code is a direct port of my PHP example fromThe State Machine Compiler(in the.\examples\Php\web
folder).
The unit test on the left-hand side inherits from theCalculator
app class,directly calls the transition methods on the contained state machine andasserts its result states and the calculation result on the stack.
The GUI test on the right-hand side talks via COM to an Internet Explorer* instance,writes into aTextBox
and clicks an ASP.NETButton
. Indirectly, these clicks callthe very same transition methods on the state machine in the embeddedCalculator
instanceand - this is the salient point - assert the result states strongly typed directlyon that instance, exactly as in the unit tests. This is possible because thetest engine runs in the same address space as the Visual Studio Development Server.Moreover, the same test engine also has access to the rendered HTML and asserts thetext of the calculation result in there, too.
Unit Test | GUI Test |
---|---|
[Test]publicvoidSqrtTest(){this._fsm.Enter("");Assert.That(this.State,Is.EqualTo(CalculatorContext.Map1.Enter));this._fsm.Enter("49");Assert.That(this.State,Is.EqualTo(CalculatorContext.Map1.Calculate));varbefore=this.Stack.Count;this._fsm.Sqrt(this.Stack);Assert.That(this.State,Is.EqualTo(CalculatorContext.Map1.Calculate));Assert.That(this.Stack.Peek(),Is.EqualTo("7"));Assert.That(this.Stack.Count,Is.EqualTo(before));} | [Test]publicvoidSqrtTest(){this.Navigate("/asp.webforms/default.aspx");this.Click("footer.enterButton");Assert.That(this.State,Is.EqualTo(CalculatorContext.Map1.Enter));this.Write("enter.operandTextBox","49");this.Click("footer.enterButton");Assert.That(this.State,Is.EqualTo(CalculatorContext.Map1.Calculate));varbefore=this.Stack.Count;this.Click("calculate.sqrtButton");Assert.That(this.State,Is.EqualTo(CalculatorContext.Map1.Calculate));Assert.That(this.Stack.Peek(),Is.EqualTo("7"));Assert.That(this.Stack.Count,Is.EqualTo(before));Assert.That(this.Html(),Does.Contain(" 7\n"));} |
To quote again:
Since one video is worth a thousand pictures, check out this screencast recorded on a
Macwin10 box running an initial GUI test suite:
AllScreen Recordings were done with COMSHDocVw.InternetExplorer. This IIE interface is now deprecated and beingreplaced with ISelenium, as it is not functionalno more with current win10 versions.
ISelenium is currently still API compatible to the legacy IEE interface. The gittagExamples-IIE-compatible
marks the end of backwards compatibility forthe various In-App test examples.
About
Don't Develop GUI Tests, Teach Your App To Test Itself!