-
Couldn't load subscription status.
- Fork 287
Description
Context
I opened another issue some time ago to show some of my frustration with what seemed to be untapped potential in the new Microsoft.Testing.Platform framework here:
Some of my concerns then were addressed: it turned out I didn't fully comprehend how MTP worked behind the scenes and how one could actually manage their own Program.cs etc. That was a nice find for me.
However... after now having worked more closely with MTP as we start a strong push on our solution to migrate from NET472 to NET9 and add UI and API test projects (higher level test project types, basically), it became increasingly frustrating to see how barebones and limited most of the abstractions around MTP really are under the hood.
This is supposed to be a discussion/rant post based on my recent experience and hopefully it will have enough constructive criticism to steer some of these decisions in what I think would be a better direction.
Reinventing the wheel (and the entire universe while we are at it...)
I was super hopefull when I saw the TestApplicationBuilder class and how the "manual" Program.cs looked like.
"This really looks like they are following the modern pattern established for other application types, nice!"
But as soon as I actually started working with it, I realized how wrong I was. Starting with that IConfiguration abomination.
My first instinct:
"oh this is cool, so they expose
builder.Configurationthe same way other apps do. I'll just attach myAzure AppConfigurationprovider here and..."
"...wait a second.... I can't call
AddAzureAppConfigurationhere..... oh...... WAIT...... this is a differentIConfigurationtype?"
So we have the first wheel reinvented right there. A completely custom IConfiguration, that has like 5% of the capabilities of the original, and of course doesn't work at all with all existing providers.
Related:
Then I saw there is no builder.Services... weird. Surely I'm missing something.
And that's when everything clicked together: the entire thing is fake. Starting from TestApplicationBuilder down to all of its properties, everything was custom built and doesn't use any of the Microsoft.Extensions.* packages.
Why on earth would you even think about doing something like this! Just use the thing you already have abstracted that is super general purpose and battle-tested! It is absolutely insane to me that you'd recreate your own.
The consequence of this is that of course, that whole Program.cs is now completely useless to me. I can't register my services, I can't register my configuration providers, I can't tap into anything else that is standard Microsoft.Extensions.Hosting. I can't add OpenTelemetry, no hosted services, nothing.
It is a useless barely functional "full rewrite" of Microsoft.Extensions.Hosting that exposes a few convoluted interfaces for library authors only.
"All right.... I think I get this now. Let's try to make
testconfig.jsonload environment variables now..."
And suddenly I'm dealing with another config file that was introduced to..... configure things... of course. Is it following the same pattern of appsettings.json? Of course not! It is fully custom!
Now we have a billion mechanisms for providing parameters and configuration settings:
launchSettings.json: doesn't work properly with VS test integration. only applies to development regardless. currently uselessrunsettings: deprecated, apparently? fair enoughtestconfig.jsona new thing... replacingrunsettings, but of course, dropping a bunch of capabilitiesxunit.runner.jsonbecause why not have one custom made for a specific test framework? After all we are dealing with raw Json which is highly proprietary and not portable at all..... wait....appsettings.{env}.json: nah, tests won't use this by default. why would they? its just a general configuration abstraction that is used in all other project types... surely there is no reason at all to tap into it... right?dotnetCLI- and of course, you have a custom MSBuild property where you can set cmd arguments as well... very discoverable, super idiomatic, great stuff
"But if you are using the MSBuild nuget package, testconfig.json is automatically copied and renamed to {application}.testconfig.json". Of course..... a random NuGet package, and not a project SDK like all other project types. Great idea guys.
But it doesn't stop there. You want to create an extension? Sure... just declare a random static method and then create a MSBuild property pointing to it in the target project. Surely super intuitive to do, particularly when the extension is not a NuGet package but a shared library in your solution. Very idiomatic, just ask people to add completely random custom properties and values to their csproj! Talk about ease of adding extensions!
Imagine if there was a hosting mechanism where you could..... add things, and configure them! That would be amazing, right? Nah... let's rely on extremely obscure MSBuild targets buried in transient package dependencies to automatically generate a bunch of code and hide that as much as possible from users! Those dumb users, they wouldn't be able to ever configure this themselves, we should hide all of it!
You know... the same way that ASP.NET Core and Worker projects hide all of their hosting and... no.... they don't do that? Oh... whatever, we will do it anyways!
Suggestions
Guys...... what on earth are you doing here? You have such a strong abstraction to build on top of, and you ditch all of it in favor of.... this mess?
Stop hiding host details from engineers
Just expose Program.cs. No more MSBuild shenanigans to "make it look like the old projects". That's completely useless. .NET engineers are used to the new hosting model for years now. They will be able to handle a new Program.cs file with 3 lines of setup, no problem.
All the MSBuild-based magic is insane right now.
Drop all this custom config nonsense
.NET already has an idiomatic configuration subsystem on Microsoft.Extensions.Configuration. All configuration should be through this, period. Drop this custom testconfig.json nonsense, use the standard appsettings.{env}.json, env vars, secrets providers. Tap into the existing IConfiguration interface. Encourage all framework authors to use it.
Libraries for anything else in the .NET ecosystem have extensions for being configured with the hosting model, usually via AddX methods. Those methods then rely on IOptions<T> to propagate configuration settings, that can be optionally bound to one of the many configuration providers (appsettings.json etc) or just be configured inline).
Just do that! AddXunit(XunitOptions...), AddHotReloadWhatever(HotReloadWhateverOptions...).
Again, get rid of the insane MSBuild-based magic and all that IExtension nonsense you invented.
Everything is DI-based
I don't want to have to use Xunit.DependencyInjection. I especially don't want to do it when the platform itself already has a hosting mechanism and even has a IServiceProvider.
Final Thoughts
This framework is an abomination. I had reservations before (as evidenced by that first issue I linked) but now that I've actually worked with it a bit more they became orders of magnitude worse.
I really really hope you'll reconsider (dramatically) some of the decisions you made thus far here. There is still time, this framework is brand new. Do thinks right, please.
I'll now go back to our new hodgepodge of a project with multiple workarounds and limitations.
- We use the
.WorkerSDK so thatappsettings.{env}.jsonare copied automatically. Not intuitive at all, but it works... - We force MTP runner via the
UseMicrosoftTestingPlatformRunnerMSBuild property (this has no reason to exist) - We now maintain a custom envvar provider extension to be able to define environment variables (including
DOTNET_ENVIRONMENT) when running the tests. Of courselaunchSettings.jsondoesn't work, why would it? That would be too easy - We depend on
Xunit.DependencyInjection, which itself uses the old callback-based host builder, because of limitations with xunit. If we want DI with another framework, well... we are screwed: this is xunit-specific, after all. Imagine if there was a central place where DI could be setup regardless of underlying test framework... wouldn't that be nice? - We configure xunit settings, MTP settings, Azure settings, Playwright settings, and domain-specific settings, all using different mechanisms