{"id":1264,"date":"2024-09-25T21:40:28","date_gmt":"2024-09-25T21:40:28","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2024\/09\/25\/getting-started-with-testing-and-net-aspire\/"},"modified":"2024-09-25T21:40:28","modified_gmt":"2024-09-25T21:40:28","slug":"getting-started-with-testing-and-net-aspire","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2024\/09\/25\/getting-started-with-testing-and-net-aspire\/","title":{"rendered":"Getting started with testing and .NET Aspire"},"content":{"rendered":"<p>Automated testing is an important part of software development, helping ensure that bugs are caught early and regression issues are prevented. In this blog post, we will explore how to get started with testing in .NET Aspire, allowing us to test scenarios across our distributed applications.<\/p>\n<h2>Testing Distributed Applications<\/h2>\n<p>Distributed applications are inherently complex, you need to ensure components such as databases, caches, etc. are available and in the correct state. Then your application may have multiple services that need to be tested together. .NET Aspire is a great tool to help us define the environment for our application, connecting together all the services and resources, making it easy to launch our environment.<\/p>\n<p>And this is equally true when it comes to end-to-end, or integration, testing of an application. We need to ensure that the database is in an expected state for a test, avoid having other tests interfere with our test, and ensure that the application is running in the correct configuration. This is something that .NET Aspire can help us with.<\/p>\n<p>Thankfully, we have the .NET Aspire <a href=\"https:\/\/www.nuget.org\/packages\/aspire.hosting.testing\">Aspire.Hosting.Testing NuGet package<\/a> which can help us with this. Let\u2019s take a look at how we can use this package to write tests.<\/p>\n<h2>Getting Started<\/h2>\n<p>To get started, we\u2019re going to create a new .NET Aspire Starter App project. This will create a new .NET Aspire application with the AppHost, Service Defaults, an API backend and a Blazor web frontend.<\/p>\n<p>Ensure you have the .NET Aspire workload installed:<\/p>\n<p>dotnet workload update<br \/>\ndotnet workload install aspire<\/p>\n<p>Then create a new project using the aspire-starter template:<\/p>\n<p>dotnet new aspire-starter &#8211;name AspireWithTesting<\/p>\n<p>Next, we need to add a test project, and we can choose from one of three testing frameworks, MSTest, xUnit or Nunit. For this example, we\u2019ll use MSTest. This can be created using the aspire-mstest template:<\/p>\n<p>dotnet new aspire-mstest &#8211;name AspireWithTesting.Tests<br \/>\ndotnet sln add AspireWithTesting.Tests<\/p>\n<div class=\"alert alert-primary\">\n<p class=\"alert-divider\"><strong>Note<\/strong><\/p>\n<p>You can have the test project included when creating with the aspire-starter template by adding the &#8211;test-framework MSTest (or other framework) flag.<\/p><\/div>\n<p>The template already references the Aspire.Hosting.Testing NuGet package, as well as the chosen testing framework (MSTest in this case), so the last thing we need to do is add a reference to the <strong>AppHost<\/strong> project in our Test project:<br \/>\ndotnet add AspireWithTesting.Tests reference AspireWithTesting.AppHost<\/p>\n<h2>Writing Tests<\/h2>\n<p>We\u2019ll find there is already a stub test file, IntegrationTest1.cs in our test project that describes the steps above and provides an example of tests that can be written, but let\u2019s start from scratch so we can understand what\u2019s going on. Create a new file called FrontEndTests.cs and add the following code:<\/p>\n<p>namespace AspireWithTesting.Tests;<\/p>\n<p>[TestClass]<br \/>\npublic class FrontEndTests<br \/>\n{<br \/>\n    [TestMethod]<br \/>\n    public async Task CanGetIndexPage()<br \/>\n    {<br \/>\n        var appHost =<br \/>\n            await DistributedApplicationTestingBuilder<br \/>\n                    .CreateAsync&lt;Projects.AspireWithTesting_AppHost&gt;();<br \/>\n        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =&gt;<br \/>\n        {<br \/>\n            clientBuilder.AddStandardResilienceHandler();<br \/>\n        });<\/p>\n<p>        await using var app = await appHost.BuildAsync();<br \/>\n        await app.StartAsync();<\/p>\n<p>        var resourceNotificationService =<br \/>\n            app.Services.GetRequiredService&lt;ResourceNotificationService&gt;();<br \/>\n        await resourceNotificationService<br \/>\n            .WaitForResourceAsync(&#8220;webfrontend&#8221;, KnownResourceStates.Running)<br \/>\n            .WaitAsync(TimeSpan.FromSeconds(30));<\/p>\n<p>        var httpClient = app.CreateHttpClient(&#8220;webfrontend&#8221;);<br \/>\n        var response = await httpClient.GetAsync(&#8220;\/&#8221;);<\/p>\n<p>        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);<br \/>\n    }<br \/>\n}<\/p>\n<p>Awesome, the test is written, let\u2019s run it:<\/p>\n<p>dotnet test<\/p>\n<p>And if everything goes to plan, we should see output such as:<\/p>\n<p>Test summary: total: 1, failed: 0, succeeded: 1, skipped: 0, duration: 0.9s<\/p>\n<h2>Understanding the Test<\/h2>\n<p>Let\u2019s break down what\u2019s happening in the test:<\/p>\n<p>var appHost =<br \/>\n    await DistributedApplicationTestingBuilder<br \/>\n            .CreateAsync&lt;Projects.AspireWithTesting_AppHost&gt;();<br \/>\nappHost.Services.ConfigureHttpClientDefaults(clientBuilder =&gt;<br \/>\n{<br \/>\n    clientBuilder.AddStandardResilienceHandler();<br \/>\n});<\/p>\n<p>await using var app = await appHost.BuildAsync();<br \/>\nawait app.StartAsync();<\/p>\n<p>This first section of the test is using the <strong>AppHost<\/strong> project that defines all our resources, services, and their relationships, and then starts it up, as if we did a dotnet run against the project, but in a test environment which we can control some additional aspects. For example, we\u2019re injecting the StandardResilienceHandler into the HttpClient that the tests are going to use to interact with the services in the AppHost. Once the testing AppHost is configured, we can build the application, ready to be started.<\/p>\n<p>var resourceNotificationService =<br \/>\n    app.Services.GetRequiredService&lt;ResourceNotificationService&gt;();<br \/>\nawait resourceNotificationService<br \/>\n    .WaitForResourceAsync(&#8220;webfrontend&#8221;, KnownResourceStates.Running)<br \/>\n    .WaitAsync(TimeSpan.FromSeconds(30));<\/p>\n<p>Because the AppHost will be starting several different resources and services, we need to ensure that they are available to us before we try to run tests against them. After all, if the web application hasn\u2019t started and we try to resolve it, we\u2019re going to get an error. The ResourceNotificationService is a service that allows us to wait for a resource to be in a particular state, in this case, we\u2019re waiting for the webfrontend (the name we set in the AppHost) to be in the Running state, and we\u2019re giving it 30 seconds to do so. This pattern would need to be repeated for any other services that we\u2019re going to interact with, whether directly or indirectly.<\/p>\n<p>var httpClient = app.CreateHttpClient(&#8220;webfrontend&#8221;);<br \/>\nvar response = await httpClient.GetAsync(&#8220;\/&#8221;);<\/p>\n<p>Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);<\/p>\n<p>Finally, we can request an instance of HttpClient from the app that has been started and provide the name of the service we want to interact with. This uses the same service discovery as the rest of the application, so we don\u2019t need to worry about the URL or port that the service is running on. We can then make a request to the service, in this case, the root of the web frontend, and check that we get a 200 OK response, confirming that the service is running and responding as expected.<\/p>\n<h2>Testing the API<\/h2>\n<p>Testing the API service is a very similar approach to the frontend service, but since it\u2019s returning data, we can take it a step further and assert against the data that is returned:<\/p>\n<p>using System.Net.Http.Json;<\/p>\n<p>namespace AspireWithTesting.Tests;<\/p>\n<p>[TestClass]<br \/>\npublic class ApiTests<br \/>\n{<br \/>\n    [TestMethod]<br \/>\n    public async Task CanGetWeatherForecast()<br \/>\n    {<br \/>\n        var appHost =<br \/>\n            await DistributedApplicationTestingBuilder.CreateAsync&lt;Projects.AspireWithTesting_AppHost&gt;();<br \/>\n        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =&gt;<br \/>\n        {<br \/>\n            clientBuilder.AddStandardResilienceHandler();<br \/>\n        });<\/p>\n<p>        await using var app = await appHost.BuildAsync();<br \/>\n        await app.StartAsync();<\/p>\n<p>        var resourceNotificationService =<br \/>\n            app.Services.GetRequiredService&lt;ResourceNotificationService&gt;();<br \/>\n        await resourceNotificationService<br \/>\n                .WaitForResourceAsync(&#8220;apiservice&#8221;, KnownResourceStates.Running)<br \/>\n                .WaitAsync(TimeSpan.FromSeconds(30));<\/p>\n<p>        var httpClient = app.CreateHttpClient(&#8220;apiservice&#8221;);<br \/>\n        var response = await httpClient.GetAsync(&#8220;\/weatherforecast&#8221;);<\/p>\n<p>        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);<\/p>\n<p>        var forecasts = await response.Content.ReadFromJsonAsync&lt;IEnumerable&lt;WeatherForecast&gt;&gt;();<br \/>\n        Assert.IsNotNull(forecasts);<br \/>\n        Assert.AreEqual(5, forecasts.Count());<br \/>\n    }<\/p>\n<p>    record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);<br \/>\n}<\/p>\n<div class=\"alert alert-primary\">\n<p class=\"alert-divider\"><strong>Note<\/strong><\/p>\n<p>Since the WeatherForecast record is private in the API project, we need to define it in the test project to be able to deserialize the JSON response.<\/p><\/div>\n<p>Once we\u2019ve asserts that the API endpoint returned a 200 OK response, we can then deserialize the JSON response into a collection of WeatherForecast objects and assert against the data. In this case, the data we have for our API is randomly generated so we\u2019re only asserting on the number of records returned, but if we had a database that the data came from, the test could assert against the expected data.<\/p>\n<h2>Video<\/h2>\n\n<h2>Summary<\/h2>\n<p>In this blog post, we\u2019ve explored how to get started with testing in .NET Aspire, allowing us to test scenarios across our distributed applications. We\u2019ve seen how to write tests for the frontend and API services, ensuring that they are running and responding as expected.<\/p>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/testing?pivots=mstest\">.NET Aspire Testing Documentation<\/a><\/p>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/getting-started-with-testing-and-dotnet-aspire\/\">Getting started with testing and .NET Aspire<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Automated testing is an important part of software development, helping ensure that bugs are caught early and regression issues are [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-1264","post","type-post","status-publish","format-standard","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1264","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=1264"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1264\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=1264"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=1264"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=1264"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}