‹ 首页

bunit-testing

@fritzandfriends · 收录于 昨天 · 上游提交 1 个月前

Write bUnit v2 component tests for BlazorWebFormsComponents. Covers BlazorWebFormsTestContext base class, Render() with inline Razor syntax, Shouldly assertions, testing data-bound components, validation components, event callbacks, JS interop mocking, service registration, authentication testing, and xUnit logger integration. Use when writing new component tests, testing complex scenarios, or understanding the BWFC test infrastructure.

适合你,如果正在用 Blazor 开发组件并需要编写测试。

/ 通过 npx 安装 校验哈希
npx oh-my-skill add fritzandfriends/blazorwebformscomponents/bunit-testing
/ 通过 bash 安装
curl -fsSL https://oh-my-skill.com/install.sh | bash -s -- fritzandfriends/blazorwebformscomponents/bunit-testing
/ 已经装过?验证本机副本,不用重装
npx oh-my-skill verify fritzandfriends/blazorwebformscomponents/bunit-testing
安装目标可用 --agent / --scope 或 --to 明确指定;省略时只会在唯一已存在的 agent 目录上自动选择,零命中或多命中会停止并提示。content_hash 缺失或不一致均拒装。
449GitHub stars
~1.9K上下文体积 · 单文件
索引托管

怎么用

技能原文 SKILL.md作者撰写 · MIT · 3154758

bUnit Test Authoring

This skill covers writing new bUnit v2 component tests for the BlazorWebFormsComponents library.

Not for migration. If you need to migrate existing tests from bUnit beta to v2, use the bunit-test-migration skill instead.
Test Infrastructure
BlazorWebFormsTestContext

All BWFC component tests should inherit from BlazorWebFormsTestContext, which extends bUnit's BunitContext with pre-registered services:

public abstract class BlazorWebFormsTestContext : BunitContext
{
    // Pre-configured:
    // - JSInterop.Mode = JSRuntimeMode.Loose
    // - LinkGenerator (mocked)
    // - IHttpContextAccessor (mocked)
    // - IWebHostEnvironment (mocked with temp paths)
    // - IMemoryCache
    // - CacheShim, ServerShim, ClientScriptShim
    // - ILogger<T> (with optional xUnit output)
}

This base class ensures components that depend on WebFormsPageBase shims can render without additional setup.

Test File Location

Tests are .razor files organized by component name:

src/BlazorWebFormsComponents.Test/
├── Button/
│   ├── Click.razor
│   ├── Enabled.razor
│   └── Style.razor
├── GridView/
│   ├── Selection.razor
│   ├── Sorting.razor
│   └── Paging.razor
├── Validations/
│   └── SetFocusOnErrorTests.razor
└── ...
Test Naming Convention

ComponentName_Scenario_ExpectedBehavior

Examples:

  • Button_Click_InvokesHandler
  • GridView_WithDataSource_RendersRows
  • RequiredFieldValidator_BlankInput_DisplaysError
Writing Tests
Basic Pattern
@inherits BlazorWebFormsTestContext

@code {
    [Fact]
    public void Button_Click_InvokesHandler()
    {
        // Arrange
        var clicked = false;
        var cut = Render(@<Button OnClick="() => clicked = true">Submit</Button>);

        // Act
        cut.Find("input").Click();

        // Assert
        clicked.ShouldBeTrue();
    }
}
Testing Component Properties
@code {
    [Fact]
    public void Label_Text_RendersInSpan()
    {
        var cut = Render(@<Label Text="Hello World" />);

        cut.Find("span").TextContent.ShouldBe("Hello World");
    }

    [Fact]
    public void TextBox_Visible_False_RendersNothing()
    {
        var cut = Render(@<TextBox Visible="false" />);

        cut.FindAll("input").Count.ShouldBe(0);
    }
}
Testing Data-Bound Components
@code {
    [Fact]
    public void GridView_WithDataSource_RendersCorrectRowCount()
    {
        var data = new List<TestItem>
        {
            new() { Id = 1, Name = "Item 1" },
            new() { Id = 2, Name = "Item 2" },
            new() { Id = 3, Name = "Item 3" }
        };

        var cut = Render(
            @<GridView ItemType="TestItem" DataSource="data" AutoGenerateColumns="false">
                <Columns>
                    <BoundField ItemType="TestItem" DataField="Name" HeaderText="Name" />
                </Columns>
            </GridView>);

        cut.FindAll("tbody tr").Count.ShouldBe(3);
    }

    public class TestItem
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}
Testing Event Callbacks
@code {
    private GridViewSelectEventArgs _receivedArgs;

    [Fact]
    public void GridView_SelectButton_FiresEventWithCorrectIndex()
    {
        var data = GetTestItems();

        var cut = Render(
            @<GridView ItemType="TestItem" DataSource="data" AutoGenerateColumns="false"
                AutoGenerateSelectButton="true"
                SelectedIndexChanging="@((GridViewSelectEventArgs e) => _receivedArgs = e)">
                <Columns>
                    <BoundField ItemType="TestItem" DataField="Name" HeaderText="Name" />
                </Columns>
            </GridView>);

        // Click Select on the third row
        cut.FindAll("tbody a")
            .Where(a => a.TextContent == "Select")
            .ToList()[2].Click();

        _receivedArgs.ShouldNotBeNull();
        _receivedArgs.NewSelectedIndex.ShouldBe(2);
    }

    [Fact]
    public void GridView_CancelEvent_PreventsAction()
    {
        var data = GetTestItems();

        var cut = Render(
            @<GridView ItemType="TestItem" DataSource="data" AutoGenerateColumns="false"
                AutoGenerateSelectButton="true"
                SelectedIndexChanging="@((GridViewSelectEventArgs e) => e.Cancel = true)">
                <Columns>
                    <BoundField ItemType="TestItem" DataField="Name" HeaderText="Name" />
                </Columns>
            </GridView>);

        cut.FindAll("tbody a").First(a => a.TextContent == "Select").Click();

        // No row should be highlighted since cancel was set
        var rows = cut.FindAll("tbody tr");
        foreach (var row in rows)
        {
            var style = row.GetAttribute("style");
            (style == null || !style.Contains("background-color")).ShouldBeTrue();
        }
    }
}
Testing Validation Components
@using BlazorWebFormsComponents.Enums

@code {
    ForwardRef<InputBase<string>> NameRef = new ForwardRef<InputBase<string>>();

    [Fact]
    public void RequiredFieldValidator_EmptyInput_ShowsError()
    {
        var model = new TestModel();
        var cut = Render(
            @<EditForm Model="@model" OnValidSubmit="() => { }">
                <InputText @ref="NameRef.Current" @bind-Value="model.Name" />
                <RequiredFieldValidator Type="string"
                    ControlRef="@NameRef"
                    ErrorMessage="Name is required." />
            </EditForm>);

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

        cut.Find("span").TextContent.ShouldContain("Name is required");
    }

    public class TestModel { public string Name { get; set; } }
}
Testing JS Interop
@code {
    [Fact]
    public void Component_WithJsCall_InvokesCorrectMethod()
    {
        // Set up specific JS mock
        JSInterop.SetupVoid("bwfc.Validation.SetFocus", _ => true);

        var cut = Render(@<MyComponent SetFocusOnError="true" />);

        // Trigger the action that calls JS
        cut.Find("form").Submit();

        // Verify JS was invoked
        JSInterop.VerifyInvoke("bwfc.Validation.SetFocus");
    }

    [Fact]
    public void Component_WithoutJsCall_DoesNotInvoke()
    {
        var cut = Render(@<MyComponent SetFocusOnError="false" />);

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

        JSInterop.VerifyNotInvoke("bwfc.Validation.SetFocus");
    }
}
Testing with Service Registration
@code {
    [Fact]
    public void Component_WithCustomService_UsesIt()
    {
        Services.AddSingleton<IMyService>(new FakeMyService());

        var cut = Render(@<MyComponent />);

        cut.Find("div").TextContent.ShouldBe("fake-data");
    }
}
Testing with Authentication
@code {
    [Fact]
    public void SecureComponent_AuthenticatedUser_ShowsContent()
    {
        var auth = this.AddTestAuthorization();
        auth.SetAuthorized("testuser");
        auth.SetRoles("Admin", "User");

        var cut = Render(@<LoginView>
            <LoggedInTemplate>Welcome!</LoggedInTemplate>
            <AnonymousTemplate>Please log in.</AnonymousTemplate>
        </LoginView>);

        cut.Markup.ShouldContain("Welcome!");
    }
}
Accessing Component Instance
@code {
    [Fact]
    public void TreeView_Nodes_HasCorrectCount()
    {
        var cut = Render(@<TreeView>
            <Nodes>
                <TreeNode Text="Node 1" />
                <TreeNode Text="Node 2" />
            </Nodes>
        </TreeView>);

        cut.FindComponent<TreeView>().Instance.Nodes.Count.ShouldBe(2);
    }
}
HTML Markup Assertions
@code {
    [Fact]
    public void Button_RendersCorrectHtml()
    {
        var cut = Render(@<Button Text="Submit" CssClass="btn" />);

        // Exact match (whitespace-insensitive)
        cut.MarkupMatches(@"<input type=""submit"" value=""Submit"" class=""btn"" />");
    }
}
Using the xUnit Logger

For complex tests that need diagnostic output:

@using Microsoft.Extensions.Logging

@code {
    private ILogger<MyTest> _logger;

    public MyTest(ITestOutputHelper output) : base(output) { }

    [Fact]
    public void Component_ComplexScenario_LogsDiagnostics()
    {
        _logger = Services.GetService<ILogger<MyTest>>();
        _logger?.LogInformation("Starting complex test");

        var cut = Render(@<MyComponent />);

        _logger?.LogDebug("Rendered successfully, checking output");
        // assertions...
    }
}

Only add logging where diagnostic output helps debugging. Most simple tests don't need it.

C# Code-Behind Tests

For tests that need programmatic component construction (no Razor syntax):

public class IsPostBackTests : IDisposable
{
    private readonly BunitContext _ctx;

    public IsPostBackTests()
    {
        _ctx = new BunitContext();
        _ctx.JSInterop.Mode = JSRuntimeMode.Loose;
        _ctx.Services.AddSingleton<LinkGenerator>(new Mock<LinkGenerator>().Object);
        _ctx.Services.AddSingleton<IHttpContextAccessor>(new Mock<IHttpContextAccessor>().Object);
        // ... register all required services
    }

    [Fact]
    public void Component_SSR_GetRequest_IsPostBackFalse()
    {
        RegisterHttpContextWithMethod("GET");
        var cut = _ctx.Render<TestComponent>();
        cut.Instance.IsPostBackValue.ShouldBeFalse();
    }

    public void Dispose() => _ctx.Dispose();
}

Prefer .razor test files for component rendering tests — they're more readable and support inline Razor syntax. Use .cs files only when you need programmatic control (e.g., testing internal APIs, building render trees manually).

Assertions Reference

| Assertion | Usage | |-----------|-------| | value.ShouldBe(expected) | Exact equality (Shouldly) | | value.ShouldBeTrue() | Boolean true | | value.ShouldNotBeNull() | Non-null check | | value.ShouldContain("text") | String contains | | cut.MarkupMatches("<html>") | HTML comparison (bUnit) | | cut.Find("selector") | CSS selector (throws if not found) | | cut.FindAll("selector") | CSS selector (returns list) | | cut.FindComponent<T>() | Find child component instance |

Running Tests
# Run all component tests
dotnet test src/BlazorWebFormsComponents.Test --nologo

# Run tests for a specific component
dotnet test src/BlazorWebFormsComponents.Test --filter "Button"

# Run with verbose output
dotnet test src/BlazorWebFormsComponents.Test --nologo -v normal
Checklist
  • [ ] Test file is in src/BlazorWebFormsComponents.Test/{ComponentName}/
  • [ ] .razor test inherits BlazorWebFormsTestContext (not raw BunitContext)
  • [ ] Test method name follows ComponentName_Scenario_ExpectedBehavior
  • [ ] Uses Shouldly assertions (ShouldBe, ShouldBeTrue, etc.)
  • [ ] Uses Render(@<Component />) inline Razor syntax
  • [ ] Data-bound tests provide realistic test data
  • [ ] Event tests verify both success and cancel paths
  • [ ] dotnet test src/BlazorWebFormsComponents.Test passes across all target frameworks
按 MIT 许可原样转载,未经改动 · 在 GitHub 查看 →

评论

登录即可评论;带「已验证安装」的,是发布者名下有本店的安装或持有记录。