Skip to content

Integration tests documentation add nunit mstest #35328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
209 changes: 196 additions & 13 deletions aspnetcore/test/integration-tests.md

Large diffs are not rendered by default.

210 changes: 196 additions & 14 deletions aspnetcore/test/integration-tests/includes/integration-tests9.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,22 @@ Test classes implement a *class fixture* interface ([`IClassFixture`](https://xu
The following test class, `BasicTests`, uses the `WebApplicationFactory` to bootstrap the SUT and provide an <xref:System.Net.Http.HttpClient> to a test method, `Get_EndpointsReturnSuccessAndCorrectContentType`. The method verifies the response status code is successful (200-299) and the `Content-Type` header is `text/html; charset=utf-8` for several app pages.

<xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory%601.CreateClient> creates an instance of `HttpClient` that automatically follows redirects and handles cookies.

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/BasicTests.cs?name=snippet1)]

:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/BasicTests.cs?name=snippet1":::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is easier to read and recommended, I could be wrong though.

Suggested change
:::code language="csharp" source="../snippets/xunit/IntegrationTests/BasicTests.cs?name=snippet1":::
:::code language="csharp" source="../snippets/xunit/IntegrationTests/BasicTests.cs" id="snippet1":::


:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/BasicTests.cs?name=snippet1":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/BasicTests.cs?name=snippet1":::

:::zone-end

By default, non-essential cookies aren't preserved across requests when the [General Data Protection Regulation consent policy](xref:security/gdpr) is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see [Essential cookies](xref:security/gdpr#essential-cookies).

Expand All @@ -83,7 +97,21 @@ Web host configuration can be created independently of the test classes by inher

1. Inherit from `WebApplicationFactory` and override <xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory%601.ConfigureWebHost%2A>. The <xref:Microsoft.AspNetCore.Hosting.IWebHostBuilder> allows the configuration of the service collection with [`IWebHostBuilder.ConfigureServices`](xref:Microsoft.AspNetCore.Hosting.IWebHostBuilder.ConfigureServices%2A)

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/CustomWebApplicationFactory.cs?name=snippet1)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/CustomWebApplicationFactory.cs?name=snippet1":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/CustomWebApplicationFactory.cs?name=snippet1":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/CustomWebApplicationFactory.cs?name=snippet1":::

:::zone-end

Database seeding in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample) is performed by the `InitializeDbForTests` method. The method is described in the [Integration tests sample: Test app organization](#test-app-organization) section.

Expand All @@ -100,13 +128,41 @@ Web host configuration can be created independently of the test classes by inher
-->
2. Use the custom `CustomWebApplicationFactory` in test classes. The following example uses the factory in the `IndexPageTests` class:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs?name=snippet1)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs?name=snippet1":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/IndexPageTests.cs?name=snippet1":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/IndexPageTests.cs?name=snippet1":::

:::zone-end

The sample app's client is configured to prevent the `HttpClient` from following redirects. As explained later in the [Mock authentication](#mock-authentication) section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a `Location` header.

3. A typical test uses the `HttpClient` and helper methods to process the request and the response:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs?name=snippet2)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs?name=snippet2":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/IndexPageTests.cs?name=snippet2":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/IndexPageTests.cs?name=snippet2":::

:::zone-end

Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's [data protection antiforgery system](xref:security/data-protection/introduction). In order to arrange for a test's POST request, the test app must:

Expand Down Expand Up @@ -139,15 +195,43 @@ The `Post_DeleteMessageHandler_ReturnsRedirectToRoot` test method of the [sample

Because another test in the `IndexPageTests` class performs an operation that deletes all of the records in the database and may run before the `Post_DeleteMessageHandler_ReturnsRedirectToRoot` method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the `messages` form in the SUT is simulated in the request to the SUT:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs?name=snippet3)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs?name=snippet3":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/IndexPageTests.cs?name=snippet3":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/IndexPageTests.cs?name=snippet3":::

:::zone-end

## Client options

See the <xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions> page for defaults and available options when creating `HttpClient` instances.

Create the `WebApplicationFactoryClientOptions` class and pass it to the <xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory%601.CreateClient> method:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs?name=snippet1)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs?name=snippet1":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/IndexPageTests.cs?name=snippet1":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/IndexPageTests.cs?name=snippet1":::

:::zone-end

***NOTE:*** To avoid HTTPS redirection warnings in logs when using HTTPS Redirection Middleware, set `BaseAddress = new Uri("https://localhost")`

Expand Down Expand Up @@ -192,11 +276,39 @@ To test the service and quote injection in an integration test, a mock service i

`IntegrationTests.IndexPageTests.cs`:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs?name=snippet4)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs?name=snippet4":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/IndexPageTests.cs?name=snippet4":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/IndexPageTests.cs?name=snippet4":::

:::zone-end

`ConfigureTestServices` is called, and the scoped service is registered:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs?name=snippet5&highlight=7-10,17,20-21)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs?name=snippet5&highlight=7-10,17,20-21":::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For highlights you can do the following:

Suggested change
:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs?name=snippet5&highlight=7-10,17,20-21":::
:::code language="csharp" source="../snippets/xunit/IntegrationTests/IndexPageTests.cs" id="snippet5" highlight="7-10,17,20-21":::


:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/IndexPageTests.cs?name=snippet5&highlight=7-10,17,20-21":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/IndexPageTests.cs?name=snippet5&highlight=7-10,17,20-21":::

:::zone-end

The markup produced during the test's execution reflects the quote text supplied by `TestQuoteService`, thus the assertion passes:

Expand All @@ -218,7 +330,21 @@ In the SUT, the `/SecurePage` page uses an <xref:Microsoft.Extensions.Dependency

In the `Get_SecurePageRedirectsAnUnauthenticatedUser` test, a <xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions> is set to disallow redirects by setting <xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions.AllowAutoRedirect> to `false`:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs?name=snippet2)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/AuthTests.cs?name=snippet2":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/AuthTests.cs?name=snippet2":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/AuthTests.cs?name=snippet2":::

:::zone-end

By disallowing the client to follow the redirect, the following checks can be made:

Expand All @@ -227,11 +353,39 @@ By disallowing the client to follow the redirect, the following checks can be ma

The test app can mock an <xref:Microsoft.AspNetCore.Authentication.AuthenticationHandler%601> in <xref:Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.ConfigureTestServices%2A> in order to test aspects of authentication and authorization. A minimal scenario returns an <xref:Microsoft.AspNetCore.Authentication.AuthenticateResult.Success%2A?displayProperty=nameWithType>:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs?name=snippet4&highlight=11-18)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/AuthTests.cs?name=snippet4&highlight=11-18":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/AuthTests.cs?name=snippet4&highlight=11-18":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/AuthTests.cs?name=snippet4&highlight=11-18":::

:::zone-end

The `TestAuthHandler` is called to authenticate a user when the authentication scheme is set to `TestScheme` where `AddAuthentication` is registered for `ConfigureTestServices`. It's important for the `TestScheme` scheme to match the scheme your app expects. Otherwise, authentication won't work.

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs?name=snippet3&highlight=7-12)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/IntegrationTests/AuthTests.cs?name=snippet3&highlight=7-12":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/IntegrationTests/AuthTests.cs?name=snippet3&highlight=7-12":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/IntegrationTests/AuthTests.cs?name=snippet3&highlight=7-12":::

:::zone-end

For more information on `WebApplicationFactoryClientOptions`, see the [Client options](#client-options) section.

Expand All @@ -243,7 +397,21 @@ See [this GitHub repository](https://github.com/blowdart/idunno.Authentication/t

Set the [environment](xref:fundamentals/environments) in the custom application factory:

[!code-csharp[](~/../AspNetCore.Docs.Samples/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/CustomWebApplicationFactory.cs?name=snippet1&highlight=36)]
:::zone pivot="xunit"

:::code language="csharp" source="../snippets/xunit/CustomWebApplicationFactory.cs?name=snippet1&highlight=36":::

:::zone-end
:::zone pivot="mstest"

:::code language="csharp" source="../snippets/mstest/CustomWebApplicationFactory.cs?name=snippet1&highlight=36":::

:::zone-end
:::zone pivot="nunit"

:::code language="csharp" source="../snippets/nunit/CustomWebApplicationFactory.cs?name=snippet1&highlight=36":::

:::zone-end

## How the test infrastructure infers the app content root path

Expand All @@ -263,8 +431,22 @@ To disable shadow copying when using xUnit, create a `xunit.runner.json` file in

## Disposal of objects

:::zone pivot="xunit"

After the tests of the `IClassFixture` implementation are executed, <xref:Microsoft.AspNetCore.TestHost.TestServer> and <xref:System.Net.Http.HttpClient> are disposed when xUnit disposes of the [`WebApplicationFactory`](xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory%601). If objects instantiated by the developer require disposal, dispose of them in the `IClassFixture` implementation. For more information, see [Implementing a Dispose method](/dotnet/standard/garbage-collection/implementing-dispose).

:::zone-end
:::zone pivot="mstest"

After the tests of the `TestClass` are executed, <xref:Microsoft.AspNetCore.TestHost.TestServer> and <xref:System.Net.Http.HttpClient> are disposed when MSTest disposes of the [`WebApplicationFactory`](xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory%601) in the `ClassCleanup` method. If objects instantiated by the developer require disposal, dispose of them in the `ClassCleanup` method. For more information, see [Implementing a Dispose method](/dotnet/standard/garbage-collection/implementing-dispose).

:::zone-end
:::zone pivot="nunit"

After the tests of the test class are executed, <xref:Microsoft.AspNetCore.TestHost.TestServer> and <xref:System.Net.Http.HttpClient> are disposed when NUnit disposes of the [`WebApplicationFactory`](xref:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory%601) in the `TearDown` method. If objects instantiated by the developer require disposal, dispose of them in the `TearDown` method. For more information, see [Implementing a Dispose method](/dotnet/standard/garbage-collection/implementing-dispose).

:::zone-end

## Integration tests sample

The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample) is composed of two apps:
Expand Down Expand Up @@ -324,4 +506,4 @@ The SUT's database context is registered in `Program.cs`. The test app's `builde
* <xref:mvc/controllers/testing>
* [Basic tests for authentication middleware](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test)

:::moniker-end
:::moniker-end
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Data.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesProject.Data;

namespace RazorPagesProject.Tests;

// <snippet1>
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class {
protected override void ConfigureWebHost(IWebHostBuilder builder) {
builder.ConfigureServices(services => {
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

services.Remove(dbContextDescriptor);

var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));

services.Remove(dbConnectionDescriptor);

// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container => {
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();

return connection;
});

services.AddDbContext<ApplicationDbContext>((container, options) => {
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});

builder.UseEnvironment("Development");
}
}
// </snippet1>
Loading