{"id":53826,"date":"2024-09-25T10:05:00","date_gmt":"2024-09-25T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=53826"},"modified":"2025-11-17T14:42:21","modified_gmt":"2025-11-17T22:42:21","slug":"getting-started-with-testing-and-dotnet-aspire","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/getting-started-with-testing-and-dotnet-aspire\/","title":{"rendered":"Getting started with testing and 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 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. 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 Aspire can help us with.<\/p>\n<p>Thankfully, we have the Aspire <a href=\"https:\/\/www.nuget.org\/packages\/aspire.hosting.testing\"><code>Aspire.Hosting.Testing<\/code> NuGet package<\/a> which can help us with this. Let&#8217;s 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&#8217;re going to create a new Aspire Starter App project. This will create a new Aspire application with the AppHost, Service Defaults, an API backend and a Blazor web frontend.<\/p>\n<p>Ensure you have the Aspire workload installed:<\/p>\n<pre><code class=\"language-bash\">dotnet workload update\ndotnet workload install aspire<\/code><\/pre>\n<p>Then create a new project using the <code>aspire-starter<\/code> template:<\/p>\n<pre><code class=\"language-bash\">dotnet new aspire-starter --name AspireWithTesting<\/code><\/pre>\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&#8217;ll use MSTest. This can be created using the <code>aspire-mstest<\/code> template:<\/p>\n<pre><code class=\"language-bash\">dotnet new aspire-mstest --name AspireWithTesting.Tests\ndotnet sln add AspireWithTesting.Tests<\/code><\/pre>\n<blockquote>\n<p>Note: You can have the test project included when creating with the <code>aspire-starter<\/code> template by adding the <code>--test-framework MSTest<\/code> (or other framework) flag.<\/p>\n<\/blockquote>\n<p>The template already references the <code>Aspire.Hosting.Testing<\/code> 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:<\/p>\n<pre><code class=\"language-bash\">dotnet add AspireWithTesting.Tests reference AspireWithTesting.AppHost<\/code><\/pre>\n<h2>Writing Tests<\/h2>\n<p>We&#8217;ll find there is already a stub test file, <code>IntegrationTest1.cs<\/code> in our test project that describes the steps above and provides an example of tests that can be written, but let&#8217;s start from scratch so we can understand what&#8217;s going on. Create a new file called <code>FrontEndTests.cs<\/code> and add the following code:<\/p>\n<pre><code class=\"language-csharp\">namespace AspireWithTesting.Tests;\n\n[TestClass]\npublic class FrontEndTests\n{\n    [TestMethod]\n    public async Task CanGetIndexPage()\n    {\n        var appHost =\n            await DistributedApplicationTestingBuilder\n                    .CreateAsync&lt;Projects.AspireWithTesting_AppHost&gt;();\n        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =&gt;\n        {\n            clientBuilder.AddStandardResilienceHandler();\n        });\n\n        await using var app = await appHost.BuildAsync();\n        await app.StartAsync();\n\n        var resourceNotificationService =\n            app.Services.GetRequiredService&lt;ResourceNotificationService&gt;();\n        await resourceNotificationService\n            .WaitForResourceAsync(\"webfrontend\", KnownResourceStates.Running)\n            .WaitAsync(TimeSpan.FromSeconds(30));\n\n        var httpClient = app.CreateHttpClient(\"webfrontend\");\n        var response = await httpClient.GetAsync(\"\/\");\n\n        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n    }\n}<\/code><\/pre>\n<p>Awesome, the test is written, let&#8217;s run it:<\/p>\n<pre><code class=\"language-bash\">dotnet test<\/code><\/pre>\n<p>And if everything goes to plan, we should see output such as:<\/p>\n<pre><code class=\"language-plaintext\">Test summary: total: 1, failed: 0, succeeded: 1, skipped: 0, duration: 0.9s<\/code><\/pre>\n<h2>Understanding the Test<\/h2>\n<p>Let&#8217;s break down what&#8217;s happening in the test:<\/p>\n<pre><code class=\"language-csharp\">var appHost =\n    await DistributedApplicationTestingBuilder\n            .CreateAsync&lt;Projects.AspireWithTesting_AppHost&gt;();\nappHost.Services.ConfigureHttpClientDefaults(clientBuilder =&gt;\n{\n    clientBuilder.AddStandardResilienceHandler();\n});\n\nawait using var app = await appHost.BuildAsync();\nawait app.StartAsync();<\/code><\/pre>\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 <code>dotnet run<\/code> against the project, but in a test environment which we can control some additional aspects. For example, we&#8217;re injecting the <code>StandardResilienceHandler<\/code> into the <code>HttpClient<\/code> 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<pre><code class=\"language-csharp\">var resourceNotificationService =\n    app.Services.GetRequiredService&lt;ResourceNotificationService&gt;();\nawait resourceNotificationService\n    .WaitForResourceAsync(\"webfrontend\", KnownResourceStates.Running)\n    .WaitAsync(TimeSpan.FromSeconds(30));<\/code><\/pre>\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&#8217;t started and we try to resolve it, we&#8217;re going to get an error. The <code>ResourceNotificationService<\/code> is a service that allows us to wait for a resource to be in a particular state, in this case, we&#8217;re waiting for the <code>webfrontend<\/code> (the name we set in the AppHost) to be in the <code>Running<\/code> state, and we&#8217;re giving it 30 seconds to do so. This pattern would need to be repeated for any other services that we&#8217;re going to interact with, whether directly or indirectly.<\/p>\n<pre><code class=\"language-csharp\">var httpClient = app.CreateHttpClient(\"webfrontend\");\nvar response = await httpClient.GetAsync(\"\/\");\n\nAssert.AreEqual(HttpStatusCode.OK, response.StatusCode);<\/code><\/pre>\n<p>Finally, we can request an instance of <code>HttpClient<\/code> 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&#8217;t 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 <code>200 OK<\/code> 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&#8217;s returning data, we can take it a step further and assert against the data that is returned:<\/p>\n<pre><code class=\"language-csharp\">using System.Net.Http.Json;\n\nnamespace AspireWithTesting.Tests;\n\n[TestClass]\npublic class ApiTests\n{\n    [TestMethod]\n    public async Task CanGetWeatherForecast()\n    {\n        var appHost =\n            await DistributedApplicationTestingBuilder.CreateAsync&lt;Projects.AspireWithTesting_AppHost&gt;();\n        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =&gt;\n        {\n            clientBuilder.AddStandardResilienceHandler();\n        });\n\n        await using var app = await appHost.BuildAsync();\n        await app.StartAsync();\n\n        var resourceNotificationService =\n            app.Services.GetRequiredService&lt;ResourceNotificationService&gt;();\n        await resourceNotificationService\n                .WaitForResourceAsync(\"apiservice\", KnownResourceStates.Running)\n                .WaitAsync(TimeSpan.FromSeconds(30));\n\n        var httpClient = app.CreateHttpClient(\"apiservice\");\n        var response = await httpClient.GetAsync(\"\/weatherforecast\");\n\n        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n        var forecasts = await response.Content.ReadFromJsonAsync&lt;IEnumerable&lt;WeatherForecast&gt;&gt;();\n        Assert.IsNotNull(forecasts);\n        Assert.AreEqual(5, forecasts.Count());\n    }\n\n    record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);\n}<\/code><\/pre>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>Since the <code>WeatherForecast<\/code> <code>record<\/code> is private in the API project, we need to define it in the test project to be able to deserialize the JSON response.<\/div><\/p>\n<p>Once we&#8217;ve asserts that the API endpoint returned a <code>200 OK<\/code> response, we can then deserialize the JSON response into a collection of <code>WeatherForecast<\/code> objects and assert against the data. In this case, the data we have for our API is randomly generated so we&#8217;re 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<p><iframe width=\"800\" height=\"450\" src=\"https:\/\/www.youtube.com\/embed\/bPIu6PZW41Q?si=I7eIqZccEJwHapEl\" allowfullscreen><\/iframe><\/p>\n<h2>Summary<\/h2>\n<p>In this blog post, we&#8217;ve explored how to get started with testing in Aspire, allowing us to test scenarios across our distributed applications. We&#8217;ve seen how to write tests for the frontend and API services, ensuring that they are running and responding as expected.<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/testing?pivots=mstest\">Aspire Testing Documentation<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to improve your software development process with automated testing in Aspire. This post covers the basics of getting started, writing tests for distributed applications, and ensuring your services run smoothly.<\/p>\n","protected":false},"author":40333,"featured_media":58917,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7783],"tags":[4,7768,7693,136],"class_list":["post-53826","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-aspire","tag-net","tag-aspire","tag-cloud-native","tag-testing"],"acf":[],"blog_post_summary":"<p>Learn how to improve your software development process with automated testing in Aspire. This post covers the basics of getting started, writing tests for distributed applications, and ensuring your services run smoothly.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/53826","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/40333"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=53826"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/53826\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/58917"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=53826"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=53826"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=53826"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}