Skip to content

baoduy/sample-aspire-dotnet-testing

Repository files navigation

Sample using Aspire .NET For Integration Tests Automation

This repository demonstrates how to set up a .NET project with MediatR, Entity Framework Core, PostgreSQL, and xUnit for unit testing. Follow the steps below to get started.

Prerequisites

Project Structure

The project is structured as follows:

  • Api: Contains the main API project.
  • Aspire.Host: Contains the hosting configuration for the API.
  • Aspire.ServiceDefaults: Contains shared services and configurations.
  • Aspire.Tests: Contains the unit tests for the API.

Dependency Graph

DependencyGraph

Setup Instructions

1. Clone the Repository

git clone https://github.com/yourusername/sample-aspire-dotnet-unittests.git
cd sample-aspire-dotnet-unittests

2. Configure the Database

Ensure PostgreSQL is installed and running on your machine. Update the connection string in appsettings.Development.json to match your PostgreSQL configuration.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "Db": "Host=localhost;Database=ProductDb;Username=yourusername;Password=yourpassword"
  }

3. Install Dependencies

Restore the NuGet packages for all projects.

dotnet restore

4. Update Program.cs

Ensure the Program.cs file is configured to use PostgreSQL and MediatR.

using System.Reflection;
using Api.Configs;
using Api.Data;
using Api.Endpoints;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Db")));

builder.Services.AddMediatR(op => op.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => options.CustomSchemaIds(t =>
    t.GetCustomAttributes<SwaggerSchemaIdAttribute>().SingleOrDefault()?.SchemaId ??
    SwashbuckleHelpers.DefaultSchemaIdSelector(t)));
//Aspire Support
builder.AddServiceDefaults();

var app = builder.Build();
if (app.Environment.IsDevelopment())
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.MapDefaultEndpoints();
//Aspire Support
app.MapDefaultEndpoints();
app.MapProductEndpoints();
app.UseHttpsRedirection();
app.MapProductEndpoints();
await app.RunAsync();

//This Startup endpoint for Unit Tests
namespace Api
{
    public class Program
    {
    }
}

5. Define the Entity

Create the Product entity in the Entities folder.

namespace Api.Data;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

6. Create the DbContext

Define the AppDbContext class to manage the entity.

using Microsoft.EntityFrameworkCore;

namespace Api.Data;

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<Product> Products { get; set; }
}

7. Create MediatR Handlers

Implement MediatR handlers for CRUD operations.

Create Product

using Api.Configs;
using Api.Data;
using MediatR;

namespace Api.Endpoints.Products;

public class CreateProduct
{
    [SwaggerSchemaId($"{nameof(CreateProduct)}{nameof(Command)}")]
    public class Command : IRequest<int>
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }

    public class Handler(AppDbContext context) : IRequestHandler<Command, int>
    {
        public async Task<int> Handle(Command request, CancellationToken cancellationToken)
        {
            var product = new Product { Name = request.Name, Price = request.Price };
            context.Products.Add(product);

            await context.SaveChangesAsync(cancellationToken);
            return product.Id;
        }
    }
}

Read Product

using Api.Configs;
using Api.Data;
using MediatR;

namespace Api.Endpoints.Products;

public class GetProduct
{
    [SwaggerSchemaId($"{nameof(GetProduct)}{nameof(Query)}")]
    public class Query : IRequest<Product?>
    {
        public int Id { get; set; }
    }

    public class Handler(AppDbContext context) : IRequestHandler<Query, Product?>
    {
        public async Task<Product?> Handle(Query request, CancellationToken cancellationToken)
            => await context.Products.FindAsync([request.Id], cancellationToken:cancellationToken);
    }

Update Product

using Api.Configs;
using Api.Data;
using MediatR;

namespace Api.Endpoints.Products;

public class UpdateProduct
{
    [SwaggerSchemaId($"{nameof(UpdateProduct)}{nameof(Command)}")]
    public class Command : IRequest
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }

    public class Handler(AppDbContext context) : IRequestHandler<Command>
    {
        public async Task Handle(Command request, CancellationToken cancellationToken)
        {
            var product = await context.Products.FindAsync([request.Id], cancellationToken);
            if (product == null) throw new Exception("Product not found");

            product.Name = request.Name;
            product.Price = request.Price;

            await context.SaveChangesAsync(cancellationToken);
        }
    }
}

Delete Product

using Api.Configs;
using Api.Data;
using MediatR;

namespace Api.Endpoints.Products;

public class DeleteProduct
{
    [SwaggerSchemaId($"{nameof(DeleteProduct)}{nameof(Command)}")]
    public class Command : IRequest
    {
        public int Id { get; set; }
    }

    public class Handler(AppDbContext context) : IRequestHandler<Command>
    {
        public async Task Handle(Command request, CancellationToken cancellationToken)
        {
            var product = await context.Products.FindAsync([request.Id],cancellationToken );
            if (product == null) throw new Exception("Product not found");

            context.Products.Remove(product);
            await context.SaveChangesAsync(cancellationToken);
        }
    }
}

8. Map Endpoints

Map the endpoints in ProductEndPointMapping.cs.

using Api.Endpoints.Products;
using MediatR;

namespace Api.Endpoints;

public static class ProductEndPointMapping
{
    public static WebApplication  MapProductEndpoints(this WebApplication app)
    {
         var group = app.MapGroup("/products")
             .WithOpenApi()
             .WithTags("ProductApis")
             .WithDescription($"The endpoints of Products")
             .WithSummary($"The endpoints of Products");

         group.MapPost("", async (IMediator mediator, CreateProduct.Command command) =>
         {
             var id = await mediator.Send(command);
             return Results.Created($"/products/{id}", id);
         });

         group.MapGet("{id:int}", async (IMediator mediator, int id) =>
         {
             var product = await mediator.Send(new GetProduct.Query { Id = id });
             return product is not null ? Results.Ok(product) : Results.NotFound();
         });

         group.MapPut("{id:int}", async (IMediator mediator, int id, UpdateProduct.Command command) =>
         {
             if (id != command.Id) return Results.BadRequest();
             await mediator.Send(command);
             return Results.NoContent();
         });

         group.MapDelete("{id:int}", async (IMediator mediator, int id) =>
         {
             await mediator.Send(new DeleteProduct.Command { Id = id });
             return Results.NoContent();
         });

        return app;
    }
}

9. Configure Hosting

Ensure the hosting configuration is set up correctly in Aspire.Host.

var builder = DistributedApplication.CreateBuilder(args);

//Database
var postgres = builder.AddPostgres("postgres").PublishAsConnectionString();
var db = postgres.AddDatabase("Db");

//Internal API
builder.AddProject<Projects.Api>("api")
    .WithReference(db);
    //.WithHttpEndpoint(5105);

10. Create Unit Tests

Create a test class to test the CRUD operations.

Add xUnit and Related Packages

Ensure the Aspire.Tests.csproj file includes the necessary packages.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <IsPackable>false</IsPackable>
        <IsTestProject>true</IsTestProject>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Aspire.Hosting.Testing" Version="8.2.0"/>
        <PackageReference Include="coverlet.collector" Version="6.0.2"/>
        <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
        <PackageReference Include="xunit" Version="2.9.0"/>
        <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
    </ItemGroup>

    <ItemGroup>
        <Using Include="System.Net"/>
        <Using Include="Microsoft.Extensions.DependencyInjection"/>
        <Using Include="Aspire.Hosting.ApplicationModel"/>
        <Using Include="Aspire.Hosting.Testing"/>
        <Using Include="Xunit"/>
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\Api\Api.csproj" />
      <ProjectReference Include="..\Aspire.Host\Aspire.Host.csproj" />
    </ItemGroup>

</Project>

Create Test Fixture

Create a test fixture to set up the test environment.

using Aspire.Hosting;
using Aspire.Tests.Extensions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace Aspire.Tests.Fixtures;

public sealed class ApiFixture : WebApplicationFactory<Api.Program>, IAsyncLifetime
{
    private readonly IHost _app;

    private readonly IResourceBuilder<PostgresServerResource> _postgres;
    private string? _postgresConnectionString;

    public ApiFixture()
    {
        var options = new DistributedApplicationOptions
        {
            AssemblyName = typeof(ApiFixture).Assembly.FullName,
            DisableDashboard = true
        };
        var builder = DistributedApplication.CreateBuilder(options);

        _postgres = builder.AddPostgres("postgres").PublishAsConnectionString();
        _app = builder.Build();
    }

    protected override IHost CreateHost(IHostBuilder builder)
    {
        builder.ConfigureHostConfiguration(config =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string?>

                { "ConnectionStrings:Db", _postgresConnectionString },
            }!);
        });

        var host = base.CreateHost(builder);
        host.EnsureDbCreated().GetAwaiter().GetResult();
        return host;
    }

    public new async Task DisposeAsync()
    {
        await base.DisposeAsync();
        await _app.StopAsync();
        if (_app is IAsyncDisposable asyncDisposable)
        {
            await asyncDisposable.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
            _app.Dispose();
        }
    }

    public async Task InitializeAsync()
    {
        var resourceNotificationService = _app.Services.GetRequiredService<ResourceNotificationService>();
        await _app.StartAsync();

Create Test Class

Implement the test class to test the CRUD operations.

using System.Net.Http.Json;
using Api.Data;
using Api.Endpoints.Products;
using Aspire.Tests.Fixtures;
using Xunit.Abstractions;

namespace Aspire.Tests;

public class ProductEndpointsTests(ApiFixture fixture, ITestOutputHelper output) : IClassFixture<ApiFixture>
{
    private readonly HttpClient _client = fixture.CreateClient();

     [Fact]
    public async Task CreateProduct_ReturnsCreatedProduct()
    {
        // Arrange
        var command = new CreateProduct.Command { Name = "Test Product", Price = 10.99m };
        // Act
        var response = await _client.PostAsJsonAsync("/products", command);

        // Assert
        response.EnsureSuccessStatusCode();
        var productId = await response.Content.ReadFromJsonAsync<int>();
        Assert.True(productId > 0);
    }

    [Fact]
    public async Task GetProduct_ReturnsProduct()
    {
        // Arrange
        var command = new CreateProduct.Command { Name = "Test Product", Price = 10.99m };
        var createResponse = await _client.PostAsJsonAsync("/products", command);
        var productId = await createResponse.Content.ReadFromJsonAsync<int>();

        // Act
        var response = await _client.GetAsync($"/products/{productId}");

        // Assert
        response.EnsureSuccessStatusCode();
        var product = await response.Content.ReadFromJsonAsync<Product>();
        Assert.NotNull(product);
        Assert.Equal("Test Product", product.Name);
        Assert.Equal(10.99m, product.Price);
    }

    [Fact]
    public async Task UpdateProduct_ReturnsNoContent()
    {
        // Arrange
        var command = new CreateProduct.Command { Name = "Test Product", Price = 10.99m };
        var createResponse = await _client.PostAsJsonAsync("/products", command);
        var productId = await createResponse.Content.ReadFromJsonAsync<int>();

        var updateCommand = new UpdateProduct.Command { Id = productId, Name = "Updated Product", Price = 20.99m };

        // Act
        var response = await _client.PutAsJsonAsync($"/products/{productId}", updateCommand);

        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
    }

    [Fact]
    public async Task DeleteProduct_ReturnsNoContent()
    {
        // Arrange
        var command = new CreateProduct.Command { Name = "Test Product", Price = 10.99m };
        var createResponse = await _client.PostAsJsonAsync("/products", command);
        var productId = await createResponse.Content.ReadFromJsonAsync<int>();

        // Act
        var response = await _client.DeleteAsync($"/products/{productId}");

        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
    }
}

11. Run the Tests

Run the tests using the .NET CLI or your preferred IDE.

dotnet test

Conclusion

This setup provides a basic CRUD implementation using MediatR, Entity Framework Core, PostgreSQL, and xUnit for unit testing. You can extend this setup to include more features and tests as needed.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages