diff --git a/Source/Mockolate/DefaultValueFactory.cs b/Source/Mockolate/DefaultValueFactory.cs new file mode 100644 index 00000000..c63290aa --- /dev/null +++ b/Source/Mockolate/DefaultValueFactory.cs @@ -0,0 +1,42 @@ +using System; + +namespace Mockolate; + +/// +/// Defines a mechanism for generating default values. +/// +public class DefaultValueFactory +{ + private readonly Func? _generator; + private readonly Func? _predicate; + + /// + /// This constructor is protected to allow inheritance. + /// + // ReSharper disable once UnusedMember.Global + protected DefaultValueFactory() + { + } + + /// + /// Creates a new default value factory for types that match the given . + /// + public DefaultValueFactory(Func predicate, Func generator) + { + _predicate = predicate; + _generator = generator; + } + + /// + /// Checks if the factory can generate a value for the specified . + /// + public virtual bool CanGenerateValue(Type type) + => _predicate?.Invoke(type) ?? false; + + /// + /// Generates a default value of the specified , with + /// the for context. + /// + public virtual object? GenerateValue(Type type, params object?[] parameters) + => _generator?.Invoke(type, parameters); +} diff --git a/Source/Mockolate/MockBehavior.cs b/Source/Mockolate/MockBehavior.cs index ea6192ac..f5cf536b 100644 --- a/Source/Mockolate/MockBehavior.cs +++ b/Source/Mockolate/MockBehavior.cs @@ -136,6 +136,18 @@ public MockBehavior UseConstructorParametersFor(params object?[] parameters) return behavior; } + /// + /// Uses the given to create default values for supported types. + /// + public MockBehavior WithDefaultValueFor(params DefaultValueFactory[] defaultValueFactories) + { + MockBehavior behavior = this with + { + DefaultValue = new DefaultValueGeneratorWithFactories(DefaultValue, defaultValueFactories), + }; + return behavior; + } + private interface IInitializer; private interface IInitializer : IInitializer @@ -169,4 +181,21 @@ public Action>[] GetSetups() return setups.Select(a => new Action>(s => a(index, s))).ToArray(); } } + + private sealed class DefaultValueGeneratorWithFactories( + IDefaultValueGenerator inner, + DefaultValueFactory[] factories) + : IDefaultValueGenerator + { + public object? GenerateValue(Type type, params object?[] parameters) + { + DefaultValueFactory? factory = factories.FirstOrDefault(f => f.CanGenerateValue(type)); + if (factory is not null) + { + return factory.GenerateValue(type, parameters); + } + + return inner.GenerateValue(type, parameters); + } + } } diff --git a/Source/Mockolate/MockBehaviorExtensions.cs b/Source/Mockolate/MockBehaviorExtensions.cs index b434b654..d01a6f17 100644 --- a/Source/Mockolate/MockBehaviorExtensions.cs +++ b/Source/Mockolate/MockBehaviorExtensions.cs @@ -1,3 +1,5 @@ +using System; + namespace Mockolate; /// @@ -34,5 +36,13 @@ public MockBehavior ThrowingWhenNotSetup(bool throwWhenNotSetup = true) { ThrowWhenNotSetup = throwWhenNotSetup, }; + + /// + /// Uses the given to create default values for . + /// + public MockBehavior WithDefaultValueFor(Func factory) + => mockBehavior.WithDefaultValueFor(new DefaultValueFactory( + t => t == typeof(T), + (_, _) => factory())); } } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index 65bf0858..a18a1ac2 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -13,6 +13,13 @@ namespace Mockolate public object?[] Parameters { get; init; } } } + public class DefaultValueFactory + { + protected DefaultValueFactory() { } + public DefaultValueFactory(System.Func predicate, System.Func generator) { } + public virtual bool CanGenerateValue(System.Type type) { } + public virtual object? GenerateValue(System.Type type, params object?[] parameters) { } + } public interface IDefaultValueGenerator { object? GenerateValue(System.Type type, params object?[] parameters); @@ -90,6 +97,7 @@ namespace Mockolate public Mockolate.MockBehavior Initialize(params System.Action>[] setups) { } public Mockolate.MockBehavior UseConstructorParametersFor(System.Func parameters) { } public Mockolate.MockBehavior UseConstructorParametersFor(params object?[] parameters) { } + public Mockolate.MockBehavior WithDefaultValueFor(params Mockolate.DefaultValueFactory[] defaultValueFactories) { } } public static class MockBehaviorExtensions { @@ -97,6 +105,7 @@ namespace Mockolate { public Mockolate.MockBehavior SkippingBaseClass(bool skipBaseClass = true) { } public Mockolate.MockBehavior ThrowingWhenNotSetup(bool throwWhenNotSetup = true) { } + public Mockolate.MockBehavior WithDefaultValueFor(System.Func factory) { } } } public static class MockExtensions diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index 49a29633..4473a263 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -12,6 +12,13 @@ namespace Mockolate public object?[] Parameters { get; init; } } } + public class DefaultValueFactory + { + protected DefaultValueFactory() { } + public DefaultValueFactory(System.Func predicate, System.Func generator) { } + public virtual bool CanGenerateValue(System.Type type) { } + public virtual object? GenerateValue(System.Type type, params object?[] parameters) { } + } public interface IDefaultValueGenerator { object? GenerateValue(System.Type type, params object?[] parameters); @@ -89,6 +96,7 @@ namespace Mockolate public Mockolate.MockBehavior Initialize(params System.Action>[] setups) { } public Mockolate.MockBehavior UseConstructorParametersFor(System.Func parameters) { } public Mockolate.MockBehavior UseConstructorParametersFor(params object?[] parameters) { } + public Mockolate.MockBehavior WithDefaultValueFor(params Mockolate.DefaultValueFactory[] defaultValueFactories) { } } public static class MockBehaviorExtensions { @@ -96,6 +104,7 @@ namespace Mockolate { public Mockolate.MockBehavior SkippingBaseClass(bool skipBaseClass = true) { } public Mockolate.MockBehavior ThrowingWhenNotSetup(bool throwWhenNotSetup = true) { } + public Mockolate.MockBehavior WithDefaultValueFor(System.Func factory) { } } } public static class MockExtensions diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index e36bda86..32164c3e 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -11,6 +11,13 @@ namespace Mockolate public object?[] Parameters { get; init; } } } + public class DefaultValueFactory + { + protected DefaultValueFactory() { } + public DefaultValueFactory(System.Func predicate, System.Func generator) { } + public virtual bool CanGenerateValue(System.Type type) { } + public virtual object? GenerateValue(System.Type type, params object?[] parameters) { } + } public interface IDefaultValueGenerator { object? GenerateValue(System.Type type, params object?[] parameters); @@ -84,6 +91,7 @@ namespace Mockolate public Mockolate.MockBehavior Initialize(params System.Action>[] setups) { } public Mockolate.MockBehavior UseConstructorParametersFor(System.Func parameters) { } public Mockolate.MockBehavior UseConstructorParametersFor(params object?[] parameters) { } + public Mockolate.MockBehavior WithDefaultValueFor(params Mockolate.DefaultValueFactory[] defaultValueFactories) { } } public static class MockBehaviorExtensions { @@ -91,6 +99,7 @@ namespace Mockolate { public Mockolate.MockBehavior SkippingBaseClass(bool skipBaseClass = true) { } public Mockolate.MockBehavior ThrowingWhenNotSetup(bool throwWhenNotSetup = true) { } + public Mockolate.MockBehavior WithDefaultValueFor(System.Func factory) { } } } public static class MockExtensions diff --git a/Tests/Mockolate.Tests/DefaultValueFactoryTests.cs b/Tests/Mockolate.Tests/DefaultValueFactoryTests.cs new file mode 100644 index 00000000..7ff600ae --- /dev/null +++ b/Tests/Mockolate.Tests/DefaultValueFactoryTests.cs @@ -0,0 +1,28 @@ +namespace Mockolate.Tests; + +public sealed class DefaultValueFactoryTests +{ + [Fact] + public async Task CanGenerateValue_ShouldFallbackToFalse() + { + MyDefaultValueFactory defaultValueFactory = new(); + + bool result = defaultValueFactory.CanGenerateValue(typeof(DefaultValueFactoryTests)); + + await That(result).IsFalse(); + } + + [Fact] + public async Task GenerateValue_ShouldFallbackToAlwaysReturnNull() + { + MyDefaultValueFactory defaultValueFactory = new(); + + object? result = defaultValueFactory.GenerateValue(typeof(int)); + + await That(result).IsNull(); + } + + private class MyDefaultValueFactory : DefaultValueFactory + { + } +} diff --git a/Tests/Mockolate.Tests/MockBehaviorTests.UseConstructorParametersForTests.cs b/Tests/Mockolate.Tests/MockBehaviorTests.UseConstructorParametersForTests.cs index 7cce35b3..d121fba1 100644 --- a/Tests/Mockolate.Tests/MockBehaviorTests.UseConstructorParametersForTests.cs +++ b/Tests/Mockolate.Tests/MockBehaviorTests.UseConstructorParametersForTests.cs @@ -4,9 +4,23 @@ public sealed partial class MockBehaviorTests { public sealed class UseConstructorParametersForTests { + [Fact] + public async Task WithExplicitConstructorParameters_ShouldIgnoreConstructorParametersFromBehavior() + { + MockBehavior behavior = MockBehavior.Default + .UseConstructorParametersFor(() => [5,]); + + MyServiceWithMultipleConstructors mock + = Mock.Create(BaseClass.WithConstructorParameters(7), behavior); + + int value = mock.Value; + + await That(value).IsEqualTo(7); + } + [Fact] public async Task - UseConstructorParametersFor_WithExplicitParameters_ShouldUseConstructorParametersFromBehavior() + WithExplicitParameters_ShouldUseConstructorParametersFromBehavior() { MockBehavior behavior = MockBehavior.Default .UseConstructorParametersFor(5); @@ -24,7 +38,7 @@ MyServiceWithMultipleConstructors mockWithCustomBehavior } [Fact] - public async Task UseConstructorParametersFor_WithPredicate_ShouldUseConstructorParametersFromBehavior() + public async Task WithPredicate_ShouldUseConstructorParametersFromBehavior() { MockBehavior behavior = MockBehavior.Default .UseConstructorParametersFor(() => [5,]); @@ -41,20 +55,6 @@ MyServiceWithMultipleConstructors mockWithCustomBehavior await That(valueWithCustomBehavior).IsEqualTo(5); } - [Fact] - public async Task WithExplicitConstructorParameters_ShouldIgnoreConstructorParametersFromBehavior() - { - MockBehavior behavior = MockBehavior.Default - .UseConstructorParametersFor(() => [5,]); - - MyServiceWithMultipleConstructors mock - = Mock.Create(BaseClass.WithConstructorParameters(7), behavior); - - int value = mock.Value; - - await That(value).IsEqualTo(7); - } - internal class MyServiceWithMultipleConstructors { public MyServiceWithMultipleConstructors() diff --git a/Tests/Mockolate.Tests/MockBehaviorTests.WithDefaultValueForTests.cs b/Tests/Mockolate.Tests/MockBehaviorTests.WithDefaultValueForTests.cs new file mode 100644 index 00000000..279b08d7 --- /dev/null +++ b/Tests/Mockolate.Tests/MockBehaviorTests.WithDefaultValueForTests.cs @@ -0,0 +1,93 @@ +namespace Mockolate.Tests; + +public sealed partial class MockBehaviorTests +{ + public sealed class WithDefaultValueForTests + { + [Fact] + public async Task MultipleTypedFactories_ShouldSupportAll() + { + MockBehavior behavior = MockBehavior.Default + .WithDefaultValueFor(() => new MySpecialValue(5)) + .WithDefaultValueFor(() => new MyInheritedSpecialValue(6, 7)); + + IMyServiceForMySpecialValue mockWithBehavior + = Mock.Create(behavior); + + await That(mockWithBehavior.Value.Value).IsEqualTo(5); + await That(mockWithBehavior.InheritedValue) + .For(x => x.Value, it => it.IsEqualTo(6)).And + .For(x => x.OtherValue, it => it.IsEqualTo(7)); + } + + [Fact] + public async Task WithExplicitDefaultValueFactory_ShouldApplyThisFactory() + { + DefaultValueFactory defaultValueFactory = new( + t => typeof(MySpecialValue).IsAssignableFrom(t), + (_, _) => new MyInheritedSpecialValue(6, 7)); + MockBehavior behavior = MockBehavior.Default + .WithDefaultValueFor(defaultValueFactory); + + IMyServiceForMySpecialValue mockWithBehavior + = Mock.Create(behavior); + + await That(mockWithBehavior.Value.Value).IsEqualTo(6); + await That(mockWithBehavior.InheritedValue) + .For(x => x.Value, it => it.IsEqualTo(6)).And + .For(x => x.OtherValue, it => it.IsEqualTo(7)); + } + + [Fact] + public async Task WithTypedFactory_ShouldRequireExactTypeMatch() + { + MockBehavior behavior1 = MockBehavior.Default + .WithDefaultValueFor(() => new MySpecialValue(5)); + MockBehavior behavior2 = MockBehavior.Default + .WithDefaultValueFor(() => new MyInheritedSpecialValue(6, 7)); + + IMyServiceForMySpecialValue mockWithBehavior1 + = Mock.Create(behavior1); + IMyServiceForMySpecialValue mockWithBehavior2 + = Mock.Create(behavior2); + + await That(mockWithBehavior1.InheritedValue).IsNull(); + await That(mockWithBehavior1.Value.Value).IsEqualTo(5); + await That(mockWithBehavior2.InheritedValue) + .For(x => x.Value, it => it.IsEqualTo(6)).And + .For(x => x.OtherValue, it => it.IsEqualTo(7)); + await That(mockWithBehavior2.Value).IsNull(); + } + + [Fact] + public async Task WithTypedFactory_ShouldUseDefaultValueFactoryFromBehavior() + { + MockBehavior behavior = MockBehavior.Default + .WithDefaultValueFor(() => new MySpecialValue(5)); + + IMyServiceForMySpecialValue mockWithDefaultBehavior + = Mock.Create(); + IMyServiceForMySpecialValue mockWithCustomBehavior + = Mock.Create(behavior); + + await That(mockWithDefaultBehavior.Value).IsNull(); + await That(mockWithCustomBehavior.Value.Value).IsEqualTo(5); + } + + internal class MySpecialValue(int value) + { + public int Value { get; } = value; + } + + internal class MyInheritedSpecialValue(int value, int otherValue) : MySpecialValue(value) + { + public int OtherValue { get; } = otherValue; + } + + internal interface IMyServiceForMySpecialValue + { + MySpecialValue Value { get; } + MyInheritedSpecialValue InheritedValue { get; } + } + } +}