How do i verify an init-call with Moq? I have the following property defined in an interface:
public string Text { init; }
I want to verify that init was called with a specific parameter. Is that possible?
Since Moq
relies on Action<I>
setterExpression for its VerifySet
, it doesn't seem possible to do that with the current API due to the init
compiler errors we would get.
However, we can rely on expressions in an extension method to circumvent the compiler errors. Following a possible implementation that works upon initial testing:
public static class MoqInitExtensions {
public static void VerifyInit<T, TPropVal>(this Mock<T> mock,
string propertyName, TPropVal propValue)
where T : class {
var property = typeof(T).GetProperty(propertyName);
if (property is null)
throw new ArgumentException($"No property {propertyName} on {typeof(T).Name}");
if (property.PropertyType.IsAssignableFrom(typeof(TPropVal)) == false)
throw new ArgumentException($"Property {propertyName} is of type {property.PropertyType} but got a value of {typeof(TPropVal)} ");
var propertySetter = property.SetMethod;
if (propertySetter is null)
throw new InvalidOperationException($"No setter for {propertyName} on {typeof(T).Name}");
// OPTIONAL:
var initOnly = propertySetter.ReturnParameter
.GetRequiredCustomModifiers()
.Any(mod => mod == typeof(IsExternalInit));
if (initOnly == false)
throw new InvalidOperationException($"{propertyName} is not init-only. Use VerifySet instead.");
var param = Expression.Parameter(typeof(T));
var prop = Expression.Property(param, property);
var assign = Expression.Assign(prop, Expression.Constant(propValue));
var setterLambda = Expression.Lambda<Action<T>>(assign, param);
var compiled = setterLambda.Compile();
mock.VerifySet(compiled);
}
}
public interface I {
string Text { init; }
}
void Main() {
var mock = new Mock<I>();
var obj = mock.Object;
// Simulate reflection call for "init" setter
var setter = typeof(I).GetProperty("Text").SetMethod;
string toCallWith = "hello";
setter.Invoke(obj, [toCallWith]);
mock.VerifyInit("Text", toCallWith); // succeeds
try {
mock.VerifyInit("Text", "not" + toCallWith);
} catch (Exception ex) {
Console.WriteLine(ex.Message);
// Expected invocation on the mock at least once, but was never performed: < param > => < param >.Text = "nothello"
//
//Performed invocations:
//
// Mock < I:1 > ():
//
// I.Text = "hello"
}
}