diff --git a/release_notes.md b/release_notes.md index d99a41e..a4c5d79 100644 --- a/release_notes.md +++ b/release_notes.md @@ -2,6 +2,8 @@ - Allow multiple path components in file-based recorded call repository constructors ([#88](https://github.com/blairconrad/SelfInitializingFakes/pull/88)) - Create missing directories for file-based recorded call repositories ([#87](https://github.com/blairconrad/SelfInitializingFakes/pull/87)) +- Serialize `Lazy`, `Task`, and `Task` return values and out and ref + parameters ([#81](https://github.com/blairconrad/SelfInitializingFakes/issues/81)) ### With special thanks for contributions to this release from: - [CableZa](https://github.com/CableZa) diff --git a/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs b/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs new file mode 100644 index 0000000..fab90fd --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs @@ -0,0 +1,48 @@ +namespace SelfInitializingFakes.Infrastructure +{ + using System; + using System.Reflection; + + /// + /// Chains other s together. + /// + internal class CompoundTypeConverter : ITypeConverter + { + private readonly ITypeConverter first; + private readonly ITypeConverter second; + + /// + /// Initializes a new instance of the class. + /// + /// The first converter to try. If it can't convert the input, the second will be tried. + /// The second converter to try, if the first was unable. + public CompoundTypeConverter(ITypeConverter first, ITypeConverter second) + { + this.first = first; + this.second = second; + } + + /// + /// Potentially converts an unserializable object to a more serializable form. + /// + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be assigned to a simpler representation of , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output) => + this.first.ConvertForRecording(input, mainConverter, out output) || + this.second.ConvertForRecording(input, mainConverter, out output); + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be reconstituted from its simpler representation as , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output) => + this.first.ConvertForPlayback(deserializedType, input, mainConverter, out output) || + this.second.ConvertForPlayback(deserializedType, input, mainConverter, out output); + } +} diff --git a/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs b/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs new file mode 100644 index 0000000..868acae --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs @@ -0,0 +1,29 @@ +namespace SelfInitializingFakes.Infrastructure +{ + using System; + + /// + /// Converts unserializable types to simpler types while recording, and reverses the transformation during. + /// + internal interface ITypeConverter + { + /// + /// Potentially converts an unserializable object to a more serializable form. + /// + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be assigned to a simpler representation of , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output); + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be reconstituted from its simpler representation as , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output); + } +} \ No newline at end of file diff --git a/src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs b/src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs new file mode 100644 index 0000000..34664ea --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs @@ -0,0 +1,79 @@ +namespace SelfInitializingFakes.Infrastructure +{ + using System; + using System.Reflection; + + /// + /// Converts types to simpler types for serialization, and back again. + /// + internal class LazyTypeConverter : ITypeConverter + { + private static readonly MethodInfo CreateLazyGenericDefinition = + typeof(LazyTypeConverter).GetMethod(nameof(CreateLazy), BindingFlags.Static | BindingFlags.NonPublic); + + /// + /// Potentially converts an unserializable object to a more serializable form. + /// + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be assigned to a simpler representation of , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output) + { + output = null; + if (input is null) + { + return false; + } + + var inputType = input.GetType(); + if (inputType.IsInstanceOf(typeof(Lazy<>))) + { + output = inputType.GetProperty("Value").GetGetMethod().Invoke(input, Type.EmptyTypes); + if (mainConverter.ConvertForRecording(output, mainConverter, out object? furtherConvertedOutput)) + { + output = furtherConvertedOutput; + } + + return true; + } + + return false; + } + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be reconstituted from its simpler representation as , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output) + { + if (deserializedType.IsInstanceOf(typeof(Lazy<>))) + { + var typeOfLazyResult = deserializedType.GetGenericArguments()[0]; + + if (input is null || input.GetType() == typeOfLazyResult) + { + var method = CreateLazyGenericDefinition.MakeGenericMethod(typeOfLazyResult); + output = method.Invoke(null, new object?[] { input }); + return true; + } + + if (mainConverter.ConvertForPlayback(typeOfLazyResult, input, mainConverter, out object? convertedInput)) + { + var method = CreateLazyGenericDefinition.MakeGenericMethod(typeOfLazyResult); + output = method.Invoke(null, new object?[] { convertedInput }); + return true; + } + } + + output = null; + return false; + } + + private static Lazy CreateLazy(T value) => new Lazy(() => value); + } +} diff --git a/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs b/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs index 55b7ee1..d193ad5 100644 --- a/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs +++ b/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs @@ -9,14 +9,17 @@ namespace SelfInitializingFakes.Infrastructure internal class PlaybackRule : IFakeObjectCallRule { private readonly Queue expectedCalls; + private readonly ITypeConverter typeConverter; /// /// Initializes a new instance of the class. /// /// The calls that are expected to be made on the fake. - public PlaybackRule(Queue expectedCalls) + /// A helper to convert values from serialized variants to their original representations. + public PlaybackRule(Queue expectedCalls, ITypeConverter typeConverter) { this.expectedCalls = expectedCalls; + this.typeConverter = typeConverter; } /// @@ -32,8 +35,8 @@ public PlaybackRule(Queue expectedCalls) public void Apply(IInterceptedFakeObjectCall fakeObjectCall) { RecordedCall recordedCall = this.ConsumeNextExpectedCall(fakeObjectCall); - SetReturnValue(fakeObjectCall, recordedCall); - SetOutAndRefValues(fakeObjectCall, recordedCall); + this.SetReturnValue(fakeObjectCall, recordedCall); + this.SetOutAndRefValues(fakeObjectCall, recordedCall); } /// @@ -44,12 +47,18 @@ public void Apply(IInterceptedFakeObjectCall fakeObjectCall) /// true all the time. public bool IsApplicableTo(IFakeObjectCall fakeObjectCall) => true; - private static void SetReturnValue(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall) + private void SetReturnValue(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall) { - fakeObjectCall.SetReturnValue(recordedCall.ReturnValue); + var returnValue = recordedCall.ReturnValue; + if (this.typeConverter.ConvertForPlayback(fakeObjectCall.Method.ReturnType, returnValue, this.typeConverter, out object? convertedReturnValue)) + { + returnValue = convertedReturnValue; + } + + fakeObjectCall.SetReturnValue(returnValue); } - private static void SetOutAndRefValues(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall) + private void SetOutAndRefValues(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall) { int outOrRefIndex = 0; for (int parameterIndex = 0; parameterIndex < fakeObjectCall.Method.GetParameters().Length; parameterIndex++) @@ -57,7 +66,17 @@ private static void SetOutAndRefValues(IInterceptedFakeObjectCall fakeObjectCall var parameter = fakeObjectCall.Method.GetParameters()[parameterIndex]; if (parameter.ParameterType.IsByRef) { - fakeObjectCall.SetArgumentValue(parameterIndex, recordedCall.OutAndRefValues[outOrRefIndex++]); + var parameterValue = recordedCall.OutAndRefValues[outOrRefIndex++]; + if (this.typeConverter.ConvertForPlayback( + parameter.ParameterType.GetElementType(), + parameterValue, + this.typeConverter, + out object? convertedParameterValue)) + { + parameterValue = convertedParameterValue; + } + + fakeObjectCall.SetArgumentValue(parameterIndex, parameterValue); } } } diff --git a/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs b/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs index 41d13cd..8b655c7 100644 --- a/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs +++ b/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs @@ -13,15 +13,18 @@ namespace SelfInitializingFakes.Infrastructure internal class RecordingRule : IFakeObjectCallRule { private readonly object target; + private readonly ITypeConverter typeConverter; private Exception? recordingException; /// /// Initializes a new instance of the class. /// /// The object to which to forward calls, in order to harvest return, out, and ref values. - public RecordingRule(object target) + /// A helper to convert values from their original representation to serializable variants. + public RecordingRule(object target, ITypeConverter typeConverter) { this.target = target; + this.typeConverter = typeConverter; } /// @@ -52,8 +55,9 @@ public void Apply(IInterceptedFakeObjectCall fakeObjectCall) try { var recordedCall = this.BuildRecordedCall(fakeObjectCall); - this.RecordedCalls.Add(recordedCall); ApplyRecordedCall(recordedCall, fakeObjectCall); + this.ConvertRecordedCallForSerialization(recordedCall); + this.RecordedCalls.Add(recordedCall); } #pragma warning disable CA1031 // We do rethrow the exception catch (Exception e) @@ -118,5 +122,21 @@ private RecordedCall BuildRecordedCall(IFakeObjectCall call) return new RecordedCall(call.Method.ToString(), result, outAndRefValues.ToArray()); } + + private void ConvertRecordedCallForSerialization(RecordedCall call) + { + if (this.typeConverter.ConvertForRecording(call.ReturnValue, this.typeConverter, out object? convertedReturnValue)) + { + call.ReturnValue = convertedReturnValue; + } + + for (int i = 0; i < call.OutAndRefValues.Length; ++i) + { + if (this.typeConverter.ConvertForRecording(call.OutAndRefValues[i], this.typeConverter, out object? convertedValue)) + { + call.OutAndRefValues[i] = convertedValue; + } + } + } } } diff --git a/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs b/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs new file mode 100644 index 0000000..cb0f6e8 --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs @@ -0,0 +1,104 @@ +namespace SelfInitializingFakes.Infrastructure +{ + using System; + using System.Reflection; + using System.Threading.Tasks; + + /// + /// Converts types to simpler types for serialization, and back again. + /// + internal class TaskTypeConverter : ITypeConverter + { + private static readonly MethodInfo CreateTaskGenericDefinition = + typeof(TaskTypeConverter).GetMethod(nameof(CreateTask), BindingFlags.Static | BindingFlags.NonPublic); + + /// + /// Potentially converts an unserializable object to a more serializable form. + /// + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be assigned to a simpler representation of , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output) + { + output = null; + if (input is null) + { + return false; + } + + var inputType = input.GetType(); + if (inputType.IsInstanceOf(typeof(Task<>))) + { + output = inputType.GetProperty("Result").GetGetMethod().Invoke(input, Type.EmptyTypes); + if (mainConverter.ConvertForRecording(output, mainConverter, out object? furtherConvertedOutput)) + { + output = furtherConvertedOutput; + } + + return true; + } + else if (inputType == typeof(Task)) + { + output = "{void Task}"; + if (mainConverter.ConvertForRecording(output, mainConverter, out object? furtherConvertedOutput)) + { + output = furtherConvertedOutput; + } + + return true; + } + + return false; + } + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// A comprehensive converter that may be used to further convert the output, if required. + /// An output object. Will be reconstituted from its simpler representation as , if this converter knows how. + /// true if the conversion happened, otherwise false. Good for building a chain of responsibility. + public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output) + { + if (deserializedType.IsInstanceOf(typeof(Task<>))) + { + var typeOfTaskResult = deserializedType.GetGenericArguments()[0]; + + if (input is null || input.GetType() == typeOfTaskResult) + { + var method = CreateTaskGenericDefinition.MakeGenericMethod(typeOfTaskResult); + output = method.Invoke(null, new object?[] { input }); + return true; + } + + if (mainConverter.ConvertForPlayback(typeOfTaskResult, input, mainConverter, out object? convertedInput)) + { + var method = CreateTaskGenericDefinition.MakeGenericMethod(typeOfTaskResult); + output = method.Invoke(null, new object?[] { convertedInput }); + return true; + } + } + else if (deserializedType == typeof(Task) && "{void Task}".Equals(input)) + { + var task = new Task(() => { }); + task.Start(); + task.Wait(); + + output = task; + return true; + } + + output = null; + return false; + } + + private static Task CreateTask(T value) + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(value); + return tcs.Task; + } + } +} diff --git a/src/SelfInitializingFakes/Infrastructure/TypeExtensions.cs b/src/SelfInitializingFakes/Infrastructure/TypeExtensions.cs new file mode 100644 index 0000000..a3dcb1b --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/TypeExtensions.cs @@ -0,0 +1,27 @@ +namespace SelfInitializingFakes.Infrastructure +{ + using System; +#if FRAMEWORK_WEAK_TYPE_CLASS + using System.Reflection; +#endif + + /// + /// Provides extension methods for . + /// + internal static class TypeExtensions + { + /// + /// See if this type is an instance of a given open generic type. + /// + /// This type argument. + /// The generic type definition to see if this type is an instance of. + /// Type info of the type argument. + public static bool IsInstanceOf(this Type @this, Type genericType) => +#if FRAMEWORK_WEAK_TYPE_CLASS + @this.GetTypeInfo().IsGenericType +#else + @this.IsGenericType +#endif + && @this.GetGenericTypeDefinition() == genericType; + } +} diff --git a/src/SelfInitializingFakes/SelfInitializingFake.of.T.cs b/src/SelfInitializingFakes/SelfInitializingFake.of.T.cs index 9535865..edd13d3 100644 --- a/src/SelfInitializingFakes/SelfInitializingFake.of.T.cs +++ b/src/SelfInitializingFakes/SelfInitializingFake.of.T.cs @@ -14,6 +14,8 @@ namespace SelfInitializingFakes public sealed class SelfInitializingFake : IDisposable where TService : class { + private static readonly ITypeConverter TypeConverter = new CompoundTypeConverter(new TaskTypeConverter(), new LazyTypeConverter()); + private readonly IRecordedCallRepository repository; private readonly RecordingRule? recordingRule; @@ -36,13 +38,15 @@ internal SelfInitializingFake(Func serviceFactory, IRecordedCallReposi { var wrappedService = serviceFactory.Invoke(); this.Object = A.Fake(); - this.recordingRule = new RecordingRule(wrappedService); + this.recordingRule = new RecordingRule(wrappedService, TypeConverter); Fake.GetFakeManager(this.Object).AddRuleFirst(this.recordingRule); } else { this.Object = A.Fake(); - Fake.GetFakeManager(this.Object).AddRuleFirst(new PlaybackRule(new Queue(callsFromRepository))); + Fake.GetFakeManager(this.Object).AddRuleFirst(new PlaybackRule( + new Queue(callsFromRepository), + TypeConverter)); } } diff --git a/tests/Acceptance/BinarySerialization.cs b/tests/Acceptance/BinarySerialization.cs index bd33a93..06f4df8 100644 --- a/tests/Acceptance/BinarySerialization.cs +++ b/tests/Acceptance/BinarySerialization.cs @@ -3,100 +3,55 @@ using System; using System.Collections.Generic; using System.IO; - using FakeItEasy; + using FluentAssertions; + using SelfInitializingFakes.Tests.Acceptance.Helpers; using Xbehave; - public static class BinarySerialization + public class BinarySerialization : TypeSerializationTestBase { - public interface IService - { - void VoidMethod(string s, out int i, ref DateTime dt); - - IDictionary NonVoidMethod(); - } - [Scenario] - public static void SerializeVoidCall( - string path, + public void SerializeCallWithDictionary( IRecordedCallRepository repository, - IService realServiceWhileRecording, - int voidMethodOutInteger, - DateTime voidMethodRefDateTime, - IDictionary nonVoidMethodResult) + IDictionary dictionaryMethodResult) { - "Given a file path" - .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); - - "And a BinaryFileRecordedCallRepository targeting that path" - .x(() => repository = new BinaryFileRecordedCallRepository(path)); - - "And a real service to wrap while recording" - .x(() => - { - realServiceWhileRecording = A.Fake(); + "Given a recorded call repository" + .x(() => repository = this.CreateRepository()); - int i; - DateTime dt = DateTime.MinValue; - A.CallTo(() => realServiceWhileRecording.VoidMethod("firstCallKey", out i, ref dt)) - .AssignsOutAndRefParameters(17, new DateTime(2017, 1, 24)); - - A.CallTo(() => realServiceWhileRecording.NonVoidMethod()) - .Returns(new Dictionary - { - ["key1"] = new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd"), - }); - }); - - "When I use a self-initializing fake in recording mode" + "When I record a dictionary-returning method via a self-initializing fake" .x(() => { - using (var fakeService = SelfInitializingFake.For(() => realServiceWhileRecording, repository)) + using (var fakeService = SelfInitializingFake.For(() => new SampleService(), repository)) { var fake = fakeService.Object; - fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); - nonVoidMethodResult = fake.NonVoidMethod(); + _ = fake.DictionaryReturningMethod(); } }); - "And I use a self-initializing fake in playback mode" + "And I play back a dictionary-returning method via a self-initializing fake" .x(() => { - using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) + using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) { var fake = playbackFakeService.Object; - int i; - DateTime dt = DateTime.MinValue; - fake.VoidMethod("blah", out i, ref dt); + dictionaryMethodResult = fake.DictionaryReturningMethod(); } }); - "Then the recording fake forwards calls to the wrapped service" + "Then the playback fake returns the recorded out and ref parameters and results" .x(() => { - int i; -#if BUG_ASSIGNING_REF_VALUE_CLEARS_INCOMING_VALUE - DateTime dt = new DateTime(2017, 1, 24); -#else - DateTime dt = DateTime.MinValue; -#endif - A.CallTo(() => realServiceWhileRecording.VoidMethod(A._, out i, ref dt)) - .MustHaveHappened(); - A.CallTo(() => realServiceWhileRecording.NonVoidMethod()).MustHaveHappened(); - }); - - "And the playback fake returns the recorded out and ref parameters and results" - .x(() => - { - voidMethodOutInteger.Should().Be(17); - voidMethodRefDateTime.Should().Be(new DateTime(2017, 1, 24)); - nonVoidMethodResult.Should() + dictionaryMethodResult.Should() .HaveCount(1).And .ContainKey("key1") .WhichValue.Should().Be(new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd")); }); } - private static IService UnusedFactory() => null!; + protected override IRecordedCallRepository CreateRepository() + { + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xml"); + return new BinaryFileRecordedCallRepository(path); + } } -} +} \ No newline at end of file diff --git a/tests/Acceptance/FileBasedSerializers.cs b/tests/Acceptance/FileBasedSerializers.cs index 77cc5ad..e5ba5b2 100644 --- a/tests/Acceptance/FileBasedSerializers.cs +++ b/tests/Acceptance/FileBasedSerializers.cs @@ -5,19 +5,13 @@ using System.IO; using System.Linq; - using FakeItEasy; using FluentAssertions; - using SelfInitializingFakes.Tests.Acceptance.Helper; + using SelfInitializingFakes.Tests.Acceptance.Helpers; using Xbehave; using Xunit; public static class FileBasedSerializers { - public interface IService - { - Guid NonVoidMethod(); - } - public static IEnumerable ConcreteRepositoryTypes() => typeof(FileBasedRecordedCallRepository).GetConcreteSubTypesInAssembly() .Select(t => new object[] { t }); @@ -30,7 +24,7 @@ public static void SerializeToDirectoryThatDoesNotExist( string missingChildDirectory, string repositoryPath, IRecordedCallRepository repository, - IService realServiceWhileRecording) + ISampleService realServiceWhileRecording) { "Given a directory that exists" .x(() => @@ -53,14 +47,14 @@ public static void SerializeToDirectoryThatDoesNotExist( .x(() => repository = (IRecordedCallRepository)Activator.CreateInstance(concreteRepositoryType, repositoryPath)); "And a real service to wrap while recording" - .x(() => realServiceWhileRecording = A.Fake()); + .x(() => realServiceWhileRecording = new SampleService()); "When I use a self-initializing fake in recording mode" .x(() => { - using var fakeService = SelfInitializingFake.For(() => realServiceWhileRecording, repository); + using var fakeService = SelfInitializingFake.For(() => realServiceWhileRecording, repository); var fake = fakeService.Object; - _ = fake.NonVoidMethod(); + _ = fake.GuidReturningMethod(); }); "Then the repository file is created" @@ -75,7 +69,7 @@ public static void CreateFromPathComponents( string pathComponent1, string pathComponent2, IRecordedCallRepository repository, - IService realServiceWhileRecording) + ISampleService realServiceWhileRecording) { "Given a base directory" .x(() => baseDirectory = Path.GetTempPath()); @@ -94,14 +88,14 @@ public static void CreateFromPathComponents( pathComponent2)); "And a real service to wrap while recording" - .x(() => realServiceWhileRecording = A.Fake()); + .x(() => realServiceWhileRecording = new SampleService()); "When I use a self-initializing fake in recording mode" .x(() => { - using var fakeService = SelfInitializingFake.For(() => realServiceWhileRecording, repository); + using var fakeService = SelfInitializingFake.For(() => realServiceWhileRecording, repository); var fake = fakeService.Object; - _ = fake.NonVoidMethod(); + _ = fake.GuidReturningMethod(); }); "Then the desired repository file is created" diff --git a/tests/Acceptance/Helpers/ISampleService.cs b/tests/Acceptance/Helpers/ISampleService.cs new file mode 100644 index 0000000..3321bbd --- /dev/null +++ b/tests/Acceptance/Helpers/ISampleService.cs @@ -0,0 +1,29 @@ +namespace SelfInitializingFakes.Tests.Acceptance.Helpers +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + public interface ISampleService + { + void VoidMethod(string s, out int i, ref DateTime dt); + + Guid GuidReturningMethod(); + + IDictionary DictionaryReturningMethod(); + + Lazy LazyIntReturningMethod(); + + Lazy LazyStringReturningMethod(); + + void MethodWithLazyOut(out Lazy lazyInt); + + Task TaskReturningMethod(); + + Task TaskIntReturningMethod(); + + Lazy> LazyTaskIntReturningMethod(); + + Task> TaskLazyIntReturningMethod(); + } +} \ No newline at end of file diff --git a/tests/Acceptance/Helpers/SampleService.cs b/tests/Acceptance/Helpers/SampleService.cs new file mode 100644 index 0000000..8507e5a --- /dev/null +++ b/tests/Acceptance/Helpers/SampleService.cs @@ -0,0 +1,36 @@ +namespace SelfInitializingFakes.Tests.Acceptance.Helpers +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + public class SampleService : ISampleService + { + public void VoidMethod(string s, out int i, ref DateTime dt) + { + i = 17; + dt = new DateTime(2017, 1, 24); + } + + public Guid GuidReturningMethod() => new Guid("5b61d48f-e9e5-49ad-9c51-a9aae056aa84"); + + public IDictionary DictionaryReturningMethod() => new Dictionary + { + ["key1"] = new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd"), + }; + + public Lazy LazyIntReturningMethod() => new Lazy(() => 3); + + public Lazy LazyStringReturningMethod() => new Lazy(() => "three"); + + public void MethodWithLazyOut(out Lazy lazyInt) => lazyInt = new Lazy(() => -14); + + public Task TaskReturningMethod() => Task.Delay(0); + + public Task TaskIntReturningMethod() => Task.FromResult(5); + + public Lazy> LazyTaskIntReturningMethod() => new Lazy>(() => Task.FromResult(19)); + + public Task> TaskLazyIntReturningMethod() => Task>.FromResult(new Lazy(() => 18)); + } +} \ No newline at end of file diff --git a/tests/Acceptance/Helpers/TypeExtensions.cs b/tests/Acceptance/Helpers/TypeExtensions.cs index 2e8f22b..193a115 100644 --- a/tests/Acceptance/Helpers/TypeExtensions.cs +++ b/tests/Acceptance/Helpers/TypeExtensions.cs @@ -1,4 +1,4 @@ -namespace SelfInitializingFakes.Tests.Acceptance.Helper +namespace SelfInitializingFakes.Tests.Acceptance.Helpers { using System; using System.Collections.Generic; diff --git a/tests/Acceptance/TypeSerializationTestBase.cs b/tests/Acceptance/TypeSerializationTestBase.cs new file mode 100644 index 0000000..7a30a24 --- /dev/null +++ b/tests/Acceptance/TypeSerializationTestBase.cs @@ -0,0 +1,256 @@ +namespace SelfInitializingFakes.Tests.Acceptance +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + + using FluentAssertions; + using SelfInitializingFakes.Tests.Acceptance.Helpers; + using Xbehave; + using Xunit; + + public abstract class TypeSerializationTestBase + { + /// Create common test cases for . + public static IEnumerable TestCases() + { + return typeof(TypeSerializationTestBase).GetNestedTypes(BindingFlags.NonPublic) +#if FRAMEWORK_TYPE_LACKS_ISABSTRACT + .Where(t => !t.GetTypeInfo().IsAbstract) +#else + .Where(t => !t.IsAbstract) +#endif + .Where(t => typeof(TestCase).IsAssignableFrom(t)) + .Select(t => new object[] { Activator.CreateInstance(t) }); + } + + [MemberData(nameof(TestCases))] + [Scenario] + public void SerializeCommonCalls(TestCase testCase, IRecordedCallRepository repository) + { + "Given a recorded call repository" + .x(() => repository = this.CreateRepository()); + + "When I use a self-initializing fake in recording mode" + .x(() => + { + using (var fakeService = SelfInitializingFake.For(() => new SampleService(), repository)) + { + var fake = fakeService.Object; + testCase.Record(fake); + } + }); + + "And I use a self-initializing fake in playback mode" + .x(() => + { + using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) + { + var fake = playbackFakeService.Object; + testCase.Playback(fake); + } + }); + + "Then the playback fake returns the recorded out and ref parameters and results" + .x(() => + { + testCase.Verify(); + }); + } + + protected static ISampleService UnusedFactory() => null!; + + protected abstract IRecordedCallRepository CreateRepository(); + + public abstract class TestCase + { + public abstract void Record(ISampleService service); + + public abstract void Playback(ISampleService service); + + public abstract void Verify(); + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodWithOutIntegerAndRefDateTime : TestCase + { + private int voidMethodOutInteger; + private DateTime voidMethodRefDateTime; + + public override void Record(ISampleService service) + { + DateTime discardDateTime = DateTime.MinValue; + service.VoidMethod("firstCallKey", out _, ref discardDateTime); + } + + public override void Playback(ISampleService service) + { + service.VoidMethod("firstCallKey", out this.voidMethodOutInteger, ref this.voidMethodRefDateTime); + } + + public override void Verify() + { + this.voidMethodOutInteger.Should().Be(17); + this.voidMethodRefDateTime.Should().Be(new DateTime(2017, 1, 24)); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodThatReturnsLazyInt : TestCase + { + private Lazy? result; + + public override void Record(ISampleService service) + { + service.LazyIntReturningMethod(); + } + + public override void Playback(ISampleService service) + { + this.result = service.LazyIntReturningMethod(); + } + + public override void Verify() + { + this.result!.IsValueCreated.Should().BeFalse(); + this.result!.Value.Should().Be(3); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodThatReturnsLazyString : TestCase + { + private Lazy? result; + + public override void Record(ISampleService service) + { + service.LazyStringReturningMethod(); + } + + public override void Playback(ISampleService service) + { + this.result = service.LazyStringReturningMethod(); + } + + public override void Verify() + { + this.result!.IsValueCreated.Should().BeFalse(); + this.result!.Value.Should().Be("three"); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodWithOutLazyInt : TestCase + { + private Lazy? lazyInt; + + public override void Record(ISampleService service) + { + service.MethodWithLazyOut(out _); + } + + public override void Playback(ISampleService service) + { + service.MethodWithLazyOut(out this.lazyInt); + } + + public override void Verify() + { + this.lazyInt!.IsValueCreated.Should().BeFalse(); + this.lazyInt!.Value.Should().Be(-14); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodThatReturnsTask : TestCase + { + private Task? result; + + public override void Record(ISampleService service) + { + service.TaskReturningMethod(); + } + + public override void Playback(ISampleService service) + { + this.result = service.TaskReturningMethod(); + } + + public override void Verify() + { + this.result!.IsCompleted.Should().BeTrue(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodThatReturnsTaskInt : TestCase + { + private Task? result; + + public override void Record(ISampleService service) + { + service.TaskIntReturningMethod(); + } + + public override void Playback(ISampleService service) + { + this.result = service.TaskIntReturningMethod(); + } + + public override void Verify() + { + this.result!.IsCompleted.Should().BeTrue(); + this.result!.Result.Should().Be(5); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodThatReturnsLazyTaskInt : TestCase + { + private Lazy>? result; + + public override void Record(ISampleService service) + { + service.LazyTaskIntReturningMethod(); + } + + public override void Playback(ISampleService service) + { + this.result = service.LazyTaskIntReturningMethod(); + } + + public override void Verify() + { + this.result!.IsValueCreated.Should().BeFalse(); + this.result!.Value.IsCompleted.Should().BeTrue(); + this.result!.Value.Result.Should().Be(19); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodThatReturnsTaskLazyInt : TestCase + { + private Task>? result; + + public override void Record(ISampleService service) + { + service.TaskLazyIntReturningMethod(); + } + + public override void Playback(ISampleService service) + { + this.result = service.TaskLazyIntReturningMethod(); + } + + public override void Verify() + { + this.result!.IsCompleted.Should().BeTrue(); + this.result!.Result.IsValueCreated.Should().BeFalse(); + this.result!.Result.Value.Should().Be(18); + } + } + } +} \ No newline at end of file diff --git a/tests/Acceptance/XmlSerialization.cs b/tests/Acceptance/XmlSerialization.cs index 68f82f2..c16edc0 100644 --- a/tests/Acceptance/XmlSerialization.cs +++ b/tests/Acceptance/XmlSerialization.cs @@ -2,94 +2,13 @@ { using System; using System.IO; - using FakeItEasy; - using FluentAssertions; - using Xbehave; - public static class XmlSerialization + public class XmlSerialization : TypeSerializationTestBase { - public interface IService + protected override IRecordedCallRepository CreateRepository() { - void VoidMethod(string s, out int i, ref DateTime dt); - - Guid NonVoidMethod(); + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xml"); + return new XmlFileRecordedCallRepository(path); } - - [Scenario] - public static void SerializeVoidCall( - string path, - IRecordedCallRepository repository, - IService realServiceWhileRecording, - int voidMethodOutInteger, - DateTime voidMethodRefDateTime, - Guid nonVoidMethodResult) - { - "Given a file path" - .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xml")); - - "And a XmlFileRecordedCallRepository targeting that path" - .x(() => repository = new XmlFileRecordedCallRepository(path)); - - "And a real service to wrap while recording" - .x(() => - { - realServiceWhileRecording = A.Fake(); - - int i; - DateTime dt = DateTime.MinValue; - A.CallTo(() => realServiceWhileRecording.VoidMethod("firstCallKey", out i, ref dt)) - .AssignsOutAndRefParameters(17, new DateTime(2017, 1, 24)); - - A.CallTo(() => realServiceWhileRecording.NonVoidMethod()) - .Returns(new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd")); - }); - - "When I use a self-initializing fake in recording mode" - .x(() => - { - using (var fakeService = SelfInitializingFake.For(() => realServiceWhileRecording, repository)) - { - var fake = fakeService.Object; - fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); - nonVoidMethodResult = fake.NonVoidMethod(); - } - }); - - "And I use a self-initializing fake in playback mode" - .x(() => - { - using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) - { - var fake = playbackFakeService.Object; - int i; - DateTime dt = DateTime.MinValue; - fake.VoidMethod("blah", out i, ref dt); - } - }); - - "Then the recording fake forwards calls to the wrapped service" - .x(() => - { - int i; -#if BUG_ASSIGNING_REF_VALUE_CLEARS_INCOMING_VALUE - DateTime dt = new DateTime(2017, 1, 24); -#else - DateTime dt = DateTime.MinValue; -#endif - A.CallTo(() => realServiceWhileRecording.VoidMethod(A._, out i, ref dt)) - .MustHaveHappened(); - A.CallTo(() => realServiceWhileRecording.NonVoidMethod()).MustHaveHappened(); - }); - - "And the playback fake returns the recorded out and ref parameters and results" - .x(() => - { - voidMethodOutInteger.Should().Be(17); - voidMethodRefDateTime.Should().Be(new DateTime(2017, 1, 24)); - nonVoidMethodResult.Should().Be(new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd")); - }); - } - - private static IService UnusedFactory() => null!; } } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index ce5c82f..4dc2472 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -44,4 +44,8 @@ + + $(DefineConstants);FRAMEWORK_TYPE_LACKS_ISABSTRACT + + diff --git a/tests/SelfInitializingFakes.Tests.FIE.3.0.0/SelfInitializingFakes.Tests.FIE.3.0.0.csproj b/tests/SelfInitializingFakes.Tests.FIE.3.0.0/SelfInitializingFakes.Tests.FIE.3.0.0.csproj index 81cbaf5..cd6fdea 100644 --- a/tests/SelfInitializingFakes.Tests.FIE.3.0.0/SelfInitializingFakes.Tests.FIE.3.0.0.csproj +++ b/tests/SelfInitializingFakes.Tests.FIE.3.0.0/SelfInitializingFakes.Tests.FIE.3.0.0.csproj @@ -4,7 +4,6 @@ net452;netcoreapp1.0 SelfInitializingFakes.Tests.FIE.3.0.0 SelfInitializingFakes.Tests.FIE.3.0.0 - $(DefineConstants);BUG_ASSIGNING_REF_VALUE_CLEARS_INCOMING_VALUE