[{"content":"In a previous blog post, I introduced the NuGet package Nwwz.Mvc.Testing. This package was designed to make it possible to make the WebApplicationFactory listen on a rela port to that you can use it with Playwright.\nThe package implements a different WebApplicationFactory that spin up a Kestrel instance instead ot the in-memory TestServer.\nWith the release of .NET 10, this workaround is no longer necessary. Microsoft has introduced a native way to run WebApplicationFactory using Kestrel.\nWebApplicationFactory.UseKestrel() In .NET 10, the WebApplicationFactory has been enhanced with a new method: UseKestrel(). This method tells the factory to use a real Kestrel server instead of the in-memory TestServer.\nThis is exactly what Nwwz.Mvc.Testing aimed to do, but now it\u0026rsquo;s built right into the framework.\nHow to use it Setting it up is now incredibly straightforward. You no longer need any custom packages or complex CreateHost overrides. Jus use the new UseKestrel() and StartServer():\nAssuming you have the default dotnet Weatherforcast WebApi project under test. You now can do this:\npublic class TestWithRealPort : IClassFixture\u0026lt;WebApplicationFactory\u0026lt;Program\u0026gt;\u0026gt; { private readonly WebApplicationFactory\u0026lt;Program\u0026gt; _factory; public TestWithRealPort(WebApplicationFactory\u0026lt;Program\u0026gt; factory) { _factory = factory; _factory.UseKestrel(); _factory.StartServer(); } [Fact] public async Task Get_EndpointsReturnSuccessAndCorrectContentType() { // Arrange var playwright = await Playwright.CreateAsync(); var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = false, // To see something happening SlowMo = 2000 // Slow it down a bit so you can read along }); var context = await browser.NewContextAsync(); var page = await context.NewPageAsync(); var baseAddress = _factory.ClientOptions.BaseAddress; // Act await page.GotoAsync(baseAddress + \u0026#34;weatherforecast\u0026#34;); // Assert Assert.Contains(\u0026#34;temperatureC\u0026#34;, await page.ContentAsync()); } } For a complete example go to: https://github.com/netwatwezoeken/full-integration-testing/tree/main\nNwwz.Mvc.Testing is now obsolete Since this functionality is now part of the standard .NET library, Nwwz.Mvc.Testing is obsolete. As of version 1.1.0 it will give you a warning when using it in .NET 10.\nI highly recommend migrating to .NET 10, and using the native UseKestrel() implementation. The migration should be minimal, as the concepts are identical.\nHappy testing!\n","permalink":"https://blog.netwatwezoeken.nl/posts/integration-testing-with-playwright-just-got-a-lot-easier/","summary":"\u003cp\u003eIn a \u003ca href=\"/simplifying-integration-testing-with-dotnet-and-playwright/\"\u003eprevious blog post\u003c/a\u003e, I introduced the NuGet package \u003ccode\u003eNwwz.Mvc.Testing\u003c/code\u003e. This package was designed to make it possible to make the \u003ccode\u003eWebApplicationFactory\u003c/code\u003e listen on a rela port to that you can use it with Playwright.\u003c/p\u003e\n\u003cp\u003eThe package implements a different \u003ccode\u003eWebApplicationFactory\u003c/code\u003e that spin up a Kestrel instance instead ot the in-memory \u003ccode\u003eTestServer\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eWith the release of \u003cstrong\u003e.NET 10\u003c/strong\u003e, this workaround is no longer necessary. Microsoft has introduced a native way to run \u003ccode\u003eWebApplicationFactory\u003c/code\u003e using Kestrel.\u003c/p\u003e","title":"Integration testing with Playwright just got easier"},{"content":"The Strangler Fig pattern is one of those transitional architecture patterns to get to a better system. But strangling SOAP is a bit difficult. If you just put a proxy in front of a SOAP service, you’ll quickly bump into a painful detail: SOAP typically exposes a single HTTP endpoint. All operations share one path, and the actual method is picked using headers like SOAPAction or action and the body contents. That makes the usual reverse‑proxy trick an \u0026ldquo;all or nothing\u0026rdquo; mechanism. Not at all a Strangler Fig approach. But there is a way!\nThis blog shows a pragmatic Strangler Fig approach for SOAP. Host one modern .NET application that selectively handles the operations are migrated in modern .NET, and transparently forwards all other operations to the legacy service (for example .NET Framework). This approach prevents changes on changing clients, WSDL URL, or endpoint path while still moving towards modern .NET. This blog also includes the use of SoapCore, to implement SOAP in modern .NET, and OpenTelemetry for monitoring. The mechanism allows for the move from .NET Framework to modern .NET without breaking clients. Then once in .NET you might also decide to migrate away from SOAP altogether. That subject is not covered in this article.\nFind the example code on GitHub.\nWhy a generic proxy (like YARP) is not the answer here YARP is excellent for HTTP routing, but SOAP concentrates all operations behind a single URL. That creates a few problems for a “drop-in proxy” approach:\nSingle path routing: There’s no per-operation path to route on. Everything is a POST on the same endpoint. Operation is in headers/body: The router would need to parse SOAPAction or the SOAP envelope to decide per-call routing. YARP can match headers when you enumerate them up front, but you either: list every legacy operation (becoming a change tax for every migration step), or use a catch-all and can’t distinguish what should be handled locally vs forwarded. Net effect: a proxy alone won’t let you gradually “take over” operations inside a single SOAP endpoint while keeping WSDL and client expectations intact. You need a component that can be both a SOAP server (for migrated ops) and a transparent SOAP forwarder (for everything else).\nThe example legacy application The example legacy application is a simple .NET Framework SOAP service that exposes three operations (Greet, Add and Subtract) using WCF. This would be the application that you want to modernize.\nPatch for the examples legacy service in the project: /LegacyService\nFor those that cannot run .NET Framework (i.e., Mac and Linux), I have included a wiremock configuration to mock the legacy service (see /AppHost/LegacyMock).\nThe modernized service This solution hosts one ASP.NET Core process that:\nImplements a subset of the same contract IService. Intercepts incoming requests, inspects SOAPAction, and if the action is not implemented in the new code, it forwards the raw request to the legacy service. How it works (walkthrough) 1. Implement a SOAP server in modern .NET In NewService/Program.cs:\nbuilder.Services.AddSoapCore() .AddSoap(builder.Configuration, builder.Environment); ... ... app.UseSoapEndpoint\u0026lt;IService\u0026gt;(soapActionPath, new SoapEncoderOptions()); AddSoap(...) (from SoapCore) enables hosting a SOAP endpoint in this app. UseSoapEndpoint\u0026lt;IService\u0026gt; exposes the modern endpoint for the operations you’ve migrated. Notice that /NewService/Soap/IService.cs and /LegacyService/IService.cs are syntactically identical. This is great for migration: you can reuse the same implementation. To summarize, this is the SOAP server implementation in modern .NET. Making use of SoapCore. Big thanks to the SoapCore team for this great library!\n2. Falling back to the Legacy service In NewService/Program.cs:\nbuilder.Services.AddHttpClient(\u0026#34;ServiceClient\u0026#34;, client =\u0026gt; { client.BaseAddress = new Uri( Environment.GetEnvironmentVariable(\u0026#34;STRANGLED_SOAP_SERVICE\u0026#34;) ?? \u0026#34;http://localhost:5000/\u0026#34;); }); ... ... app.MapSoapFallback\u0026lt;IService\u0026gt;(soapActionPath, \u0026#34;ServiceClient\u0026#34;); app.UseSoapEndpoint\u0026lt;IService\u0026gt;(soapActionPath, new SoapEncoderOptions()); The fact that MapSoapFallback is before UseSoapEndpoint means that the fallback middleware is executed before the SOAP endpoint. This allows the fallback middleware to inspect the incoming SOAPAction and decide whether to forward the request to legacy or not.\nThat logic is implemented in NewService/SoapProxy.cs. It\nchecks which actions are implemented in the new service, inspects the incoming SOAPAction header, to either forward the request to legacy or handle it locally. To forward, it uses the \u0026ldquo;ServiceClient\u0026rdquo; HttpClient. The check on implementation is done by reflection on the IService contract. Any method marked with [OperationContract] is considered migrated. This use of reflection prevents you from having to maintain a list of migrated operations.\n⚠️ Note that the CallFallbackSoapService() in NewService/SoapProxy.cs is a naive implementation that might need further hardening for production use! ⚠️\n2. Observability with OpenTelemetry In NewService/OpenTelemetry.cs there is a straightforward OpenTelemetry configuration. This enables you to see any incoming and outgoing HTTP requests.\nAdditionally, in NewService/Program.cs there is a AddSoapActionTag() call that adds a tag to all incoming requests. This allows you to see which SOAP action a certain HTTP call is about.\nSee it in action Pre-requisites .NET 9 SDK Docker runtime or .NET Framework 4.8 runtime Start the app From the solution root, run the Aspire host: Rider/Visual Studio: set startup to AppHost and Run CLI: dotnet run --project AppHost/AppHost.csproj Wait until both resources are healthy: WireMock: http://localhost:5001 New service: https://localhost:7057. Optional: open the Aspire dashboard and click the legacy-service-mock links: Requests → see proxied SOAP calls arrive at legacy Mappings → inspect or tweak WireMock mappings When on Windows you can also decide to run the example lecagy application in .NET Framework 4.8 from /LegacyService/LegacyService.csproj. Then in AppHost/Apphost.cs comment the two marked lines. Running the example in this mode does not require Docker.\nMake a few SOAP calls There’s a ready-to-run HTTP scratch file: AppHost/EndpointTesting.http. Adjust the @host variable to match your new-service URL if it’s not https://localhost:7057.\nMigrated operation: Greet (handled by the new .NET service) POST {{host}}/Service.svc Content-Type: text/xml; charset=UTF-8 SOAPAction: http://netwatwezoeken.nl/IService/Greet \u0026lt;s:Envelope xmlns:s=\u0026#34;http://schemas.xmlsoap.org/soap/envelope/\u0026#34;\u0026gt; \u0026lt;s:Body\u0026gt; \u0026lt;Greet xmlns=\u0026#34;http://netwatwezoeken.nl\u0026#34;\u0026gt; \u0026lt;input\u0026gt; \u0026lt;FirstName\u0026gt;Jos\u0026lt;/FirstName\u0026gt; \u0026lt;LastName\u0026gt;Hendriks\u0026lt;/LastName\u0026gt; \u0026lt;/input\u0026gt; \u0026lt;/Greet\u0026gt; \u0026lt;/s:Body\u0026gt; \u0026lt;/s:Envelope\u0026gt; Run it and expect a friendly response from the modern service (Hello Jos, this is modern .NET). In the Aspire Traces you should see a single span for the SOAP call: Notice that the soap action is visible in the trace.\nLegacy operation: Add (forwarded to WireMock) POST {{host}}/Service.svc Content-Type: text/xml; charset=UTF-8 SOAPAction: http://netwatwezoeken.nl/IService/Add \u0026lt;s:Envelope xmlns:s=\u0026#34;http://schemas.xmlsoap.org/soap/envelope/\u0026#34;\u0026gt; \u0026lt;s:Body\u0026gt; \u0026lt;Add xmlns=\u0026#34;http://netwatwezoeken.nl\u0026#34;\u0026gt; \u0026lt;a\u0026gt;1\u0026lt;/a\u0026gt; \u0026lt;b\u0026gt;3\u0026lt;/b\u0026gt; \u0026lt;/Add\u0026gt; \u0026lt;/s:Body\u0026gt; \u0026lt;/s:Envelope\u0026gt; Run it and expect a valid SOAP response coming from the legacy mock. In the Aspire Traces you should see a trace with two spans for the SOAP call: Notice that the .NET service has delegated the call to the legacy service.\nSummary You can migrate SOAP safely to modern .NET. One operation at a time while clients keep calling the same endpoint. The modern service serves calls to new operations; legacy still serves all others. No coordination needed with consumers. Thanks to SoapCore implementation of SOAP in modern .NET, this is a breeze. And with OpenTelemetry, you can see the SOAP calls and measure the impact of the migration.\nThis is the essence of the Strangler Fig pattern: the new tree grows around the old one, taking over function by function until the legacy can be retired.\nNotes NewService/SoapProxy.cs is not production hardened. Make adjustments accordingly before using this in production. Some clients send quoted actions or use SOAP 1.2 action parameter. The sample focuses on SOAP 1.1 SOAPAction; extend the detector if you need both. ","permalink":"https://blog.netwatwezoeken.nl/posts/strangling-soap/","summary":"\u003cp\u003eThe Strangler Fig pattern is one of those transitional architecture patterns to get to a better system. But strangling SOAP is a bit difficult. If you just put a proxy in front of a SOAP service, you’ll quickly bump into a painful detail: SOAP typically exposes a single HTTP endpoint. All operations share one path, and the actual method is picked using headers like \u003ccode\u003eSOAPAction\u003c/code\u003e or \u003ccode\u003eaction\u003c/code\u003e and the body contents. That makes the usual reverse‑proxy trick an \u0026ldquo;all or nothing\u0026rdquo; mechanism. Not at all a Strangler Fig approach. But there is a way!\u003c/p\u003e","title":"Strangling SOAP: migrate to modern .NET without breaking clients"},{"content":"One of the recent additions in .NET is that you can now have single file projects. In this article we\u0026rsquo;ll explore how to use Playwright in such file.\nTL;DR: Full example at the end of this page.\nWhy? More and more I use Playwright not only for testing but also for small automation scripts and to quickly make demos using it\u0026rsquo;s handy built-in recorder. Before .NET 10 that would required a C# project, a directory with a csproj file a cs file and the Playwright package installed.\nHow does dotnet run apps.cs work? It\u0026rsquo;s straightforward, create a single file like below and run it using dotnet run app.cs, or whatever name you gave the file.\nConsole.WriteLine(\u0026#34;Hello World!\u0026#34;); For simplicity you can also omit run and just type dotnet app.cs.\nUsing Playwright in a single file app To use Playwirght you will nee the package installed though. Since there is not csproj there now is a different way to do so. Use #:package Microsoft.Playwright@1.56.0 to specify the package and version.\n#:package Microsoft.Playwright@1.56.0 using Microsoft.Playwright; using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Chromium.LaunchAsync( new BrowserTypeLaunchOptions { Headless = false }); But above code will not run. It will result in a System.InvalidOperationException because Reflection-based serialization has been disabled for this applicatio. The reason is that Playwright uses reflection to serialize the browser object and that by default AOT is enabled for single files apps. We need one more line to disable AOT:\n#:property PublishAot=false #:package Microsoft.Playwright@1.56.0 using Microsoft.Playwright; using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Chromium.LaunchAsync( new BrowserTypeLaunchOptions { Headless = false }); Now is you did not have Playwright 1.56.0 installed before you will get thi error:\nUnhandled exception. Microsoft.Playwright.PlaywrightException: Executable doesn\u0026#39;t exist at /Users/jhendriks/Library/Caches/ms-playwright/chromium-1194/chrome-mac/Chromium.app/Contents/MacOS/Chromium ╔════════════════════════════════════════════════════════════╗ ║ Looks like Playwright was just installed or updated. ║ ║ Please run the following command to download new browsers: ║ ║ ║ ║ pwsh bin/Debug/netX/playwright.ps1 install ║ ║ ║ ║ \u0026lt;3 Playwright Team ║ ╚════════════════════════════════════════════════════════════╝ at Microsoft.Playwright.Transport.Connection.InnerSendMessageToServerAsync[T](ChannelOwner object, String method, Dictionary`2 dictionary, Boolean keepNulls) in /_/src/Playwright/Transport/Connection.cs:line 201 at Microsoft.Playwright.Transport.Connection.WrapApiCallAsync[T](Func`1 action, Boolean isInternal, String title) in /_/src/Playwright/Transport/Connection.cs:line 499 at Microsoft.Playwright.Core.BrowserType.LaunchAsync(BrowserTypeLaunchOptions options) in /_/src/Playwright/Core/BrowserType.cs:line 56 at Program.\u0026lt;Main\u0026gt;$(String[] args) at Program.\u0026lt;Main\u0026gt;(String[] args) Luckily Playwright also allows you to install browsers through the API:\n#:property PublishAot=false #:package Microsoft.Playwright@1.56.0 using Microsoft.Playwright; var exitCode = Microsoft.Playwright.Program.Main(new[] { \u0026#34;install\u0026#34; }); if (exitCode != 0) throw new Exception(\u0026#34;Failed to install playwright\u0026#34;); using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Chromium.LaunchAsync( new BrowserTypeLaunchOptions { Headless = false }); Then finally you start adding your Playwright instructions.\nFull example Here is an example on who you can use the recorder easily create a video of your browser interaction:\n","permalink":"https://blog.netwatwezoeken.nl/posts/playwright-and-single-file-dotnet/","summary":"\u003cp\u003eOne of the recent additions in .NET is that you can now have single file projects. In this article we\u0026rsquo;ll explore how to use Playwright in such file.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Playwright and dotnet run app.cs\" loading=\"lazy\" src=\"/assets/images/2025/11/Playwright-dotnet-run-app2.png\"\u003e\u003c/p\u003e\n\u003cp\u003eTL;DR: Full example at the end of this page.\u003c/p\u003e\n\u003ch2 id=\"why\"\u003eWhy?\u003c/h2\u003e\n\u003cp\u003eMore and more I use Playwright not only for testing but also for small automation scripts and to quickly make demos using it\u0026rsquo;s handy built-in recorder. Before .NET 10 that would required a C# project, a directory with a \u003ccode\u003ecsproj\u003c/code\u003e file a \u003ccode\u003ecs\u003c/code\u003e file and the Playwright package installed.\u003c/p\u003e","title":"Playwright in a single file dotnet app"},{"content":"Gáspár Nagy dropped a few interesting ideas and thoughts during the keynote of Living Documentation Event 2025. One of them was about testing and clean architecture, Or hexagonical-, union- or port and adaptors- architecture for that matter. Here is a quick recap of what clean architecture is about.\nThe core concept is that all business rules and logic are encoded in Application \u0026amp; Domain. The layers Api / UI, sometimes called Presentation, and Infra (Infrastructure) keep the core clean from external influence like user interfaces, database details and such. The goal is that the core stays clean from all kinds of implementation details that do not contribute to actual business logic.\nGáspár Nagy pointed out that Test could or berhaps should be a first-class citizen of the clean architecture as depicted below.\nThen later in a Reqnroll workshop by Bas Dijkstra and Gáspár I was reminded that the use of BDD style test using Gherkin (Given\u0026hellip; When\u0026hellip; Then\u0026hellip;) only makes sense when you actually treat them as specifications. Because that is what it is a way to make specifications executable. If it is just about testing then there are probably easier ways to write them without the need of the extra complexity that Gherkin brings. It is a good reminder because I sometimes tend to get a bit carried away with Reqnroll\u0026hellip;..\nDuring that same workshop some people asked why anyone would actually need Reqnroll if there are already so many other tools. Why would you bother having another tool? For me that was a simple answer: \u0026ldquo;I work mostly in .NET and to get things adopted quickly it really helps that underlying technology is known already.\u0026rdquo;. So as a developer I understand that every language needs som sort of BDD tool. But from a testing (or actually specification) point of view it might not be so obvious to have all these different tools on the market. And I under stand that why should you use Reqnroll if you are happily using Cucumber to call an API and asserts the results? There is no good reason to your BDD framework to be the same as the technology as the software under test if all you do is JSON over HTTP.\nHow it all falls together And then I realized. You do need Reqnroll if the software under test is written in C#! It is all about having specifications that are executable. I believe that when executing clean architecture properly, then most of the specifications (e.g. business logic) is encoded in the core (Application \u0026amp; Domain). Usually quite some effort is spent into keeping the core clean by making sure that an API call or the UI follow the same C# call into the core logic for example. This means that such calls should allow specifications to be executed without the need of any UI, Api or underlying Database. And to do so you do need a BDD framework that is able to make those C# calls! Also this very much makes these test very much a first-class citizen of the architecture. Any specification that cannot be checked in that manner might be feedback on how well the clean architecture is executed.\nSure you still need to assure that the whole thing works in an integrated way. But testing and specification checking should absolutely be an integral part of the architecture.\nI help organisations and developers to build better software faster. Feel free to reach out if you want to know more.\n","permalink":"https://blog.netwatwezoeken.nl/posts/testing-as-first-class-architecture-citizen/","summary":"\u003cp\u003eGáspár Nagy dropped a few interesting ideas and thoughts during the keynote of Living Documentation Event 2025. One of them was about testing and clean architecture, Or hexagonical-, union- or port and adaptors- architecture for that matter. Here is a quick recap of what clean architecture is about.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"clean architecture\" loading=\"lazy\" src=\"/assets/images/2025/04/clean-arch.svg\"\u003e\u003c/p\u003e\n\u003cp\u003eThe core concept is that all business rules and logic are encoded in \u003ccode\u003eApplication \u0026amp; Domain\u003c/code\u003e. The layers \u003ccode\u003eApi / UI\u003c/code\u003e, sometimes called Presentation, and \u003ccode\u003eInfra\u003c/code\u003e (Infrastructure) keep the core clean from external influence like user interfaces, database details and such. The goal is that the core stays clean from all kinds of implementation details that do not contribute to actual business logic.\u003c/p\u003e","title":"Testing as a First-Class Citizen of Architecture"},{"content":"In my earlier blog, Integration Testing with Playwright and Testcontainers, I shared how to use a WebApplicationFactory with TestContainers and Playwright to set up integration tests. While this approach works well, I encountered a problem: the setup inadvertently spins up two separate hosts, leading to unwanted behaviour.\nNwwz.Mvc.Testing I am introducing NuGet package Nwwz.Mvc.Testing to solve those issue.It\u0026rsquo;s a simple helper to easily setup a .NET application fro integration testing while allowing a browser to connect to it.\nThe problem The solution from my previous blog relies on the use of a CustomWebApplicationFactory with some additional code in it to allow the browser to access the application under test:\nprotected override IHost CreateHost(IHostBuilder builder) { // Create a plain host that we can return. var dummyHost = builder.Build(); // Configure and start the actual host. builder.ConfigureWebHost(webHostBuilder =\u0026gt; webHostBuilder.UseKestrel()); var host = builder.Build(); host.Start(); return dummyHost; } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseUrls(\u0026#34;https://localhost:7048\u0026#34;); .... } In short this means that not one but two hosts are created. One TestServer which is baked into Microsoft\u0026rsquo;s WebApplicationFactory and a normal IServer due to the Createhost() that uses Kestrel internally. While this might be an issue from resource perspective this also two other side effects.\nConfigureWebHost is called twice Both TestServer and Createhost() make a call to ConfigureWebHost(). This means that that method is called twice. So if there are things in there that cannot run twice, that will be a problem.\nCreateClient() CreateClient() returns a HttpClient that allows interaction with the host created by TestServer, while the Kestrel host listens on the network. These hosts don\u0026rsquo;t share anything so data that is changed via the API using the HttpClient is not visible in the UI that is accessed through the Kestrel endpoint.\nAlthough the issue with CreateClient() can be solved as describe in this article described in this article. That solution still instantiates a TestServer.\nSolution The solution is to have a WebApplicationFactory that spins up Kestrel and does not spin up a TestServer. That is exactly what Nwwz.Mvc.Testing does.\nThis package provides an alternative implementation of Microsoft\u0026rsquo;s WebApplicationFactory. It makes a single call to ConfigureWebHost() and starts Kestrel to expose the application to the network. CreateClient() still returns a client that connects to the application under test. Additionally the server address is now exposed via Property ServerAddress for it to be used by frameworks like Playwright.\nTo use simply install the package: dotnet add package Nwwz.Mvc.Testing\nThe code is forked from Microsoft.AspNetCore.Mvc.Testing and can be found at https://github.com/netwatwezoeken/Mvc.Testing\nI have also updated the example that I used before: https://github.com/netwatwezoeken/full-integration-testing/tree/nwwz-mvc-testing\nCompatible with additional features This new packages is completely compatible with Microsoft.AspNetCore.Mvc.Testing. The derived class that you might already have (e.g. a CustomWebApplicationFActory) still works and CreateClient() will still return a client that connects to the application under test.\nRandom port Each time Kestrel is started an available port is selected. This allows parallelization of tests.\nHttps By default https is used. To disable https and use http instead set \u0026lsquo;UseHttps\u0026rsquo; to false:\ninternal class CustomWebApplicationFactory : Nwwz.Mvc.Testing.WebApplicationFactory\u0026lt;Program\u0026gt; { public CustomWebApplicationFactory() { UseHttps = false; } } Help, remarks and questions As always I\u0026rsquo;m happy to to answer questions and get feedback.\nI help organisations and developers to build better software faster. Feel free to reach out if you want to know more.\n","permalink":"https://blog.netwatwezoeken.nl/posts/simplifying-integration-testing-with-dotnet-and-playwright/","summary":"\u003cp\u003eIn my earlier blog, \u003ca href=\"https://blog.netwatwezoeken.nl/integration-testing-with-playwright-and-testcontainers/\"\u003eIntegration Testing with Playwright and Testcontainers\u003c/a\u003e, I shared how to use a WebApplicationFactory with TestContainers and Playwright to set up integration tests. While this approach works well, I encountered a problem: the setup inadvertently spins up two separate hosts, leading to unwanted behaviour.\u003c/p\u003e\n\u003ch2 id=\"nwwzmvctesting\"\u003eNwwz.Mvc.Testing\u003c/h2\u003e\n\u003cp\u003eI am introducing NuGet package \u003ca href=\"https://www.nuget.org/packages/Nwwz.Mvc.Testing\"\u003eNwwz.Mvc.Testing\u003c/a\u003e to solve those issue.It\u0026rsquo;s a simple helper to easily setup a .NET application fro integration testing while allowing a browser to connect to it.\u003c/p\u003e","title":"Simplifying integration testing with .NET and Playwright"},{"content":"In my last blog I wrote about why mutation testing is a better way of measuring test completeness. I also mentioned the drawback regarding performance.\nIn this blog I\u0026rsquo;d like to discuss what Stryker does to increase performance and the things you can do in your pipeline to get the most out of mutations testing every check-in.\nWhat stryker already does Concurrency By default Stryker takes half of the available cores to do the work. Use the concurrency to use more or less cores of you system. I tested this on a 8 core machine. When using 4 core the whole process takes 02:42. Whit 8 cores that same process took almost 30 seconds less, 02:18.\nUtilise code coverage Initially Stryker runs all test and measures the code coverage for each test. It does so to do 2 things:\nAny code that is uncovered does not need their mutations tested. Those mutants will surely survive. No need to spend CPU cycles on that. Stryker keeps track of which tests actually cover which code. This means that it knows exactly which test to run per mutation made. That\u0026rsquo;s a lot more efficient than running all tests for each mutation. This is also a reminder to be sure to make tests to be as specific as possible. A couple of tests that hit a specific piece of functionality (and thus code) will run through mutations faster than larger tests that all hit a large piece of functionality and code.\nStryker does provide a option to tune the way it measures coverage.\nI might be worthwhile to experiment with this. On one particular piece of code i tried perTestInIsolation was slower than perTest. The analysis took 3,5 minute (InIsolation) instead of 20 seconds. In the execution it only saved 14 seconds. Most likely due to the fact that the more thorough analysis discovered more parts of code that were actually uncovered.\nMy best guess is that with \u0026rsquo;lower\u0026rsquo; scores perTestInIsolation takes less time in total. Please do the experiment on your own code to determine the best setting in your context.\nSince Stryker can be configured to only mutate changed code. Using Since you can have Stryker to only include changes since an certain commit-isch (i.e. a branch, a tag or commit) that are made. By default it will use the master branch. The minimal requirement is to enable it in the config. Here is an example of minimal config that relies on main as the default branch.\n{ \u0026#34;stryker-config\u0026#34;: { \u0026#34;thresholds\u0026#34;: { \u0026#34;high\u0026#34;: 100, \u0026#34;low\u0026#34;: 100, \u0026#34;break\u0026#34;: 100 }, \u0026#34;since\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;target\u0026#34;: \u0026#34;main\u0026#34;, \u0026#34;ignore-changes-in\u0026#34;: [\u0026#34;**/*stryker-config.json\u0026#34;] } } } Since does come with a drawback. Stryker will only report about the new code.\nBaseline Baseline is similar to since. With the addition that it does generate a full report. It is still experimental and you do need a place to store the baseline.\nAn efficient pipeline To get fast feedback double check how many cores are available to the process that runs your pipeline and to set the concurrency accordingly. You might even decide to invest in more cores for that pipeline.\nWhen using feature branches you could use since to compare a feature branch to the target branch. Then on your target branch run a full analysis on each merge.\nI greatly prefer trunk based development where each push to main triggers the pipeline. So ideally I want my pipeline to compare against the previous pipeline run. This is not necessarily the previous commit though. I think this can only be achieved by using Stryker\u0026rsquo;s experimental baseline functionality. A less sophisticated solution is to just assume a single commit per push. This allows you to use the parent commit of the current commit that is built to pass to to the stryker command, like so:\ndotnet stryker -f stryker-config.json --since:82d8b49fb871b1cbe5d5027c0275f76da00b78b5 Remember that since does not generate full reports. To get these you could add a pipeline that runs once every day to get the full report.\nAnother approach (which works for both feature branches and trunk based) is to split pipelines so that you can enjoy deploy to a test environment quickly and still get the full mutation results later. For my previous client that is exactly what we did, see figure below.\n%%{init: {\u0026#39;theme\u0026#39;:\u0026#39;dark\u0026#39;}}%% flowchart TD A[git push] --\u0026gt; B(unit tests) A[git push] --\u0026gt; M{mutation testing} subgraph pipeline 1 B --\u0026gt; C(build releasable package) end subgraph pipeline 2 M end C --\u0026gt; D(be able to deploy) C --\u0026gt; Z[feedback complete] M --\u0026gt; Z On each commit a pipeline is triggered which executes all tests, in parallel triggers the mutation pipeline and then does a release build. The result is that we are able to quickly deploy to a test environment and still get all of the feedback a later.\nYet another approach might be to use since in your every day commits and run a full mutation test every night.\nConclusion It\u0026rsquo;s a balance between the speed, quality and costs\nBe sure to have small ans specific tests Use all your cores and optionally even invest Experiment with the how Stryker should measure code coverage Use since or baseline to only mutate new code Be creative with pipelines to get the feedback that suits you best at the moment it suits you best ","permalink":"https://blog.netwatwezoeken.nl/posts/using-stryker-a-ci-cd-pipeline/","summary":"\u003cp\u003eIn \u003ca href=\"/mutation-testing-ftw/\"\u003emy last blog\u003c/a\u003e I wrote about why mutation testing is a better way of measuring test completeness.\nI also mentioned the drawback regarding performance.\u003c/p\u003e\n\u003cp\u003eIn this blog I\u0026rsquo;d like to discuss what Stryker does to increase performance and the things you can do in your pipeline to get the most out of mutations testing every check-in.\u003c/p\u003e\n\u003ch2 id=\"what-stryker-already-does\"\u003eWhat stryker already does\u003c/h2\u003e\n\u003ch3 id=\"concurrency\"\u003eConcurrency\u003c/h3\u003e\n\u003cp\u003eBy default Stryker takes half of the available cores to do the work. Use the concurrency to use more or less cores of you system. I tested this on a 8 core machine. When using 4 core the whole process takes 02:42. Whit 8 cores that same process took almost 30 seconds less, 02:18.\u003c/p\u003e","title":"Stryker in a CI/CD pipeline"},{"content":"It is pretty common to use some sort of code coverage measurement tool along with unit tests to gather feedback on completeness of your test suite. Measuring code coverage is great start to find out if your code needs more testing. But by it\u0026rsquo;s nature it also comes with some caveats. In this article I\u0026rsquo;ll explain these caveats and how mutation testing fills these caps and turns out more valuable as a source of feedback.\nA limitation of code coverage Code coverage measurement works pretty straight forward. When a test runs a records is kep on which lines and statements were hit during that test execution. That mechanism might leave some unnoticeable gaps though.\nLet\u0026rsquo;s take the following Person class. It has a single method to determine if the person is an adult or not.\npublic class Person(int age) { public bool IsAdult() { if (age \u0026gt;= 18) { return true; } return false; } } To test that code you could write something like below. One test that results in true and one that results in false. This covers all possible outcomes.\npublic class PersonTest { [Theory] [InlineData(19, true)] [InlineData(17, false)] public void Adultlogic(int age, bool expected) { // Arrange var sut = new Person(age); // Act var actual = sut.IsAdult(); // Assert Assert.Equal(expected, actual); } } Great success! Running this test results in a perfect coverage score of 100%.\nSo all is good, right? Well mostly\u0026hellip;.. What about the edge case? In case you did your Boundary-value analysis you would have added that test in the first place. Testing with value 18 is probably something really obvious to the more experienced developer. But what if the logic is more complex? The code coverage measurement was clearly not 100% watertight here.\nLack of assertions Sometimes reality can be even a bit worse though. When software and the test grows more complex you might even end up with a test case that even lacks the assertion. Or Asserts the wrong thing due to human error. Hard to imagine this happening with this simple code, but I\u0026rsquo;m sure we have all seen a a test or two that actually did not test anything at all. Just like the following test. Someone messed up the Assert.\npublic class PersonTest { [Theory] [InlineData(19, true)] [InlineData(17, false)] public void Adultlogic(int age, bool expected) { // Arrange var sut = new Person(age); // Act var actual = sut.IsAdult(); // Assert Assert.Equal(actual, actual); } } Again, the measured code coverage is 100%. All code covered!\nMutants to the rescue Mutation testing can help here. The idea is simple any change to code should make at least one test fail. If it does not it indicates a gap in the testsuite. So we nee mutants, to then kill them\u0026hellip;.\nOne popular tool to do this automatically is Stryker. Named after William Stryker from the X-MEN, Known for his extreme intolerance towards mutants.\nTo run this toll on the above code be sure to install it run: dotnet tool install -g dotnet-stryker or add it to your tool manifest.\nWith this example default setting are fine. Which means the tool can be executed with: dotnet stryker from within the unittest project folder.\nBy default Stryker generates HTML output and gives some basic information on the commandline. Below are the most relevant parts of that output.\nAnalysis starting. ... ... Number of tests found: 2 for project /Users/jos/src/MutationAndCoverage/MutationAndCoverage/MutationAndCoverage.csproj. Initial test run started. 7 mutants created ... 2 mutants got status Ignored. Reason: Removed by block already covered filter 2 total mutants are skipped for the above mentioned reasons 5 total mutants will be tested ... ... Killed: 4 Survived: 1 Timeout: 0 ... ... The final mutation score is 80.00 % Stryker was able to define 5 that were actually worth to run against the test suite. And so it went ahead and ran the test suite once for every mutant created. Onne of those mutants survived! 4 out of 5 comes down to 80%. In other words, Stryker found a gap in our tests!\nThe HTML report provides better information. The report displays the code modification (the mutant) that survived the tests. \u0026lt;= could be changed to \u0026lt; without the tests detecting it. This directly points to the gap in our test. The edge case is not checked!\nThanks to mutation testing we now know what code coverage could not tell us. If you would run Stryker on with the tests that have the failing assertion (Assert.Equal(actual, actual);) then the different is even more dramatic. 100% vs 0%.\nConclusion Mutation testing gives feedback on completeness of the testsuite where code coverage only measures what code had been running during your tests. That is a big difference. The feedback from mutation testing is so much better than just code coverage that I don\u0026rsquo;t use code coverage anymore.\nThere is one downside though. It does take more time to calculate all those mutants an to run the whole test suite multiple times. I the next blog I will explain about the smart things Stryker already does for you and what you can do to efficiently use Stryker in your CI/CD pipeline.\n","permalink":"https://blog.netwatwezoeken.nl/posts/mutation-testing-ftw/","summary":"\u003cp\u003eIt is pretty common to use some sort of code coverage measurement tool along with unit tests to gather feedback on completeness of your test suite. Measuring code coverage is great start to find out if your code needs more testing. But by it\u0026rsquo;s nature it also comes with some caveats. In this article I\u0026rsquo;ll explain these caveats and how mutation testing fills these caps and turns out more valuable as a source of feedback.\u003c/p\u003e","title":"Use mutation testing to replace code coverage"},{"content":"With .NET 8 almost released it is time to make upgrading easier. In this blog I\u0026rsquo;ll explain about Directory.Build.props and Directory.Packages.props and how these can make upgrading easier.\nMost solutions consist of multiple projects. Even if all of your production code is in a single project chances are that your tests are in a different project. The separation that projects bring can be great, but sometimes managing dependencies can be a hassle.\nThe challenge with multiple csproj files In many cases a solutions consists of multiple projects like depicted below.\n└── solution/ ├── src/ │ ├── Main │ └── TheOtherProject └── tests/ ├── IntegrationTests └── UnitTests A typical csproj file contains a target framework, some project properties and some package references.\n\u0026lt;Project Sdk=\u0026#34;Microsoft.NET.Sdk.Web\u0026#34;\u0026gt; \u0026lt;PropertyGroup\u0026gt; \u0026lt;TargetFramework\u0026gt;net7.0\u0026lt;/TargetFramework\u0026gt; \u0026lt;Nullable\u0026gt;enable\u0026lt;/Nullable\u0026gt; \u0026lt;ImplicitUsings\u0026gt;enable\u0026lt;/ImplicitUsings\u0026gt; \u0026lt;TreatWarningsAsErrors\u0026gt;true\u0026lt;/TreatWarningsAsErrors\u0026gt; \u0026lt;RootNamespace\u0026gt;Main\u0026lt;/RootNamespace\u0026gt; \u0026lt;/PropertyGroup\u0026gt; \u0026lt;ItemGroup\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog\u0026#34; Version=\u0026#34;3.0.1\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Extensions.Logging\u0026#34; Version=\u0026#34;7.0.0\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Settings.Configuration\u0026#34; Version=\u0026#34;7.0.1\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Sinks.Console\u0026#34; Version=\u0026#34;4.1.0\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Sinks.File\u0026#34; Version=\u0026#34;5.0.0\u0026#34; /\u0026gt; \u0026lt;/ItemGroup\u0026gt; \u0026lt;ItemGroup\u0026gt; \u0026lt;ProjectReference Include=\u0026#34;..\\TheOtherProject.csproj\u0026#34; /\u0026gt; \u0026lt;/ItemGroup\u0026gt; \u0026lt;/Project\u0026gt; Each project defines it\u0026rsquo;s own dependencies and the versions of those dependencies. What happens when TheOtherProject also uses Serilog but it is still on version 2.12.0? Most likely you\u0026rsquo;ll want all of those versions to be the same. Even with a good IDE like Rider and Visual Studio this can be a bit of a hassle.\nA very similar problem migh arise when selecting the target framework. Chances are that you will have keep all of your projects within a solution aligned.\nCreating clean and consistent project files With .NET 6 Directory.Build.props and Directory.Packages.props were introduced. The names almost speak for themselves. These files contain directory wide build and packages properties. These files can be created in the top level of your solution folder. Typically placed where the sln file is stored.\nThings like TargetFramework and ImplicitUSings can be set in the Directory.Build.props as below.\n\u0026lt;Project\u0026gt; \u0026lt;PropertyGroup\u0026gt; \u0026lt;TargetFramework\u0026gt;net7.0\u0026lt;/TargetFramework\u0026gt; \u0026lt;ImplicitUsings\u0026gt;enable\u0026lt;/ImplicitUsings\u0026gt; \u0026lt;Nullable\u0026gt;enable\u0026lt;/Nullable\u0026gt; \u0026lt;TreatWarningsAsErrors\u0026gt;true\u0026lt;/TreatWarningsAsErrors\u0026gt; \u0026lt;/PropertyGroup\u0026gt; \u0026lt;/Project\u0026gt; Directory.Packages.props is straight forward as well:\n\u0026lt;Project\u0026gt; \u0026lt;PropertyGroup\u0026gt; \u0026lt;ManagePackageVersionsCentrally\u0026gt;true\u0026lt;/ManagePackageVersionsCentrally\u0026gt; \u0026lt;/PropertyGroup\u0026gt; \u0026lt;ItemGroup\u0026gt; \u0026lt;PackageVersion Include=\u0026#34;Serilog\u0026#34; Version=\u0026#34;3.0.1\u0026#34; /\u0026gt; \u0026lt;PackageVersion Include=\u0026#34;Serilog.Extensions.Logging\u0026#34; Version=\u0026#34;7.0.0\u0026#34; /\u0026gt; \u0026lt;PackageVersion Include=\u0026#34;Serilog.Settings.Configuration\u0026#34; Version=\u0026#34;7.0.1\u0026#34; /\u0026gt; \u0026lt;PackageVersion Include=\u0026#34;Serilog.Sinks.Console\u0026#34; Version=\u0026#34;4.1.0\u0026#34; /\u0026gt; \u0026lt;PackageVersion Include=\u0026#34;Serilog.Sinks.File\u0026#34; Version=\u0026#34;5.0.0\u0026#34; /\u0026gt; \u0026lt;/ItemGroup\u0026gt; \u0026lt;/Project\u0026gt; With these 2 files created Main.csproj can be cleaned up:\n\u0026lt;Project Sdk=\u0026#34;Microsoft.NET.Sdk.Web\u0026#34;\u0026gt; \u0026lt;PropertyGroup\u0026gt; \u0026lt;RootNamespace\u0026gt;Main\u0026lt;/RootNamespace\u0026gt; \u0026lt;/PropertyGroup\u0026gt; \u0026lt;ItemGroup\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Extensions.Logging\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Settings.Configuration\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Sinks.Console\u0026#34; /\u0026gt; \u0026lt;PackageReference Include=\u0026#34;Serilog.Sinks.File\u0026#34; /\u0026gt; \u0026lt;/ItemGroup\u0026gt; \u0026lt;ItemGroup\u0026gt; \u0026lt;ProjectReference Include=\u0026#34;..\\TheOtherProject.csproj\u0026#34; /\u0026gt; \u0026lt;/ItemGroup\u0026gt; \u0026lt;/Project\u0026gt; As can be seen the project file is now clear from any specific package or framework versions. These now depend on what is in the solution wide properties. That will aso make any package updates and framework updates consistent, by definition.\nInstalling packages Support for these property files is fully embedded in the dotnet ecosystem. For example, when running dotnet add package Serilog a terminal under solution/src/TheOtherProject/. Only \u0026ldquo;\u0026lt;PackageReference Include=\u0026quot;Serilog\u0026quot; /\u0026gt;\u0026rdquo; is added to TheOtherProject.csproj. Notice hte absence of a version number because it relies on what is in Directory.Packages.props already.\nThe same goes for added a packages that no other project hase reference yet. donet add will update TheOtherProject.csproj and in addition update Directory.Packages.props with the selected version.\nConclusion Use of Directory.Build.props and Directory.Packages.props make a solution just a bit easier toe maintain. This will make an upgrade from .NET 6 or 7 to .NET 8 less complicated. The best is that you can do this before .NET 8 is out and already enjoy better package versioning.\nIn my experience bot Rider and Visual Studio also deal with this solution wide properties well. But always check your commits as from now on a packages version in a csproj file is something to try and avoid.\n","permalink":"https://blog.netwatwezoeken.nl/posts/solution-wide-properties-in-dotnet/","summary":"\u003cp\u003eWith .NET 8 almost released it is time to make upgrading easier. In this blog I\u0026rsquo;ll explain about \u003ccode\u003eDirectory.Build.props\u003c/code\u003e and \u003ccode\u003eDirectory.Packages.props\u003c/code\u003e and how these can make upgrading easier.\u003c/p\u003e\n\u003cp\u003eMost solutions consist of multiple projects. Even if all of your production code is in a single project chances are that your tests are in a different project. The separation that projects bring can be great, but sometimes managing dependencies can be a hassle.\u003c/p\u003e","title":"Simplifying .NET solution upgrades with Directory.Build.props and Directory.Packages.props"},{"content":"Testing plays a crucial role in software development and we strive to test as much as possible using isolated unittests. However, there are times when these unittests fail to capture issues that arise when the entire application is tested. Such issues may include discrepancies between front-end and back-end implementation of APIs, or differences in behavior between real and mocked databases. These problems can be easily identified by running a series of scenarios that test the entire application, including the front-end, back-end, and database. Many developers resort to manual testing to achieve this, but automated testing offers greater repeatability and the ability to integrate checks into the development pipeline.\nThis article demonstrates how to easily automate the testing process using Playwright .NET and Testcontainers for .NET. By doing so, developers can eliminate the need for manual testing and streamline the testing process without relying on deployments.\nThe application under test For the purpose of this article I reused a blazor example contact application and replaced SQLite with SQL Server.\nThe result is a straight forward setup. A SQL Server database, a ASP.NET application and a Blazor frontend.\nUse a real database While local installation of a database is always an option there are better ways. Using the Testcontainers for .NET project you can easily setup a database from scratch for each test or testrun. You don\u0026rsquo;t need to have a custom script or a preinstalled database somewwhere when you have Docker.\nLet\u0026rsquo;s start with the assuption that a CustomWebApplicationFactory is is present.\nIn our CustomWebApplicationFactory we will add the definition of a real database.\ninternal class CustomWebApplicationFactory : WebApplicationFactory\u0026lt;Program\u0026gt; { public readonly MsSqlContainer DatabaseContainer; public FullIntegrationWebApplicationFactory() { try { DatabaseContainer = new MsSqlBuilder() .Build(); } catch (ArgumentException ae) { throw new XunitException($\u0026#34;Is docker installed and running? {ae.Message}.\u0026#34;); } } protected override IHost CreateHost(IHostBuilder builder) { .... } } This code will start a docker container with MS SQL Server 2019 (the default at the tie of writing) container. The connectionstring is exposed the via DatabaseContainer.GetConnectionString(). That allows you to do thing like overriding DbContext as part of ConfigureTestServices as shown below.\nservices.AddScoped((sp) =\u0026gt; { var options = new DbContextOptionsBuilder\u0026lt;DatabaseContext\u0026gt;() .UseSqlServer(DatabaseContainer.ConnectionString + \u0026#34;;TrustServerCertificate=True\u0026#34;) .Options; var db = new DbContext(options); db.Database.Migrate(); return db; }); }); Then to actually start the container you can use a xUnit class fixture:\npublic class TestFixture : IAsyncLifetime { private readonly CustomWebApplicationFactory _factory; public TestFixture() { _factory = new FullIntegrationWebApplicationFactory(); } public async Task InitializeAsync() { await _factory.DatabaseContainer.StartAsync(); } public async Task DisposeAsync() { await _factory.DatabaseContainer.StopAsync(); } Full example on GitHub https://github.com/netwatwezoeken/full-integration-testing\nAutomate browser interaction To automate a test we must be able to do whatever the user is doing on the browser. Playwright works very well in this scenario because it is fully integratable in you IDE workflow (Visual Studio or Rider). Tests can be written in C# and run through xUnit, NUnit or MSTest.\nPlaywright can be added through a xUnit ClassFixture. Let\u0026rsquo;s add it to TestFixture from the previous part.\npublic class TestFixture : IAsyncLifetime { private readonly FullIntegrationWebApplicationFactory _factory; private IPlaywright _playwright; private IBrowser _browser; private IBrowserContext _context; public TestFixture() { _factory = new FullIntegrationWebApplicationFactory(); } public async Task InitializeAsync() { await _factory.DatabaseContainer.StartAsync(); _playwright = await Playwright.CreateAsync(); _browser = await _playwright.Chromium.LaunchAsync(); _context = await _browser.NewContextAsync(); Page = await _context.NewPageAsync(); _factory.CreateClient(); } public IPage Page { get; set; } public async Task DisposeAsync() { await _factory.DatabaseContainer.StopAsync(); await _context.DisposeAsync(); _playwright.Dispose(); } } ⚠️ 2024-12-28 update: the code below has some problems. Please see my new article for more information.\nBy default the WebApplicationFactory does not listen on a port by default. But we want to Playright to start a real browser that will need to connect to something. To do so you must override CreateHost and add a line to ConfigureWebHost:\nprotected override IHost CreateHost(IHostBuilder builder) { // Create a plain host that we can return. var dummyHost = builder.Build(); // Configure and start the actual host. builder.ConfigureWebHost(webHostBuilder =\u0026gt; webHostBuilder.UseKestrel()); var host = builder.Build(); host.Start(); return dummyHost; } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseUrls(\u0026#34;https://localhost:7048\u0026#34;); .... } With that fixture and modification to the CustomWebApplicationFactory in place we can create a test.\npublic class Contacts : IClassFixture\u0026lt;TestFixture\u0026gt; { private readonly IPage _page; public EditContact(TestFixture fixture) { _page = fixture.Page; } [Fact] public async Task RemoveContact() { // Go to the page and wait for the loading to complete await _page.GotoAsync(\u0026#34;https://localhost:7048/\u0026#34;); await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); await _page.Locator(\u0026#34;.contact-detail\u0026#34;) .IsVisibleAsync(); // check that there\u0026#39;s one contact Assert.Equal(1, await _page.Locator(\u0026#34;.contact-detail\u0026#34;) .CountAsync()); // Delete the contact await _page.Locator(\u0026#34;.clickable\u0026#34;).ClickAsync(); await _page.GetByRole( AriaRole.Button, new() { NameString = \u0026#34;Confirm\u0026#34; }).ClickAsync(); // Check if the contact is deleted await _page.GetByText(\u0026#34;Page 1 of 0: showing 0 of 0 items. Previous Next\u0026#34;) .ClickAsync(); Assert.Equal(0, await _page.Locator(\u0026#34;.contact-detail\u0026#34;).CountAsync()); } } Full example on GitHub: https://github.com/netwatwezoeken/full-integration-testing\n⚠️ 2024-12-28 update: This repository has been update to solve the problems described in my new article.\nPlease note that you need Powershell 7 to be installed and follow the intruction given by Playwright during the first run. Yes, Powershel 7, also when on MacOS or Linux\nWith this in place you can use the xUnit ClassFixtures to either run test in isolation (slower) or start the DB and browser once or a couple of times (faster). Thanks to the usage of the xUnit test runner tests can now be easily run from within your IDE.\nPipeline integration Having these test in the IDE is great. Nice and quick feedback before check-in of code. A build pipeline can be fairly simple. But there are some caveats.\nThe first thing is .NET 7. At the time of writing .NET 7 is out and suported under standard support (STS). The official playwright docker image only supports .NET 6 (LTS). Either nwwz/playwright-dotnet Or build your own image. Added .NET 7 to the official playwright image is fairly easy as can be seen in this Dockerfile\nNext up are the certificates for https. Certificates are not present by default and also not shared with the browser. Certificates can be generated using the dev-certs command:\ndotnet dev-certs https --clean dotnet dev-certs https --trust Then in the TestsFixture we can tell the browser to ignore https errors:\n_context = await _browser.NewContextAsync(new BrowserNewContextOptions() { IgnoreHTTPSErrors = true }); And lastly the Ryuk container. This is a helper container started by testcontainers to cleanup container when not in use anymore. Typically a cloud hosted environment already lceans up after and does not allow any containers to be run in priviledged mode. Simply disable the Ryuk container by setting TESTCONTAINERS_RYUK_DISABLED to true.\nAll of the above in a Bitbucket pipeline would look somthing like this:\n- step: name: End to end testing image: nwwz/playwright-dotnet:v1.30.0-focal script: - dotnet dev-certs https --clean - dotnet dev-certs https --trust - export TESTCONTAINERS_RYUK_DISABLED=true - dotnet restore - cd tests/End2EndTests - dotnet test artifacts: - tests/End2EndTests/traces/** services: - docker Finetuning and caveats Other databases or SQL Sever versions The Testcontainers projects allows for a lot more than just MS SQL Server containers. The project comes with some useful builders to setup a variety of services you might need. And then there is always the option to specify your own specific image, ports, environment variables, etc.\nUse the following if you specifically want to specify the version of SQL Server:\nDatabaseContainer = new MsSqlBuilder() .WithImage(\u0026#34;mcr.microsoft.com/mssql/server:2022-latest\u0026#34;) .Build(); Turn off Slowmo and set Headless to true The example project has Headless disabled and slowmo set to 2000ms per action. This is great to see what is going on. On enviroments like the Bitbucket cloud hosted runners this will fail due to the lack a of screen. Be sure to either comment or completely remove these 2 lines:\n_browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { //Headless = false, //SlowMo = 2000 }); More dependencies than just the database Of course a real application might have more that just it\u0026rsquo;s own database to talk to. Keep in mind that Testcontainers for .NET has helpers for many services. Redis, Minio, Mongo, RabbitMq and even Kafka. Even without a helper available it is really easy to spin up any other docker image to be used in testing.\nConclusion Playwright .NET and Testcontainers for .NET within a .NET solution provide a nice and easy way have E2E integration tests at both IDE an pipeline level.\nI help organisations and developers to build better software faster. Feel free to reach out through the my socials if you want to know more, need help, have questions or remarks.\n","permalink":"https://blog.netwatwezoeken.nl/posts/integration-testing-with-playwright-and-testcontainers/","summary":"\u003cp\u003eTesting plays a crucial role in software development and we strive to test as much as possible using isolated unittests. However, there are times when these unittests fail to capture issues that arise when the entire application is tested. Such issues may include discrepancies between front-end and back-end implementation of APIs, or differences in behavior between real and mocked databases. These problems can be easily identified by running a series of scenarios that test the entire application, including the front-end, back-end, and database. Many developers resort to manual testing to achieve this, but automated testing offers greater repeatability and the ability to integrate checks into the development pipeline.\u003c/p\u003e","title":"Integration testing with Playwright and Testcontainers in ASP.NET"},{"content":"With the release of .NET 6 also came the option to use source generation in System.Text.Json. In short this means that instead of runtime reflection some of the work is now done compile time. This makes it more efficient. But how much? And should I care? Just measure it. I made a small project that you can use and adjust to measure the difference for your specific case. Let the results help you decide what\u0026rsquo;s best for your application.\nBenchmarking code The code can be found here: netwatwezoeken/jsonbenchmark (github.com). It uses BenchmarkDotNet for measurements and Bogus to generate fake data. Both serialization and de-serialization are measured. I chose to have the following Book as object under test. Both a list of one thousand books and a and single Book are benchmarked.\nI included Newtonsoft for reference. Hence the project contains three json (de)serialization methods\nNewtonsoft System.Text.Json Generated System.Text.Json Settings for method 1 and 2 can be found in SerializationOptions.cs. Settings for generated System.Text.Json are in JsonContext.cs.\nTo verify that all serialization settings are compliant for all three mechanisms a couple of test are available. These allow you to validate if all methods give the same result. We are trying to compare apples to apples.\nTo get results run the App in Release mode. To modify the benchmarks just replace Book by your own class or record.\nSimple as that! Continue reading to see some of my results and findings.\nResults As can be seen in the results below deserialization is 7% (Book) and 10% (List\u0026lt;Book) slower with the source generator compared to normal System.Text.Json. However the performance gain when serializing List\u0026lt;Book\u0026gt; seems higher (27%) compared to serializing a single Book (15%).\nMemory wise the differences are small.\nBenchmarks were executed on:\nBenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000 AMD Ryzen 9 5900X, 1 CPU, 24 logical and 12 physical cores .NET SDK=6.0.100 Serialization Deserialization Specifics worth mentioning Most of the code should be self explanatory. Hopefully it will help you to do some measurements yourself and come to a good decision.\nThere are a few things I\u0026rsquo;d like to mention:\nGenerated serialization it needs a JsonConverter Attribute on enum CoverType for JsonStringEnumConverter to work. I was not able to set this in JsonContext.cs Some author names were represented in escaped unicode by System.Text.Json but not by Newtonsoft. Thefore UnsafeRelaxedJsonEscaping is added to the settings for method 2 and Unescape() is used in the unittest of method 3. Thank Bogus for providing good data! Enums in nested classes result in a compiler error. As a workaround, explicitly add any nested enum to JsonContext using a JsonSerializable attribute. See also https://github.com/dotnet/runtime/issues/61860. Things to consider How will it affect your application? Only expect significant results when your application is doing a lot of (de)serialization. In a few cases processing time might matter. I would not bother optimizing something that is only executed once a minute and already performs well within limits. When moving from one method to the other, be sure to thoroughly test your (de)serialization results and double check the settings. As an example: Newtonsoft is case insensitive by default. System.Text.Json is not. Personally, I would not mix Newtonsoft and System.Text.Json in a single solution. However, mixing generated and reflection based System.Text.Json serialization could be interesting. Only apply source generation where valuable. Additional code. You will add code that needs maintenance. Not much, but you do need to list which classes you plan to (de)serialize. Also take care that the serialization settings are now in a different place. Mixed use of (de)serialization might require additional caution to keep the settings in sync. The difference between Newtonsoft and System.Text.Json might not be that dramatic for your application: What Those Benchmarks Of System.Text.Json Don\u0026rsquo;t Mention (dotnetcoretutorials.com) Like the article or want to give some feedback? Feel free to reach!\n","permalink":"https://blog.netwatwezoeken.nl/posts/source-generation-in-system-text/","summary":"\u003cp\u003eWith the release of .NET 6 also came the option to use source generation in System.Text.Json. In short this means that instead of runtime reflection some of the work is now done compile time. This makes it more efficient. But how much? And should I care?\nJust measure it. I made a small project that you can use and adjust to measure the difference for your specific case. Let the results help you decide what\u0026rsquo;s best for your application.\u003c/p\u003e","title":"Source generation in System.Text.Json, should I care?"},{"content":"Jenkins and kubernetes are a great combination. I love the concept of leveraing container technology to provide any build tools that you might require. However in many cases the examples provided are not always the best or most secure.\nIn Section 10.2.2 Building your image of Mannings Microservices in Action the following Jenkins Pipeline script is suggested:\ndef withPod(body) { podTemplate(label: \u0026#39;pod\u0026#39;, serviceAccount: \u0026#39;jenkins\u0026#39;, containers: [ containerTemplate(name: \u0026#39;docker\u0026#39;, image: \u0026#39;docker\u0026#39;, command: \u0026#39;cat\u0026#39;, ttyEnabled: true), containerTemplate(name: \u0026#39;kubectl\u0026#39;, image: \u0026#39;morganjbruce/kubectl\u0026#39;, command: \u0026#39;cat\u0026#39;, ttyEnabled: true) ], volumes: [ hostPathVolume(mountPath: \u0026#39;/var/run/docker.sock\u0026#39;, hostPath: \u0026#39;/var/run/docker.sock\u0026#39;), ] ) { body() } } withPod { node(\u0026#39;pod\u0026#39;) { def tag = \u0026#34;${env.BRANCH_NAME}.${env.BUILD_NUMBER}\u0026#34; def service = \u0026#34;market-data:${tag}\u0026#34; checkout scm container(\u0026#39;docker\u0026#39;) { stage(\u0026#39;Build\u0026#39;) { sh(\u0026#34;docker build -t ${service} .\u0026#34;) } } container(\u0026#39;kubectl\u0026#39;) { stage(\u0026#39;Deploy\u0026#39;) { sh(\u0026#34;kubectl --namespace=staging apply -f deploy/staging/\u0026#34;) } } } } In this script there are three things that bother me:\nMounting the docker socket Depending on docker The kubectl image In this blog I\u0026rsquo;ll explain the issue with each of these three points and I\u0026rsquo;ll provide a solution that solves these issues.\nMounting the docker socket is bad Docker runs a root also the owner of /var/run/docker.sock is root. This means that anyone with access to that file can get root access.\nAn exploit can be pretty simple as seen on Stack Overflow. Thanks cyphar!\nThe danger of using this with Jenkins is also explicitly mentioned by Peterm Benjamin on dev.to\nKubernetes without Docker With modern kubernetes configurations it might very well be the case that Docker is not installed on any of the nodes. This has a simple reason. You only need a Container Runtime Interface (CRI). Read more about this on kubernetes.io\nOne of the most widely use implementations is Containerd. Containerd in fact is part of Docker!\nMounting the docker socket without Docker installed will just result in errors like:\nCannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? Pick your docker images Pick your dependencies wisely! This also goes for docker images. I always check for two things:\nIs the sourcecode available? Is the image activly maintained? morganjbruce/kubectl is a no on both checks.\nIt appears the one of the authors of the book just made this image to accompany the book. There is no Dockerfile and it does not seem to be actively maintained. The book does a good job in explaining the concept. But this is not a sutainable solution.\nA better solution Let\u0026rsquo;s start with the Docker depenceny and security ussue. The are several tools to build Docker images. The following requirements make that list a bit shorter:\nRun as container on kubernetes Run unprivileged I think Kaniko is the most promising. I have not tried img but will definitely do so another time.\nMy solution for the kubectl image is to take one of the list below. These have sources available and seem to be updated regulary.\nhttps://hub.docker.com/r/bitnami/kubectl https://hub.docker.com/r/lachlanevenson/k8s-kubectl https://hub.docker.com/r/nwwz/kubectl (mine) Buid your own Combining Kaniko and another kubectl image into a better solution:\ndef withPod(body) { podTemplate(serviceAccount: \u0026#39;jenkins\u0026#39;, containers: [ containerTemplate(name: \u0026#39;kaniko\u0026#39;, image: \u0026#39;gcr.io/kaniko-project/executor:debug\u0026#39;, command: \u0026#39;/busybox/cat\u0026#39;, ttyEnabled: true), containerTemplate(name: \u0026#39;kubectl\u0026#39;, image: \u0026#39;nwwz/kubectl:debug-v1.18\u0026#39;, command: \u0026#39;cat\u0026#39;, ttyEnabled: true) ], volumes: [ secretVolume(secretName: \u0026#39;registrycred\u0026#39;, mountPath: \u0026#39;/cred\u0026#39;) ] ) { body() } } withPod { node(POD_LABEL) { checkout scm container(\u0026#39;kaniko\u0026#39;) { stage(\u0026#39;Build image\u0026#39;) { sh(\u0026#34;cp /cred/.dockerconfigjson /kaniko/.docker/config.json\u0026#34;) sh(\u0026#34;executor --context=`pwd` --dockerfile=`pwd`/Dockerfile --destination=${service} --single-snapshot\u0026#34;) } } container(\u0026#39;kubectl\u0026#39;) { stage(\u0026#39;Deploy\u0026#39;) { sh(\u0026#34;kubectl --namespace=staging apply -f deploy/staging/\u0026#34;) } } } } Please note that the script not only builds the image but also pushes it into a registry. Therefore add your credentials to the kubernetes cluster using this command:\nkubectl create secret docker-registry registrycred -n build-env --docker-server=\u0026lt;your-registry-server\u0026gt; --docker-username=\u0026lt;your-name\u0026gt; --docker-password=\u0026lt;your-password\u0026gt; --docker-email=\u0026lt;your-email\u0026gt; registrycred must match the name use in your Jekinsfile and the namespace must match the namespace that Jenkins uses.\nConclusion Examples are often great but they serve a purpose. I guess the purpose of Mannings Microservices in Action is more a conceptual exampe than it is a production worthy example. In any case examples are just examples and should used as such. The proposed solution is no exception. It\u0026rsquo;s merely an example of what could be done better.\nBut always be cautious about potential security issue and always check docker images that you depend on.\n","permalink":"https://blog.netwatwezoeken.nl/posts/a-safer-jenkins-pipeline-on-kubernetes/","summary":"\u003cp\u003eJenkins and kubernetes are a great combination. I love the concept of leveraing container technology to provide any build tools that you might require. However  in many cases the examples provided are not always the best or most secure.\u003c/p\u003e\n\u003cp\u003eIn Section 10.2.2 \u003cem\u003eBuilding your image\u003c/em\u003e of Mannings Microservices in Action the following Jenkins Pipeline script is suggested:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-groovy\" data-lang=\"groovy\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ewithPod\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebody\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003epodTemplate\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003elabel:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;pod\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003eserviceAccount:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;jenkins\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003econtainers:\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003econtainerTemplate\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003ename:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;docker\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003eimage:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;docker\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003ecommand:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;cat\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003ettyEnabled:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"o\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003econtainerTemplate\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003ename:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;kubectl\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003eimage:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;morganjbruce/kubectl\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003ecommand:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;cat\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003ettyEnabled:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e],\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nl\"\u003evolumes:\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003ehostPathVolume\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"nl\"\u003emountPath:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/var/run/docker.sock\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e,\u003c/span\u003e \u003cspan class=\"nl\"\u003ehostPath:\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;/var/run/docker.sock\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003ebody\u003c/span\u003e\u003cspan class=\"o\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ewithPod\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;pod\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"n\"\u003etag\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;${env.BRANCH_NAME}.${env.BUILD_NUMBER}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003edef\u003c/span\u003e \u003cspan class=\"n\"\u003eservice\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;market-data:${tag}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003echeckout\u003c/span\u003e \u003cspan class=\"n\"\u003escm\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003econtainer\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;docker\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Build\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esh\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;docker build -t ${service} .\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003econtainer\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;kubectl\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"n\"\u003estage\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Deploy\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esh\u003c/span\u003e\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;kubectl --namespace=staging apply -f deploy/staging/\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e)\u003c/span\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn this script there are three things that bother me:\u003c/p\u003e","title":"Safer Jenkins pipelines on kubernetes"},{"content":"This artice describes how you can create a kubernetes custer using Vagrant, Hyper-V and Packer.\nUpdate (11-05-2020) I have updated the scripts to use contianerd in favor of docker. Don\u0026rsquo;t worry using docker is still possible. In /packer/ubuntu-18.04-amd64.json replace containerd.sh woth docker.sh.\nWhy? I found myself in the need of having a kubernetes test cluster. Instead of using minikube or enabling Kubernetes on Docker Desktop. I thought I could learn from setting up a cluster from scratch. This also allows me to easiliy simulate network failures or have nodes go down.\nPrerequisites Hyper-V enabled Packer added to your PATH Vagrant installed The only reason I choose Hyper-V is that my machine runs Windows 10 Pro, which includes Hyper-V. Please note that both Packer an Vagrant support many other platforms.\nPlease make sure all prerequisites are installed.\nPrepare a Virtual Machine image First we are going to use packer to create a Hyper-V base image. The image will be used to build the cluster. For this I took the Ubuntu packer file from bento as an example. On top of that I\u0026rsquo;ve added docker installation and installation of kubernetes tools.\nPlease download or clone https://github.com/netwatwezoeken/vagrant-kubernetes.git and navigate to the packer directory in an elevated command promt. The current user needs to be in \u0026ldquo;Hyper-V Administrators\u0026rdquo; or \u0026ldquo;Administrators\u0026rdquo; to communicate to Hyper-V.\nOptionally start the Hyper-V Manager to see what packer does. Now run packer build ubuntu-18.04-amd64.json\nThis will take some while and packer might ask for permission to run a http server to serve the pre-seed config file.\nWhat will happen is the following:\nUbuntu iso is downloaded VM is created Ubuntu installation starts (using a preseed config) When installation is complete Ubuntu will reboot with ssh enabled Installation continues using ssh VM shuts down Imagefile is built Here\u0026rsquo;s a video as reference: When the image is completed, add it to vagrant using: vagrant box add builds\\ubuntu-18.04.hyperv.box --name ubuntu-k8s-docker\nOne time preparations Install vagrant-reload: vagrant plugin install vagrant-reload\nCreat a new network swith in Hyper-V for cluster to run on. Please see my previous blog for that. The ip ranges match with the ip addresses in Vagrantfile.\nSpin up a cluster Navigate to the directory in which https://github.com/netwatwezoeken/vagrant-kubernetes.git was cloned.\nIn Vagrantfile change the number of worker nodes to the desired amount. Look for NodeCount. The default is 2.\nThen, using elevated command promt, execute vagrant up\nUnfortunately Vagrant will ask to which swwtch a machine must be connected initially. Please be sure to select the Default Switch! The Vagrant script will reconnect to the static ip switch after netwroking is properly configured.\nApart from the need to select a switch for each machine no further actions are required.\nAfter the process is complete you can use ssh (username: vagrant, password: vagrant) to login to the master node on 192.168.40.5. Type kubectl get nodes to see if all nodes are ready.\nvagrant@k8smaster:~$ kubectl get nodes NAME STATUS ROLES AGE VERSION k8smaster Ready master 11m v1.18.0 kworker1 Ready \u0026lt;none\u0026gt; 6m59s v1.18.0 kworker2 Ready \u0026lt;none\u0026gt; 3m56s v1.18.0 Delete the cluster Deleting is easy. Just type vagrant destroy\nC:\\src\\vagrant-kubernetes\u0026gt;vagrant destroy kworker2: Are you sure you want to destroy the \u0026#39;kworker2\u0026#39; VM? [y/N] y ==\u0026gt; kworker2: Running cleanup tasks for \u0026#39;reload\u0026#39; provisioner... ==\u0026gt; kworker2: Stopping the machine... ==\u0026gt; kworker2: Deleting the machine... kworker1: Are you sure you want to destroy the \u0026#39;kworker1\u0026#39; VM? [y/N] y ==\u0026gt; kworker1: Running cleanup tasks for \u0026#39;reload\u0026#39; provisioner... ==\u0026gt; kworker1: Stopping the machine... ==\u0026gt; kworker1: Deleting the machine... k8smaster: Are you sure you want to destroy the \u0026#39;k8smaster\u0026#39; VM? [y/N] y ==\u0026gt; k8smaster: Running cleanup tasks for \u0026#39;reload\u0026#39; provisioner... ==\u0026gt; k8smaster: Stopping the machine... ==\u0026gt; k8smaster: Deleting the machine... C:\\src\\vagrant-kubernetes\u0026gt; Final thoughts I can now easily setup a multi node kubernetes cluster for some proper testing. Additionally, setting this up really helped me learn more about kubernetes.\nThere is some improvements to be made though:\nSetup of the masternode takes quite some time because kubernetes images are downloaded. There is possibly some speed to gain to download these already when building the Hyper-V image Maybe replace docker by containerd Done, see update at the start of this article! Adding an Ingress Controller or some other usefull stuff to the initial cluster Stuff to ponder about for the next blog\u0026hellip;\n","permalink":"https://blog.netwatwezoeken.nl/posts/automated-kubernetes-installation-on-hyper-v/","summary":"\u003cp\u003eThis artice describes how you can create a kubernetes custer using \u003ca href=\"https://www.vagrantup.com/\"\u003eVagrant\u003c/a\u003e, Hyper-V and \u003ca href=\"https://packer.io/\"\u003ePacker\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"update-11-05-2020\"\u003eUpdate (11-05-2020)\u003c/h2\u003e\n\u003cp\u003eI have updated the scripts to use contianerd in favor of docker. Don\u0026rsquo;t worry using docker is still possible. In \u003ccode\u003e/packer/ubuntu-18.04-amd64.json\u003c/code\u003e replace \u003ccode\u003econtainerd.sh\u003c/code\u003e woth \u003ccode\u003edocker.sh\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"why\"\u003eWhy?\u003c/h2\u003e\n\u003cp\u003eI found myself in the need of having a kubernetes test cluster. Instead of using minikube or enabling Kubernetes on Docker Desktop. I thought I could learn from setting up a cluster from scratch. This also allows me to easiliy simulate network failures or have nodes go down.\u003c/p\u003e","title":"Automated Kubernetes installation on Hyper-V"},{"content":"The goal is to have networking with static ip addresses for virtual machines on Hyper-V. The machine must also be able to access the internet. The easiest way is to start a Powershell session with elevated rights. Then use the follwing three commands. Feel free to change any names and ip address ranges.\nCreate a new switch New-VMSwitch -SwitchName \u0026#34;NAT Switch 192.168.40.x\u0026#34; -SwitchType Internal Assign and IP address New-NetIPAddress -IPAddress 192.168.40.1 -PrefixLength 24 -InterfaceAlias \u0026#34;vEthernet (NAT Switch 192.168.40.x)\u0026#34; Enable NAT New-NetNAT -Name \u0026#34;NATNetwork\u0026#34; -InternalIPInterfaceAddressPrefix 192.168.40.0/24 This will result in:\na virtual network interface card (vEthernet) that has ip 192.168.40.1 assigned to it. a virtual network switch (NAT Switch 192.168.40.x) that can be connected to virtual machines Network Address Translation (NAT) between vEthernet and the hosts physical network connection. Be sure the configure networking on your virtual machines as follows:\nAddress: 192.168.40.\u0026lt;anything between 2 and 254\u0026gt; Mask: 255.255.255.0 Gateway 192.168.40.1 That\u0026rsquo;s it!\n","permalink":"https://blog.netwatwezoeken.nl/posts/static-ip-for-hyper-v-with-nat/","summary":"\u003cp\u003eThe goal is to have networking with static ip addresses for virtual machines on Hyper-V. The machine must also be able to access the internet.\nThe easiest way is to start a Powershell session with elevated rights. Then use the follwing three commands. Feel free to change any names and ip address ranges.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCreate a new switch\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eNew-VMSwitch\u003c/span\u003e \u003cspan class=\"n\"\u003e-SwitchName\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;NAT Switch 192.168.40.x\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003e-SwitchType\u003c/span\u003e \u003cspan class=\"n\"\u003eInternal\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col start=\"2\"\u003e\n\u003cli\u003eAssign and IP address\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eNew-NetIPAddress\u003c/span\u003e \u003cspan class=\"n\"\u003e-IPAddress\u003c/span\u003e \u003cspan class=\"mf\"\u003e192.168\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"py\"\u003e40\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"py\"\u003e1\u003c/span\u003e \u003cspan class=\"n\"\u003e-PrefixLength\u003c/span\u003e \u003cspan class=\"mf\"\u003e24\u003c/span\u003e \u003cspan class=\"n\"\u003e-InterfaceAlias\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;vEthernet (NAT Switch 192.168.40.x)\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col start=\"3\"\u003e\n\u003cli\u003eEnable NAT\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eNew-NetNAT\u003c/span\u003e \u003cspan class=\"n\"\u003e-Name\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;NATNetwork\u0026#34;\u003c/span\u003e \u003cspan class=\"n\"\u003e-InternalIPInterfaceAddressPrefix\u003c/span\u003e \u003cspan class=\"mf\"\u003e192.168\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"py\"\u003e40\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"mf\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e/\u003c/span\u003e\u003cspan class=\"mf\"\u003e24\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis will result in:\u003c/p\u003e","title":"Static IPs with NAT on Hyper-V"},{"content":" .NET wat we zoeken is me, Jos Hendriks {\n\u0026nbsp;\u0026nbsp;A lead developer\n\u0026nbsp;\u0026nbsp;that is hands-on and has solid experience in developing software\n} .NET wat we zoeken\n{\n\u0026nbsp;\u0026nbsp;A technical coach\n\u0026nbsp;\u0026nbsp;who helps people grow in becoming better at their work\n} .NET wat we zoeken\n{\n\u0026nbsp;\u0026nbsp;A software architect\n\u0026nbsp;\u0026nbsp;with experience in all .NET versions and systems design\n} .NET wat we zoeken\n","permalink":"https://blog.netwatwezoeken.nl/about/","summary":"\u003cdiv class=\"container\" style=\"font-size:1.2rem;font-family: Consolas,monaco,monospace;\"\u003e\n\u003cdiv class=\"item\"\u003e\n\u003ch4\u003e.NET wat we zoeken is me, Jos Hendriks\u003c/h4\u003e\n\u003c/div\u003e\n\u003cbr\u003e\n\u003cdiv\u003e\n{\u003cbr\u003e\n\u003cspan class=\"yellow-code\"\u003e\u0026nbsp;\u0026nbsp;A\u003c/span\u003e \u003cspan class=\"green-code\"\u003elead developer\u003c/span\u003e\u003cbr\u003e\u0026nbsp;\u0026nbsp;\u003cspan class=\"yellow-code\"\u003ethat \u003c/span\u003e\u003cspan class=\"blue-code\"\u003eis hands-on and has solid experience in developing software\u003c/span\u003e\u003cbr\u003e\n}\n\u003cp class=\"purple-code\"\u003e.NET wat we zoeken\u003c/p\u003e\n\u003c/div\u003e\n\u003cbr\u003e\n\u003cdiv\u003e\n{\u003cbr\u003e\n\u003cspan class=\"yellow-code\"\u003e\u0026nbsp;\u0026nbsp;A\u003c/span\u003e \u003cspan class=\"green-code\"\u003etechnical coach\u003c/span\u003e\u003cbr\u003e\u0026nbsp;\u0026nbsp;\u003cspan class=\"yellow-code\"\u003ewho \u003c/span\u003e\u003cspan class=\"blue-code\"\u003ehelps people grow in becoming better at their work\u003c/span\u003e\u003cbr\u003e\n}\n\u003cp class=\"purple-code\"\u003e.NET wat we zoeken\u003c/p\u003e\n\u003c/div\u003e\n\u003cbr\u003e\n\u003cdiv\u003e\n{\u003cbr\u003e\n\u003cspan class=\"yellow-code\"\u003e\u0026nbsp;\u0026nbsp;A\u003c/span\u003e \u003cspan class=\"green-code\"\u003esoftware architect\u003c/span\u003e\u003cbr\u003e\u0026nbsp;\u0026nbsp;\u003cspan class=\"yellow-code\"\u003ewith \u003c/span\u003e\u003cspan class=\"blue-code\"\u003eexperience in all .NET versions and systems design\u003c/span\u003e\u003cbr\u003e\n}\n\u003cp class=\"purple-code\"\u003e.NET wat we zoeken\u003c/p\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"About"}]