Agent Skills: Blazor Testing

Testing Blazor apps with bUnit, xUnit, WebApplicationFactory, and integration testing patterns

UncategorizedID: lobbi-docs/claude/blazor-testing

Install this agent skill to your local

pnpm dlx add-skill https://github.com/markus41/claude/tree/HEAD/plugins/dotnet-blazor/skills/blazor-testing

Skill Files

Browse the full folder contents for blazor-testing.

Download Skill

Loading file tree…

plugins/dotnet-blazor/skills/blazor-testing/SKILL.md

Skill Metadata

Name
blazor-testing
Description
Testing Blazor apps with bUnit, xUnit, WebApplicationFactory, and integration testing patterns

Blazor Testing

Test Project Setup

dotnet new xunit -n MyApp.Tests
dotnet add MyApp.Tests package bunit
dotnet add MyApp.Tests package FluentAssertions
dotnet add MyApp.Tests package NSubstitute
dotnet add MyApp.Tests package Microsoft.AspNetCore.Mvc.Testing
dotnet add MyApp.Tests package Testcontainers.PostgreSql  # For real DB tests

bUnit Component Tests

public sealed class CounterTests : TestContext
{
    [Fact]
    public void Counter_StartsAtZero()
    {
        var cut = RenderComponent<Counter>();
        cut.Find("p").TextContent.Should().Contain("0");
    }

    [Fact]
    public void Counter_IncrementsOnClick()
    {
        var cut = RenderComponent<Counter>();
        cut.Find("button").Click();
        cut.Find("p").TextContent.Should().Contain("1");
    }
}

Testing with Parameters and Services

public sealed class ProductListTests : TestContext
{
    [Fact]
    public async Task Should_Display_Products()
    {
        var mockService = Substitute.For<IProductService>();
        mockService.GetAllAsync(Arg.Any<CancellationToken>())
            .Returns(new List<ProductDto>
            {
                new(1, "Widget", 9.99m),
                new(2, "Gadget", 19.99m)
            });
        Services.AddSingleton(mockService);

        var cut = RenderComponent<ProductList>();
        cut.WaitForState(() => cut.FindAll(".product-card").Count > 0);

        cut.FindAll(".product-card").Should().HaveCount(2);
        cut.Markup.Should().Contain("Widget");
    }
}

Testing Forms

[Fact]
public void Form_Validates_Required_Fields()
{
    var cut = RenderComponent<CreateProductForm>();

    cut.Find("form").Submit();

    cut.FindAll(".validation-message").Should().NotBeEmpty();
}

[Fact]
public void Form_Submits_Valid_Data()
{
    var mockService = Substitute.For<IProductService>();
    Services.AddSingleton(mockService);

    var cut = RenderComponent<CreateProductForm>();
    cut.Find("#name").Change("New Product");
    cut.Find("#price").Change("29.99");
    cut.Find("form").Submit();

    mockService.Received(1).CreateAsync(
        Arg.Is<CreateProductRequest>(r => r.Name == "New Product"),
        Arg.Any<CancellationToken>());
}

API Integration Tests

public sealed class ProductApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.RemoveAll<AppDbContext>();
                services.AddDbContext<AppDbContext>(opts =>
                    opts.UseInMemoryDatabase($"Test-{Guid.NewGuid()}"));
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/products");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }

    [Theory]
    [InlineData("", HttpStatusCode.BadRequest)]
    [InlineData("Valid Name", HttpStatusCode.Created)]
    public async Task CreateProduct_ValidatesInput(string name, HttpStatusCode expected)
    {
        var response = await _client.PostAsJsonAsync("/api/products",
            new { Name = name, Price = 10.0 });
        response.StatusCode.Should().Be(expected);
    }
}

Test with Real Database (Testcontainers)

public sealed class ProductDbTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithImage("postgres:16")
        .Build();

    public async Task InitializeAsync() => await _postgres.StartAsync();
    public async Task DisposeAsync() => await _postgres.DisposeAsync();

    [Fact]
    public async Task Can_Create_And_Read_Product()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseNpgsql(_postgres.GetConnectionString())
            .Options;

        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        db.Products.Add(new Product { Name = "Test", Price = 9.99m });
        await db.SaveChangesAsync();

        var product = await db.Products.FirstAsync();
        product.Name.Should().Be("Test");
    }
}