Bunit 2 方式绑定

Bunit 2 way-binding

我有一个 Search 组件,它实现了一个去抖定时器,因此它不会调用 ValueChanged(因此不会立即更新与之相关的 属性)。

我的问题

bUnit 测试似乎没有双向绑定我正在更新的值。

测试代码

private string StringProperty { get; set; }

[Fact]
public async Task AfterDebounce_ValueUpdates()
{
    var myString = "";
    var cut = RenderComponent<Search>(parameters => parameters
            .Add(p => p.Value, StringProperty)
            .Add(p => p.ValueChanged, (s) => myString = s)
        );

    var input = cut.Find("input");
    input.Input("unit test");

    Assert.Equal("unit test", cut.Instance.Value);
    Assert.NotEqual("unit test", myString);
    //Assert.NotEqual("unit test", StringProperty);

    await Task.Delay(5000);

    Assert.Equal("unit test", myString);
    //Assert.Equal("unit test", StringProperty);
}

我原以为注释掉的部分会起作用(因为它们与 ValueChanged 做同样的事情来更新 属性),但它们失败了。

组件

public class Search : ComponentBase
{    
    [Parameter] public string? Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }

    [DisallowNull] public ElementReference? Element { get; protected set; }

    private System.Timers.Timer timer = null;
    protected string? CurrentValue {
        get => Value;
        set {
            var hasChanged = !EqualityComparer<string>.Default.Equals(value, Value);
            if (hasChanged)
            {
                Value = value;

                DisposeTimer();
                timer = new System.Timers.Timer(350);
                timer.Elapsed += TimerElapsed_TickAsync;
                timer.Enabled = true;
                timer.Start();
            }
        }
    }

    private void DisposeTimer()
    {
        if (timer != null)
        {
            timer.Enabled = false;
            timer.Elapsed -= TimerElapsed_TickAsync;
            timer.Dispose();
            timer = null;
        }
    }

    private async void TimerElapsed_TickAsync(
        object sender,
        EventArgs e)
    {
        await ValueChanged.InvokeAsync(Value);
    }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(10, "input");
        builder.AddAttribute(20, "type", "text");
        builder.AddAttribute(60, "value", BindConverter.FormatValue(CurrentValue));
        builder.AddAttribute(70, "oninput", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValue = __value, CurrentValue));
        builder.AddElementReferenceCapture(80, __inputReference => Element = __inputReference);
        builder.CloseElement();
    }
}

如何使用:

可以像这样使用,只要 Query 更新,网格就会更新。

<Search @bind-Value=Query />
<Grid Query=@Query />

@code {
    private string? Query { get; set; }
}

这在实践中运行良好,但在测试时我遇到了问题。

我在自己的机器上本地试过,测试通过

这是您的组件的简化版本,每次值更改时只调用 TimerElapsed_TickAsync 一次,而不是每次计时器用完时调用 TimerElapsed_TickAsync (AutoReset 默认为 true),以及两种不同的编写测试的方法两者都在我的机器上通过:

public class Search : ComponentBase, IDisposable
{
    private readonly Timer timer;

    [Parameter] public string? Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    [DisallowNull] public ElementReference? Element { get; protected set; }

    public Search()
    {
        timer = new Timer(350);
        timer.Elapsed += TimerElapsed_TickAsync;
        timer.Enabled = true;
        timer.AutoReset = false;
    }

    protected string? CurrentValue
    {
        get => Value;
        set
        {
            var hasChanged = !EqualityComparer<string>.Default.Equals(value, Value);
            if (hasChanged)
            {
                RestartTimer();
                Value = value;
            }
        }
    }

    private void RestartTimer()
    {
        if (timer.Enabled)
            timer.Stop();
        timer.Start();
    }

    private void TimerElapsed_TickAsync(object sender, EventArgs e) 
        => ValueChanged.InvokeAsync(Value);

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(10, "input");
        builder.AddAttribute(20, "type", "text");
        builder.AddAttribute(60, "value", BindConverter.FormatValue(CurrentValue));
        builder.AddAttribute(70, "oninput", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValue = __value, CurrentValue));
        builder.AddElementReferenceCapture(80, __inputReference => Element = __inputReference);
        builder.CloseElement();
    }

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

以及测试的C#版本:

[Fact]
public async Task AfterDebounce_ValueUpdates()
{
    var expected = "test input";
    var count = 0;
    var value = "";
    var cut = RenderComponent<Search>(parameters => parameters
            .Add(p => p.Value, value)
            .Add(p => p.ValueChanged, (s) =>
            {
                value = s;
                count++;
            })
        );

    cut.Find("input").Input(expected);

    await Task.Delay(350);

    Assert.Equal(1, count);
    Assert.Equal(expected, value);
}

和测试的 .razor 版本(也就是写在 .razor 文件中):

@inherits TestContext
@code
{
    [Fact]
    public async Task AfterDebounce_ValueUpdates()
    {
        var expected = "test input";
        var value = "";
        var cut = Render(@<Search @bind-Value="value" /> );

        cut.Find("input").Input(expected);

        await Task.Delay(350);

        Assert.Equal(expected, value);
    }
}