1

我有一个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; }
}

这在实践中效果很好,但是在测试时我遇到了问题。

4

1 回答 1

2

我在我的机器上本地尝试,测试通过了。

这是您的组件的简化版本,每次值更改仅调用 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);
    }
}
于 2021-09-11T09:20:00.650 回答