{"id":22513331,"url":"https://github.com/toniarnold/aspnettest","last_synced_at":"2025-08-03T16:30:49.535Z","repository":{"id":62163649,"uuid":"134396312","full_name":"toniarnold/aspnettest","owner":"toniarnold","description":"Don't Develop GUI Tests, Teach Your App To Test Itself!","archived":false,"fork":false,"pushed_at":"2024-09-03T22:16:52.000Z","size":29025,"stargazers_count":3,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2024-11-29T08:25:05.409Z","etag":null,"topics":["acceptance-test-driven-development","asp","asp-net","asp-net-core-mvc","asp-net-web-forms","gui-testing","nunit","state-machine","test-engine"],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/toniarnold.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-05-22T10:02:13.000Z","updated_at":"2023-07-04T21:24:43.000Z","dependencies_parsed_at":"2023-02-16T06:01:28.548Z","dependency_job_id":null,"html_url":"https://github.com/toniarnold/aspnettest","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toniarnold%2Faspnettest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toniarnold%2Faspnettest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toniarnold%2Faspnettest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toniarnold%2Faspnettest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/toniarnold","download_url":"https://codeload.github.com/toniarnold/aspnettest/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227637292,"owners_count":17797240,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["acceptance-test-driven-development","asp","asp-net","asp-net-core-mvc","asp-net-web-forms","gui-testing","nunit","state-machine","test-engine"],"created_at":"2024-12-07T03:11:29.599Z","updated_at":"2024-12-07T03:11:30.528Z","avatar_url":"https://github.com/toniarnold.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# aspnettest\n\n[Technical Documentation](./doc/toc.md)\n\nMost recent development: [Blazor Server](./doc/blazor.md) with\n[SpecFlow](./doc/specflow.md), [LinkedIn-Artikelserie (in\nGerman)](https://www.linkedin.com/pulse/blazor-1-disruptive-entwicklungen-toni-arnold)\n\n## [Don't Develop GUI Tests, Teach Your App To Test Itself!](http://www.drdobbs.com/testing/dont-develop-gui-tests-teach-your-app-to/240168468) - in ASP.NET/NUnit/C#\n\n*While* reading above article on Dr. Dobb's, I immediately knew: \n\"This is it!\" - even more so on the ASP.NET stack. Quote from  the article:\n\n\u003e\t### GUI Test Cheating: Do It Yourself\n\n\u003e Basically, the premise for this development was:\n\n\u003e * What are the issues with GUI tests? Control detection and ease of development and maintenance.\n\n\u003e * 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.\n\n### Three ideas make up a whole\n\nRunning the GUI tests in the application process is just one of three orthogonal\nconcepts. Having a persistent monolithic core .NET application detached from the\nconcrete GUI framework is the second one. Whether it is persisted only across\nrequests (ViewState in WebForms terms, the DOM in SPA terms), the browser\nsession or a database entry referenced by a persistent browser cookie is an\ninterchangeable configuration detail (and can even be changed at runtime).\nFor-free transparent persistence allows programming just like a desktop\napplication where the class with `static void Main(string[] args)` naturally\nprovides static state memory that gets manipulated by GUI events.\n\nThe React/Redux combo implements a similar idea solely in Frontend-JS in a much\nmore elaborated, but also more \"opinionated\" way: It provides a central\nmonolithic state store (optionally persisted with the redux-localstorage npm\npackage) which gets manipulated by the state-changing \"Reducer\" in response to\n\"Actions\" emitted by the GUI. The GUI updates itself reactively according to\nglobal state changes. The core difference: Redux conceives global state as\n*immutable* (the Reducer creates new state instances) while here, state is\nconceived as *mutable*:\n\nPersistence across requests is the precondition for implementing the application\nas an SMC state machine as the third idea where button clicks perform state\ntransitions (mutating the object) and distinct states correspond to pages or\nparts  displayed in the browser. The SMC part just decorates the application and\nimposes no further restrictions on it - calling formal state transitions from\nGUI events instead of class methods directly is just a matter of convention.\nAutomatically generating state diagrams as self-documentation is not the least\nvirtue provided by the SMC compiler. Concrete states can be viewed as short\nnames for a given set of (serialized) field values of the Main/state object.\n\nWhen GUI events execute state transitions on a persistent SMC class statically\naccessible to the testing class, test case assertions can observe the expected\nmodel state directly and explicitly instead of guessing correctness indirectly\n(and thus roughly) by observing the GUI state change triggered by the model\nstate change. In short:\n\n1. In-process web application GUI test...\n2. ...of a persistent, monolithic...\n3. ...state machine...\n\n...decoupled from the concrete web GUI framework (ancient WebForms, SSR MVC\ncore, WebSharper SPA (C# or F#), Blazor Server) makes the aspnettest way of\ndoing things (to my knowledge) unique - and blatantly ordinary at the same time,\nsimply based on the model of classic native desktop applications.\n\nAnd even further: With WebForms (using `UpdatePanel`), WebSharper and Blazor\nServer, nothing stands in the way of compositionally building up very complex\nweb applications made out of arbitrary many loosely coupled SMC monoliths, as\ndemonstrated with the respective triptych examples with three components not\neven sharing the persistence mechanism (DOM, session and database).\n\n### The different frameworks\n\nThe architecture presented has been implemented successively and isomorphically in these\nframeworks in chronological order:\n\n1. ASP.NET WebForms\n2. ASP.NET Core MVC (Controllers and Actions)\n3. WebSharper\n4. Blazor Server\n\nWebForms runs on .NET Framework and not on .NET Core like the other three\narchitectures. [WebSharper](./doc/websharper.md) seems to have been discontinued\nnow, and MVC Controllers and Actions never caught up to the old WebForms with\nrespect to an abstract in-memory representation of the page structure\n(ultimately the DOM). Finally, Blazor Server arrived there where WebForms once\nwas:\n\nThe fully compositional Components (Blazor) roughly correspond to the User\nControls (WebForms). Both are statically accessed by the running tests and can\nobtain the framework-generated unique ClientID resp. the (undocumented) ID HTML\nattribute to unambiguously access DOM elements from the Browser.\n\nIn WebForms (and Core), synchronization is simple, as server round-trips always\nentail a full HTTP request. WebSharper SPAs are completely different: There is\nno synchronization, the tests have to wait and poll for changes to happen\n(`AssertPoll`). Blazor Server changed the game again: The `OnAfterRender` resp.\n`OnAfterRenderAsync` overrides allow a tight synchronization, as they're called\n*on the server* after the JS client in the browser has finished rendering.\n\n\n### In Code\n\nThe demo code is a direct port of my PHP example from [The State Machine Compiler](http://smc.sourceforge.net) \n(in the `.\\examples\\Php\\web` folder).\n\nThe unit test on the left-hand side inherits from the `Calculator` app class,\ndirectly calls the transition methods on the contained state machine and \nasserts its result states and the calculation result on the stack.\n\nThe GUI test on the right-hand side talks via COM to an Internet Explorer[*](#migration-to-selenium) instance, \nwrites into a `TextBox` and clicks an ASP.NET `Button`. Indirectly, these clicks call\nthe very same transition methods on the state machine in the embedded `Calculator` instance \nand - this is the salient point - assert the result states strongly typed directly \non that instance, exactly as in the unit tests. This is possible because the \ntest engine runs in the same  address space as the Visual Studio Development Server. \nMoreover, the same test engine also has access to the rendered HTML and asserts the \ntext of the calculation result in there, too.\n\n\u003ctable\u003e\n\u003ctr\u003e\u003cth\u003eUnit Test\u003c/th\u003e\u003cth\u003eGUI Test\u003c/th\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\n\n```csharp\n[Test]\npublic void SqrtTest()\n{\n\tthis._fsm.Enter(\"\");\n\tAssert.That(this.State, Is.EqualTo(CalculatorContext.Map1.Enter));\n\tthis._fsm.Enter(\"49\");\n\tAssert.That(this.State, Is.EqualTo(CalculatorContext.Map1.Calculate));\n\tvar before = this.Stack.Count;\n\tthis._fsm.Sqrt(this.Stack);\n\tAssert.That(this.State, Is.EqualTo(CalculatorContext.Map1.Calculate));\n\tAssert.That(this.Stack.Peek(), Is.EqualTo(\"7\"));\n\tAssert.That(this.Stack.Count, Is.EqualTo(before));\n}\n```\n\n\u003c/td\u003e\u003ctd\u003e\n\n```csharp\n[Test]\npublic void SqrtTest()\n{\n\tthis.Navigate(\"/asp.webforms/default.aspx\");\n\tthis.Click(\"footer.enterButton\");\n\tAssert.That(this.State, Is.EqualTo(CalculatorContext.Map1.Enter));\n\tthis.Write(\"enter.operandTextBox\", \"49\");\n\tthis.Click(\"footer.enterButton\");\n\tAssert.That(this.State, Is.EqualTo(CalculatorContext.Map1.Calculate));\n\tvar before = this.Stack.Count;\n\tthis.Click(\"calculate.sqrtButton\");\n\tAssert.That(this.State, Is.EqualTo(CalculatorContext.Map1.Calculate));\n\tAssert.That(this.Stack.Peek(), Is.EqualTo(\"7\"));\n\tAssert.That(this.Stack.Count, Is.EqualTo(before));\n\tAssert.That(this.Html(), Does.Contain(\" 7\\n\"));\n}\n```\n\n\u003c/td\u003e\u003c/tr\u003e\n\u003c/table\u003e\n\t\t\nTo quote again:\n\n\u003e### How Does It Look and Feel?\n\n\u003eSince one video is worth a thousand pictures, check out this screencast recorded on a ~~Mac~~ win10 box running an initial GUI test suite:\n\n![Tests running...](doc/img/running.gif)\n\n\n## Migration to Selenium\n\nAll [Screen Recordings](./doc/recordings.md) were done with COM\nSHDocVw.InternetExplorer. This IIE interface is now deprecated and being\n[replaced with ISelenium](doc/migrate-iie-iselenium.md), as it is not functional\nno more with current win10 versions.\n\nISelenium is currently still API compatible to the legacy IEE interface. The git\ntag ```Examples-IIE-compatible``` marks the end of backwards compatibility for\nthe various In-App test examples.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoniarnold%2Faspnettest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftoniarnold%2Faspnettest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoniarnold%2Faspnettest/lists"}