{"id":16990263,"url":"https://github.com/jcoliz/blazorfunctionalteststack","last_synced_at":"2026-04-12T11:37:36.133Z","repository":{"id":138561806,"uuid":"500534284","full_name":"jcoliz/BlazorFunctionalTestStack","owner":"jcoliz","description":"Demonstrates a simple yet powerful approach to Business-Driven Development and Functional Testing in .NET on a Blazor app.","archived":false,"fork":false,"pushed_at":"2022-06-07T02:44:30.000Z","size":250,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-22T03:13:54.510Z","etag":null,"topics":["asp-net-core","bdd","blazor","dotnet","playwright"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jcoliz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-06-06T17:41:51.000Z","updated_at":"2022-06-06T17:45:27.000Z","dependencies_parsed_at":null,"dependency_job_id":"fe603f4d-6c4c-442a-b4b7-81669c6754e4","html_url":"https://github.com/jcoliz/BlazorFunctionalTestStack","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jcoliz/BlazorFunctionalTestStack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcoliz%2FBlazorFunctionalTestStack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcoliz%2FBlazorFunctionalTestStack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcoliz%2FBlazorFunctionalTestStack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcoliz%2FBlazorFunctionalTestStack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jcoliz","download_url":"https://codeload.github.com/jcoliz/BlazorFunctionalTestStack/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcoliz%2FBlazorFunctionalTestStack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31713876,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-12T06:22:27.080Z","status":"ssl_error","status_checked_at":"2026-04-12T06:21:52.710Z","response_time":58,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["asp-net-core","bdd","blazor","dotnet","playwright"],"created_at":"2024-10-14T03:09:34.059Z","updated_at":"2026-04-12T11:37:36.092Z","avatar_url":"https://github.com/jcoliz.png","language":"F#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Blazor Functional Test Stack\n\nDemonstrates a simple yet powerful approach to Behaviour-Driven Development\nand Functional Testing in .NET on a Blazor app.\n\nThe stack is: [.NET](https://dotnet.microsoft.com/en-us/download) | [NUnit](https://nunit.org/) | [Playwright for .NET](https://playwright.dev/dotnet/docs/intro) | [TickSpec](https://github.com/fsprojects/TickSpec) | [FsUnit](https://fsprojects.github.io/FsUnit/)\n\nI have found this stack enables me to really quickly add functional tests to a new\nweb app, using this process:\n\n1. Copy entire Tests.Functional directory into an app\n2. Change the local.runsettings to match default project port\n3. Add data-test-id's to code under test\n4. Change the .feature file to match the app\n\n## How to try it\n\n### Clone it\n\n```\nPS\u003e git clone https://github.com/jcoliz/BlazorFunctionalTestStack.git\nPS\u003e cd BlazorFunctionalTestStack\n```\n\n### Build it\n\nIf you don't already have the .NET 6.0 SDK installed, be sure to get a copy first from the [Download .NET](https://dotnet.microsoft.com/en-us/download) page.\n\n```\nPS\u003e dotnet build\n```\n\n### Install browsers\n\nIf this is your first time running the version of PlayWright used by the tests, you'll need to\ninstall the browsers.\n\n```\nPS\u003e pwsh .\\Tests.Functional\\bin\\Debug\\net6.0\\playwright.ps1 install\n```\n\n### Run app in backround\n\nThis script requires PowerShell 7. If you are running an old version, this is a great time\nto [upgrade](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows)! Otherwise, you could open another window and run it there.\n\n```\nPS\u003e .\\startbg.ps1\n\nId     Name            PSJobTypeName   State         HasMoreData     Location             Command\n--     ----            -------------   -----         -----------     --------             -------\n27     uitestsbg       BackgroundJob   Running       True            localhost            dotnet run\n```\n\n### Run the tests\n\n```\nPS\u003e dotnet test\n\nTest run for .\\Tests.Functional\\bin\\Debug\\net6.0\\Tests.Functional.dll (.NETCoreApp,Version=v6.0)\nMicrosoft (R) Test Execution Command Line Tool Version 17.1.0\nCopyright (c) Microsoft Corporation.  All rights reserved.\n\nStarting test execution, please wait...\nA total of 1 test files matched the specified pattern.\n\nPassed!  - Failed:     0, Passed:     7, Skipped:     0, Total:     7, Duration: 13 s - Tests.Functional.dll (net6.0)\n```\n\n### Stop the background app\n\nBest to do it now before you forget!\n\n```\nPS\u003e .\\stopbg.ps1\n```\n\n### Examine the screen shots\n\n```\nPS\u003e start .\\Tests.Functional\\bin\\Debug\\net6.0\\Screenshot\\\n```\n\n![Screenshots](/docs/images/Screenshots.png)\n\n## Check out the tests\n\nThe tests are written in Gherkin. You can find the full set in the [Porfolio.feature](Tests.Functional/Portfolio.feature) file. Gherkin is a great way to write clear, expressive\ntests that humans can make sense of.\n\nBonus is that you can write the Gherkin *before* writing any new code, to follow \nBehaviour Driven Development principles.\n\n```Gherkin\nFeature: Site is alive and healthy\n\nScenario: Root loads OK\n    When user launches site\n    Then page loaded ok\n    And save a screenshot named 00_Root\n\nScenario Outline: Page navigates correctly from root\n    When user navigates to \u003cPage\u003e page via NavMenu\n    Then page title is \u003cTitle\u003e\n    And element h1 is \u003cHeading\u003e\n    Then save a screenshot named \u003cId\u003e_\u003cPage\u003e\n\nExamples:\n| Id | Page    | Title              | Heading           |\n| 10 | Home    | Index              | Hello, world!     |\n| 20 | Counter | Counter            | Counter           |\n| 30 | Fetch   | Weather forecast   | Weather forecast  |\n\nScenario: Counter increments when clicking button\n    Given user navigated to Counter page via NavMenu\n    When clicking Increment 5 times\n    Then currentCount is 5\n```\n\nThen, each step is backed by a few lines of Playwright code. You may notice that the tests\nare in F#. This is because TickSpec uses the language. Not to fear! F# is pretty easy, and generally more concise than the C# alternatives.\n\n```F#\nlet [\u003cGiven\u003e] ``user launched site`` (page: IPage) (uri: Uri) = \n    page.GotoAsync(uri.ToString()) |\u003e Async.AwaitTask |\u003e Async.RunSynchronously\n```\n\n```F#\nlet [\u003cThen\u003e] ``page loaded ok`` (response: IResponse) =\n    response.Ok \n        |\u003e should be True\n```\n\n```F#\nlet [\u003cThen\u003e] ``(\\S*) is (.*)`` (element:string) (expected:string) (page: IPage) =\n    page.TextContentAsync($\"data-test-id={element}\") \n        |\u003e Async.AwaitTask \n        |\u003e Async.RunSynchronously \n        |\u003e should equal expected\n```\n\n## Using data-test-id selectors\n\nBy convention, I prefer to [define explicit contracts](https://playwright.dev/dotnet/docs/selectors#define-explicit-contract) for elements under test. This ensures that later if the text is changed, or the composition of the page is changed, it's highly likely that the tests will still pass.\n\nThus, the steps defined here use data-test-id by default.\n\n```html\n\u003cp role=\"status\"\u003eCurrent count: \u003cspan data-test-id=\"currentCount\"\u003e@currentCount\u003c/span\u003e\u003c/p\u003e\n\n\u003cbutton data-test-id=\"Increment\" class=\"btn btn-primary\" @onclick=\"IncrementCount\"\u003eClick me\u003c/button\u003e\n```\n\n## In-depth look at the stack\n\n### .NET\n\nThe first choice is to write the tests in the same framework used to write the code.\nPersonally, I prefer .NET for everything, so it's my default starting point.\n\n### NUnit\n\nI actually prefer MSTest for its simplicity. However, MSTest doesn't work well in this case,\nso I needed to step up to NUnit. The problem is that it won't surface separate scenarios\nas separate tests. See [MSTestWiring.fs](https://github.com/fsprojects/TickSpec/blob/master/Examples/ByFramework/MSTest/MSTest.FSharp/MSTestWiring.fs) and [testfx-docs #52](https://github.com/Microsoft/testfx-docs/pull/52).\n\n### Playwright for .NET\n\nThe alternative is Selenium using Webdriver. These have a reputation for producing somewhat\nunstable tests. Playwright is build on DevTools, which is newer. I've found my Playwright tests\nto be perfectly stable, once I got the timeouts correct for the environment I'm on. Overall,\nI'm super happy with the ease of use and stability of Playwright.\n\n### TickSpec\n\nUse of TickSpec is probably the most unorthodox choice. SpecFlow is definitely the common choice.\nMy view is that TickSpec is more lightweight and closer to the metal. SpecFlow tends to abstract\naway the details, with IDE extensions, and extra UI. I don't need extra UI. Or more abstractions.\n\nTickSpec also brings the use of F#. For some, a whole new language may be a bit much just to\nadopt a test framework, and I understand that. Still, for me, I think F# is pretty cool, and\nenjoy learning it a bit more.\n\n### FsUnit\n\nAdopting FsUnit allows for having a consistent coding style to the F#-defined steps. This is\nreally an optional piece of the stack. Still, I find it helps for overall readability and consistency of the steps.\n\n## Changes from TickSpec\n\nThe main work of collecting tests is done by [FeatureFixture.cs](Tests.Functional/FeatureFixture.cs). This is taken from the [TickSpec NUnit Examples](https://github.com/fsprojects/TickSpec/blob/master/Examples/ByFramework/NUnit/FSharp.NUnit/FeatureFixture.fs), with a few changes for PlayWright.\n\n1. Inherit the underlying `PageTest` class, provided by `Microsoft.Playwright.NUnit`.\n2. Add a `ServiceCollection`\n3. Add the `Page` and `Uri` into the `ServiceCollection`, for tests to access\n\nHere we inherit the base class, and define the `ServiceCollection`:\n\n```diff\n /// Class containing all BDD tests in current assembly as NUnit unit tests\n [\u003cTestFixture\u003e]\n type FeatureFixture () =\n+    inherit PageTest()\n+\n+    static let Services : ServiceCollection =\n+        new ServiceCollection();\n+\n     /// Test method for all BDD tests in current assembly as NUnit unit tests\n     [\u003cTest\u003e]\n```\n\nHere we add the the `Page` and `Uri` into the `ServiceCollection`, for tests to access. \nSteps can access these objects via dependency injection, by declaring a parameter of\nthe given type.\n\n```diff\n@@ -16,10 +24,12 @@ type FeatureFixture () =\n         if scenario.Tags |\u003e Seq.exists ((=) \"ignore\") then\n             raise (new IgnoreException(\"Ignored: \" + scenario.ToString()))\n         try\n+            Services.AddSingleton\u003cUri\u003e(new Uri(TestContext.Parameters[\"uri\"])) |\u003e ignore\n+            Services.AddSingleton\u003cIPage\u003e(base.Page) |\u003e ignore\n             scenario.Action.Invoke()\n         with\n         | :? TargetInvocationException as ex -\u003e ExceptionDispatchInfo.Capture(ex.InnerException).Throw()\n```\n\nHere we give the steps access to the `ServiceCollection`.\n\n```diff\n@@ -38,6 +48,7 @@ type FeatureFixture () =\n\n         let assembly = Assembly.GetExecutingAssembly()\n         let definitions = new StepDefinitions(assembly.GetTypes())\n+        definitions.ServiceProviderFactory \u003c- fun () -\u003e Services.BuildServiceProvider()\n\n         assembly.GetManifestResourceNames()\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcoliz%2Fblazorfunctionalteststack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjcoliz%2Fblazorfunctionalteststack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcoliz%2Fblazorfunctionalteststack/lists"}