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