Skip to content

Commit f8e8643

Browse files
committed
Enhance test coverage for BindingConverterResolver and BindingHookEvaluator with additional unit tests
1 parent f218f20 commit f8e8643

File tree

5 files changed

+568
-36
lines changed

5 files changed

+568
-36
lines changed

src/ReactiveUI/Bindings/Property/Internal/BindingConverterResolver.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace ReactiveUI;
1919
[RequiresUnreferencedCode("Uses RxConverters and Splat which may require dynamic type resolution")]
2020
internal class BindingConverterResolver : IBindingConverterResolver
2121
{
22-
private static readonly ConcurrentDictionary<(Type fromType, Type? toType), ISetMethodBindingConverter?> _setMethodCache = new();
22+
private static readonly ConcurrentDictionary<(Type fromType, Type? toType), Func<object?, object?, object?[]?, object?>?> _setMethodCache = new();
2323

2424
/// <inheritdoc/>
2525
public object? GetBindingConverter(Type fromType, Type toType) =>
@@ -33,18 +33,20 @@ internal class BindingConverterResolver : IBindingConverterResolver
3333
return null;
3434
}
3535

36-
var converter = _setMethodCache.GetOrAdd(
36+
return _setMethodCache.GetOrAdd(
3737
(fromType, toType),
38-
static key => ResolveBestSetMethodConverter(key.fromType, key.toType));
39-
40-
if (converter is null)
41-
{
42-
return null;
43-
}
44-
45-
// Adapt the converter's contract to the local call shape expected by SetThenGet.
46-
// Note: do not store this delegate in the cache; the cache stores the converter instance.
47-
return (currentValue, newValue, indexParameters) => converter.PerformSet(currentValue, newValue, indexParameters);
38+
static key =>
39+
{
40+
var converter = ResolveBestSetMethodConverter(key.fromType, key.toType);
41+
if (converter is null)
42+
{
43+
return null;
44+
}
45+
46+
// Adapt the converter's contract to the local call shape expected by SetThenGet.
47+
// Cache the delegate to ensure reference equality for repeated calls.
48+
return (currentValue, newValue, indexParameters) => converter.PerformSet(currentValue, newValue, indexParameters);
49+
});
4850
}
4951

5052
/// <summary>

src/ReactiveUI/Bindings/Property/Internal/BindingHookEvaluator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public bool EvaluateBindingHooks<TViewModel, TView>(
4141
Func<IObservedChange<object, object?>[]> vmFetcher = vmExpression is not null
4242
? (() =>
4343
{
44+
if (viewModel is null)
45+
{
46+
return [];
47+
}
48+
4449
vmChainGetter!.TryGetAllValues(viewModel, out var fetchedValues);
4550
return fetchedValues;
4651
})

src/tests/ReactiveUI.Tests/Bindings/Property/Unit/BindingConverterResolverTests.cs

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
// The .NET Foundation licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
55

6+
using ReactiveUI.Builder;
7+
using ReactiveUI.Tests.Utilities.AppBuilder;
8+
69
namespace ReactiveUI.Tests;
710

811
/// <summary>
912
/// Unit tests for <see cref="BindingConverterResolver"/>.
1013
/// </summary>
1114
/// <remarks>
12-
/// These tests verify converter resolution logic without manipulating global Splat state.
13-
/// Tests use the real implementation and rely on converters registered in the DI container.
15+
/// These tests verify converter resolution logic.
16+
/// Tests use the Executor paradigm to manage AppBuilder state and registrations.
1417
/// </remarks>
18+
[TestExecutor<BindingConverterResolverTests.Executor>]
1519
public class BindingConverterResolverTests
1620
{
1721
/// <summary>
@@ -24,7 +28,7 @@ public async Task GetBindingConverter_WithRegisteredTypedConverter_ReturnsConver
2428
// Arrange
2529
var resolver = new BindingConverterResolver();
2630

27-
// Act - IntegerToStringTypeConverter is registered by default in RxConverters
31+
// Act - IntegerToStringTypeConverter is registered by default via WithCoreServices
2832
var converter = resolver.GetBindingConverter(typeof(int), typeof(string));
2933

3034
// Assert
@@ -59,10 +63,12 @@ public async Task GetSetMethodConverter_WithCaching_ReturnsSameInstance()
5963
var resolver = new BindingConverterResolver();
6064

6165
// Act
62-
var converter1 = resolver.GetSetMethodConverter(typeof(int), typeof(string));
63-
var converter2 = resolver.GetSetMethodConverter(typeof(int), typeof(string));
66+
// Use MockType which has a registered set method converter
67+
var converter1 = resolver.GetSetMethodConverter(typeof(MockType), typeof(MockType));
68+
var converter2 = resolver.GetSetMethodConverter(typeof(MockType), typeof(MockType));
6469

65-
// Assert - Should return same cached instance (or both null)
70+
// Assert
71+
await Assert.That(converter1).IsNotNull();
6672
await Assert.That(converter1).IsSameReferenceAs(converter2);
6773
}
6874

@@ -100,4 +106,143 @@ public async Task GetBindingConverter_UsesRxConverters_WhenAvailable()
100106
await Assert.That(converter).IsNotNull();
101107
await Assert.That(converter).IsTypeOf<IntegerToStringTypeConverter>();
102108
}
103-
}
109+
110+
/// <summary>
111+
/// Verifies that GetBindingConverter falls back to Splat if not in RxConverters.
112+
/// </summary>
113+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
114+
[Test]
115+
public async Task GetBindingConverter_FallsBackToSplat_WhenRxConvertersFails()
116+
{
117+
// Arrange
118+
var resolver = new BindingConverterResolver();
119+
120+
// Act
121+
// MockType converter is registered in Splat (via Executor) but NOT in RxConverters (standard list)
122+
var converter = resolver.GetBindingConverter(typeof(MockType), typeof(MockType));
123+
124+
// Assert
125+
await Assert.That(converter).IsNotNull();
126+
await Assert.That(converter).IsTypeOf<MockBindingTypeConverter>();
127+
}
128+
129+
/// <summary>
130+
/// Verifies that GetSetMethodConverter returns a registered converter.
131+
/// </summary>
132+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
133+
[Test]
134+
public async Task GetSetMethodConverter_ReturnsConverter_WhenRegistered()
135+
{
136+
// Arrange
137+
var resolver = new BindingConverterResolver();
138+
139+
// Act
140+
var converterFunc = resolver.GetSetMethodConverter(typeof(MockType), typeof(MockType));
141+
142+
// Assert
143+
await Assert.That(converterFunc).IsNotNull();
144+
145+
// Invoke to verify
146+
var result = converterFunc!(new MockType(), new MockType(), null);
147+
await Assert.That(result).IsEqualTo("SetPerformed");
148+
}
149+
150+
/// <summary>
151+
/// Verifies that GetSetMethodConverter returns null when no converter is registered.
152+
/// </summary>
153+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
154+
[Test]
155+
public async Task GetSetMethodConverter_WithUnregisteredType_ReturnsNull()
156+
{
157+
// Arrange
158+
var resolver = new BindingConverterResolver();
159+
160+
// Act
161+
var converterFunc = resolver.GetSetMethodConverter(typeof(System.Net.IPAddress), typeof(System.Numerics.BigInteger));
162+
163+
// Assert
164+
await Assert.That(converterFunc).IsNull();
165+
}
166+
167+
/// <summary>
168+
/// Verifies that GetSetMethodConverter handles null toType gracefully.
169+
/// </summary>
170+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
171+
[Test]
172+
public async Task GetSetMethodConverter_WithNullToType_HandlesGracefully()
173+
{
174+
// Arrange
175+
var resolver = new BindingConverterResolver();
176+
177+
// Act
178+
var converterFunc = resolver.GetSetMethodConverter(typeof(MockType), null);
179+
180+
// Assert
181+
// The MockSetMethodBindingConverter returns affinity 0 for null toType, so converter should be null
182+
// The test verifies the method handles null toType gracefully without throwing
183+
await Assert.That(converterFunc).IsNull();
184+
}
185+
186+
/// <summary>
187+
/// Verifies that GetBindingConverter handles null services gracefully.
188+
/// </summary>
189+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
190+
[Test]
191+
public async Task GetBindingConverter_WithNoRxConverters_FallsBackToSplat()
192+
{
193+
// Arrange
194+
var resolver = new BindingConverterResolver();
195+
196+
// Act - Get a converter that should be found in Splat
197+
var converter = resolver.GetBindingConverter(typeof(MockType), typeof(MockType));
198+
199+
// Assert
200+
await Assert.That(converter).IsNotNull();
201+
await Assert.That(converter).IsTypeOf<MockBindingTypeConverter>();
202+
}
203+
204+
/// <summary>
205+
/// Test executor that registers mock converters.
206+
/// </summary>
207+
public class Executor : BaseAppBuilderTestExecutor
208+
{
209+
/// <inheritdoc/>
210+
protected override void ConfigureAppBuilder(IReactiveUIBuilder builder, TestContext context)
211+
{
212+
ArgumentNullException.ThrowIfNull(builder);
213+
ArgumentNullException.ThrowIfNull(context);
214+
215+
builder
216+
.WithRegistration(r => r.RegisterConstant<IBindingTypeConverter>(new MockBindingTypeConverter()))
217+
.WithRegistration(r => r.RegisterConstant<ISetMethodBindingConverter>(new MockSetMethodBindingConverter()))
218+
.WithCoreServices();
219+
}
220+
}
221+
222+
private class MockType
223+
{
224+
}
225+
226+
private class MockBindingTypeConverter : IBindingTypeConverter
227+
{
228+
public Type FromType => typeof(MockType);
229+
230+
public Type ToType => typeof(MockType);
231+
232+
public int GetAffinityForObjects() => 100;
233+
234+
public bool TryConvertTyped(object? from, object? conversionHint, out object? result)
235+
{
236+
result = null;
237+
return false;
238+
}
239+
}
240+
241+
private class MockSetMethodBindingConverter : ISetMethodBindingConverter
242+
{
243+
public int GetAffinityForObjects(Type? fromType, Type? toType) =>
244+
fromType == typeof(MockType) && toType == typeof(MockType) ? 100 : 0;
245+
246+
public object? PerformSet(object? current, object? newValue, object?[]? arguments) => "SetPerformed";
247+
}
248+
}

0 commit comments

Comments
 (0)