From 5f0446ccf2d6a6d65a8dae519aa2f0e161d402fa Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Sat, 20 Jun 2020 06:42:05 -0400 Subject: [PATCH 1/9] Fix up bad serialization tests --- tests/Acceptance/BinarySerialization.cs | 61 +++++++------------ tests/Acceptance/XmlSerialization.cs | 55 ++++++----------- ...lfInitializingFakes.Tests.FIE.3.0.0.csproj | 1 - 3 files changed, 39 insertions(+), 78 deletions(-) diff --git a/tests/Acceptance/BinarySerialization.cs b/tests/Acceptance/BinarySerialization.cs index bd33a93..3812290 100644 --- a/tests/Acceptance/BinarySerialization.cs +++ b/tests/Acceptance/BinarySerialization.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.IO; - using FakeItEasy; using FluentAssertions; using Xbehave; @@ -20,7 +19,6 @@ public interface IService public static void SerializeVoidCall( string path, IRecordedCallRepository repository, - IService realServiceWhileRecording, int voidMethodOutInteger, DateTime voidMethodRefDateTime, IDictionary nonVoidMethodResult) @@ -31,31 +29,15 @@ public static void SerializeVoidCall( "And a BinaryFileRecordedCallRepository targeting that path" .x(() => repository = new BinaryFileRecordedCallRepository(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 Dictionary - { - ["key1"] = 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)) + using (var fakeService = SelfInitializingFake.For(() => new Service(), repository)) { + DateTime discardDateTime = DateTime.MaxValue; var fake = fakeService.Object; - fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); - nonVoidMethodResult = fake.NonVoidMethod(); + fake.VoidMethod("firstCallKey", out _, ref discardDateTime); + _ = fake.NonVoidMethod(); } }); @@ -65,27 +47,12 @@ public static void SerializeVoidCall( using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) { var fake = playbackFakeService.Object; - int i; - DateTime dt = DateTime.MinValue; - fake.VoidMethod("blah", out i, ref dt); + fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); + nonVoidMethodResult = fake.NonVoidMethod(); } }); - "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" + "Then the playback fake returns the recorded out and ref parameters and results" .x(() => { voidMethodOutInteger.Should().Be(17); @@ -98,5 +65,19 @@ public static void SerializeVoidCall( } private static IService UnusedFactory() => null!; + + private class Service : IService + { + public void VoidMethod(string s, out int i, ref DateTime dt) + { + i = 17; + dt = new DateTime(2017, 1, 24); + } + + public IDictionary NonVoidMethod() => new Dictionary + { + ["key1"] = new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd"), + }; + } } } diff --git a/tests/Acceptance/XmlSerialization.cs b/tests/Acceptance/XmlSerialization.cs index 68f82f2..ee3470a 100644 --- a/tests/Acceptance/XmlSerialization.cs +++ b/tests/Acceptance/XmlSerialization.cs @@ -2,7 +2,6 @@ { using System; using System.IO; - using FakeItEasy; using FluentAssertions; using Xbehave; @@ -19,7 +18,6 @@ public interface IService public static void SerializeVoidCall( string path, IRecordedCallRepository repository, - IService realServiceWhileRecording, int voidMethodOutInteger, DateTime voidMethodRefDateTime, Guid nonVoidMethodResult) @@ -30,28 +28,15 @@ public static void SerializeVoidCall( "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)) + using (var fakeService = SelfInitializingFake.For(() => new Service(), repository)) { + DateTime discardDateTime = DateTime.MaxValue; var fake = fakeService.Object; - fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); - nonVoidMethodResult = fake.NonVoidMethod(); + fake.VoidMethod("recordingCallKey", out _, ref discardDateTime); + _ = fake.NonVoidMethod(); } }); @@ -61,27 +46,12 @@ public static void SerializeVoidCall( using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) { var fake = playbackFakeService.Object; - int i; - DateTime dt = DateTime.MinValue; - fake.VoidMethod("blah", out i, ref dt); + fake.VoidMethod("blah", out voidMethodOutInteger, ref voidMethodRefDateTime); + nonVoidMethodResult = fake.NonVoidMethod(); } }); - "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" + "Then the playback fake returns the recorded out and ref parameters and results" .x(() => { voidMethodOutInteger.Should().Be(17); @@ -91,5 +61,16 @@ public static void SerializeVoidCall( } private static IService UnusedFactory() => null!; + + private class Service : IService + { + public void VoidMethod(string s, out int i, ref DateTime dt) + { + i = 17; + dt = new DateTime(2017, 1, 24); + } + + public Guid NonVoidMethod() => new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd"); + } } } 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 From 658d4581cd2c66e08eae55c1221009a462d398a2 Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Sat, 20 Jun 2020 07:38:56 -0400 Subject: [PATCH 2/9] Extract SampleService helper --- tests/Acceptance/BinarySerialization.cs | 37 ++++++---------------- tests/Acceptance/FileBasedSerializers.cs | 24 ++++++-------- tests/Acceptance/Helpers/ISampleService.cs | 14 ++++++++ tests/Acceptance/Helpers/SampleService.cs | 21 ++++++++++++ tests/Acceptance/Helpers/TypeExtensions.cs | 2 +- tests/Acceptance/XmlSerialization.cs | 34 ++++++-------------- 6 files changed, 63 insertions(+), 69 deletions(-) create mode 100644 tests/Acceptance/Helpers/ISampleService.cs create mode 100644 tests/Acceptance/Helpers/SampleService.cs diff --git a/tests/Acceptance/BinarySerialization.cs b/tests/Acceptance/BinarySerialization.cs index 3812290..5de63dc 100644 --- a/tests/Acceptance/BinarySerialization.cs +++ b/tests/Acceptance/BinarySerialization.cs @@ -3,25 +3,20 @@ using System; using System.Collections.Generic; using System.IO; + using FluentAssertions; + using SelfInitializingFakes.Tests.Acceptance.Helpers; using Xbehave; public static class BinarySerialization { - public interface IService - { - void VoidMethod(string s, out int i, ref DateTime dt); - - IDictionary NonVoidMethod(); - } - [Scenario] public static void SerializeVoidCall( string path, IRecordedCallRepository repository, int voidMethodOutInteger, DateTime voidMethodRefDateTime, - IDictionary nonVoidMethodResult) + IDictionary dictionaryMethodResult) { "Given a file path" .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); @@ -32,23 +27,23 @@ public static void SerializeVoidCall( "When I use a self-initializing fake in recording mode" .x(() => { - using (var fakeService = SelfInitializingFake.For(() => new Service(), repository)) + using (var fakeService = SelfInitializingFake.For(() => new SampleService(), repository)) { DateTime discardDateTime = DateTime.MaxValue; var fake = fakeService.Object; fake.VoidMethod("firstCallKey", out _, ref discardDateTime); - _ = fake.NonVoidMethod(); + _ = fake.DictionaryReturningMethod(); } }); "And I use a self-initializing fake in playback mode" .x(() => { - using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) + using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) { var fake = playbackFakeService.Object; fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); - nonVoidMethodResult = fake.NonVoidMethod(); + dictionaryMethodResult = fake.DictionaryReturningMethod(); } }); @@ -57,27 +52,13 @@ public static void SerializeVoidCall( { 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!; - - private class Service : IService - { - public void VoidMethod(string s, out int i, ref DateTime dt) - { - i = 17; - dt = new DateTime(2017, 1, 24); - } - - public IDictionary NonVoidMethod() => new Dictionary - { - ["key1"] = new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd"), - }; - } + private static ISampleService UnusedFactory() => null!; } } 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..eebecb1 --- /dev/null +++ b/tests/Acceptance/Helpers/ISampleService.cs @@ -0,0 +1,14 @@ +namespace SelfInitializingFakes.Tests.Acceptance.Helpers +{ + using System; + using System.Collections.Generic; + + public interface ISampleService + { + void VoidMethod(string s, out int i, ref DateTime dt); + + Guid GuidReturningMethod(); + + IDictionary DictionaryReturningMethod(); + } +} \ 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..0c3212f --- /dev/null +++ b/tests/Acceptance/Helpers/SampleService.cs @@ -0,0 +1,21 @@ +namespace SelfInitializingFakes.Tests.Acceptance.Helpers +{ + using System; + using System.Collections.Generic; + + 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"), + }; + } +} \ 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/XmlSerialization.cs b/tests/Acceptance/XmlSerialization.cs index ee3470a..5aea532 100644 --- a/tests/Acceptance/XmlSerialization.cs +++ b/tests/Acceptance/XmlSerialization.cs @@ -2,25 +2,20 @@ { using System; using System.IO; + using FluentAssertions; + using SelfInitializingFakes.Tests.Acceptance.Helpers; using Xbehave; public static class XmlSerialization { - public interface IService - { - void VoidMethod(string s, out int i, ref DateTime dt); - - Guid NonVoidMethod(); - } - [Scenario] public static void SerializeVoidCall( string path, IRecordedCallRepository repository, int voidMethodOutInteger, DateTime voidMethodRefDateTime, - Guid nonVoidMethodResult) + Guid guidMethodResult) { "Given a file path" .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xml")); @@ -31,23 +26,23 @@ public static void SerializeVoidCall( "When I use a self-initializing fake in recording mode" .x(() => { - using (var fakeService = SelfInitializingFake.For(() => new Service(), repository)) + using (var fakeService = SelfInitializingFake.For(() => new SampleService(), repository)) { DateTime discardDateTime = DateTime.MaxValue; var fake = fakeService.Object; fake.VoidMethod("recordingCallKey", out _, ref discardDateTime); - _ = fake.NonVoidMethod(); + _ = fake.GuidReturningMethod(); } }); "And I use a self-initializing fake in playback mode" .x(() => { - using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) + using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) { var fake = playbackFakeService.Object; fake.VoidMethod("blah", out voidMethodOutInteger, ref voidMethodRefDateTime); - nonVoidMethodResult = fake.NonVoidMethod(); + guidMethodResult = fake.GuidReturningMethod(); } }); @@ -56,21 +51,10 @@ public static void SerializeVoidCall( { voidMethodOutInteger.Should().Be(17); voidMethodRefDateTime.Should().Be(new DateTime(2017, 1, 24)); - nonVoidMethodResult.Should().Be(new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd")); + guidMethodResult.Should().Be(new Guid("5b61d48f-e9e5-49ad-9c51-a9aae056aa84")); }); } - private static IService UnusedFactory() => null!; - - private class Service : IService - { - public void VoidMethod(string s, out int i, ref DateTime dt) - { - i = 17; - dt = new DateTime(2017, 1, 24); - } - - public Guid NonVoidMethod() => new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd"); - } + private static ISampleService UnusedFactory() => null!; } } From fa48caf9b9bad27ce714c6ae82339349267e9f4a Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Fri, 19 Jun 2020 19:36:00 -0400 Subject: [PATCH 3/9] Convert Lazys when recording and playing back --- .../Infrastructure/ITypeConverter.cs | 27 ++++++++ .../Infrastructure/LazyTypeConverter.cs | 61 +++++++++++++++++++ .../Infrastructure/PlaybackRule.cs | 32 +++++++--- .../Infrastructure/RecordingRule.cs | 24 +++++++- .../Infrastructure/TypeExtensions.cs | 27 ++++++++ .../SelfInitializingFake.of.T.cs | 7 ++- tests/Acceptance/BinarySerialization.cs | 21 ++++++- tests/Acceptance/Helpers/ISampleService.cs | 6 ++ tests/Acceptance/Helpers/SampleService.cs | 12 +++- tests/Acceptance/XmlSerialization.cs | 21 ++++++- 10 files changed, 222 insertions(+), 16 deletions(-) create mode 100644 src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs create mode 100644 src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs create mode 100644 src/SelfInitializingFakes/Infrastructure/TypeExtensions.cs diff --git a/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs b/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs new file mode 100644 index 0000000..ee6900e --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs @@ -0,0 +1,27 @@ +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. + /// 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, out object? output); + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// 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, 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..a966651 --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs @@ -0,0 +1,61 @@ +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. + /// 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, 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); + return true; + } + + return false; + } + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// 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, out object? output) + { + if (deserializedType.IsInstanceOf(typeof(Lazy<>))) + { + var typeOfLazyResult = deserializedType.GetGenericArguments()[0]; + var method = CreateLazyGenericDefinition.MakeGenericMethod(typeOfLazyResult); + output = method.Invoke(null, new object?[] { input }); + 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..d437a6e 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, 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,16 @@ 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, + 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..98d6332 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, out object? convertedReturnValue)) + { + call.ReturnValue = convertedReturnValue; + } + + for (int i = 0; i < call.OutAndRefValues.Length; ++i) + { + if (this.typeConverter.ConvertForRecording(call.OutAndRefValues[i], out object? convertedValue)) + { + call.OutAndRefValues[i] = convertedValue; + } + } + } } } 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..9548578 100644 --- a/src/SelfInitializingFakes/SelfInitializingFake.of.T.cs +++ b/src/SelfInitializingFakes/SelfInitializingFake.of.T.cs @@ -31,18 +31,21 @@ internal SelfInitializingFake(Func serviceFactory, IRecordedCallReposi this.repository = repository ?? throw new ArgumentNullException(nameof(repository)); + var typeConverter = new LazyTypeConverter(); var callsFromRepository = this.repository.Load(); if (callsFromRepository == null) { 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 5de63dc..e7e58ad 100644 --- a/tests/Acceptance/BinarySerialization.cs +++ b/tests/Acceptance/BinarySerialization.cs @@ -16,7 +16,10 @@ public static void SerializeVoidCall( IRecordedCallRepository repository, int voidMethodOutInteger, DateTime voidMethodRefDateTime, - IDictionary dictionaryMethodResult) + IDictionary dictionaryMethodResult, + Lazy lazyIntMethodResult, + Lazy lazyStringMethodResult, + Lazy lazyOutResult) { "Given a file path" .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); @@ -33,6 +36,9 @@ public static void SerializeVoidCall( var fake = fakeService.Object; fake.VoidMethod("firstCallKey", out _, ref discardDateTime); _ = fake.DictionaryReturningMethod(); + _ = fake.LazyIntReturningMethod(); + _ = fake.LazyStringReturningMethod(); + fake.MethodWithLazyOut(out _); } }); @@ -44,6 +50,9 @@ public static void SerializeVoidCall( var fake = playbackFakeService.Object; fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); dictionaryMethodResult = fake.DictionaryReturningMethod(); + lazyIntMethodResult = fake.LazyIntReturningMethod(); + lazyStringMethodResult = fake.LazyStringReturningMethod(); + fake.MethodWithLazyOut(out lazyOutResult); } }); @@ -52,10 +61,20 @@ public static void SerializeVoidCall( { voidMethodOutInteger.Should().Be(17); voidMethodRefDateTime.Should().Be(new DateTime(2017, 1, 24)); + dictionaryMethodResult.Should() .HaveCount(1).And .ContainKey("key1") .WhichValue.Should().Be(new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd")); + + lazyIntMethodResult.IsValueCreated.Should().BeFalse(); + lazyIntMethodResult.Value.Should().Be(3); + + lazyStringMethodResult.IsValueCreated.Should().BeFalse(); + lazyStringMethodResult.Value.Should().Be("three"); + + lazyOutResult.IsValueCreated.Should().BeFalse(); + lazyOutResult.Value.Should().Be(-14); }); } diff --git a/tests/Acceptance/Helpers/ISampleService.cs b/tests/Acceptance/Helpers/ISampleService.cs index eebecb1..b65a5ff 100644 --- a/tests/Acceptance/Helpers/ISampleService.cs +++ b/tests/Acceptance/Helpers/ISampleService.cs @@ -10,5 +10,11 @@ public interface ISampleService Guid GuidReturningMethod(); IDictionary DictionaryReturningMethod(); + + Lazy LazyIntReturningMethod(); + + Lazy LazyStringReturningMethod(); + + void MethodWithLazyOut(out Lazy lazyInt); } } \ No newline at end of file diff --git a/tests/Acceptance/Helpers/SampleService.cs b/tests/Acceptance/Helpers/SampleService.cs index 0c3212f..4876aac 100644 --- a/tests/Acceptance/Helpers/SampleService.cs +++ b/tests/Acceptance/Helpers/SampleService.cs @@ -14,8 +14,14 @@ public void VoidMethod(string s, out int i, ref DateTime dt) public Guid GuidReturningMethod() => new Guid("5b61d48f-e9e5-49ad-9c51-a9aae056aa84"); public IDictionary DictionaryReturningMethod() => new Dictionary - { - ["key1"] = new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd"), - }; + { + ["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); } } \ No newline at end of file diff --git a/tests/Acceptance/XmlSerialization.cs b/tests/Acceptance/XmlSerialization.cs index 5aea532..ce1bc05 100644 --- a/tests/Acceptance/XmlSerialization.cs +++ b/tests/Acceptance/XmlSerialization.cs @@ -15,7 +15,10 @@ public static void SerializeVoidCall( IRecordedCallRepository repository, int voidMethodOutInteger, DateTime voidMethodRefDateTime, - Guid guidMethodResult) + Guid guidMethodResult, + Lazy lazyIntMethodResult, + Lazy lazyStringMethodResult, + Lazy lazyOutResult) { "Given a file path" .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xml")); @@ -32,6 +35,9 @@ public static void SerializeVoidCall( var fake = fakeService.Object; fake.VoidMethod("recordingCallKey", out _, ref discardDateTime); _ = fake.GuidReturningMethod(); + _ = fake.LazyIntReturningMethod(); + _ = fake.LazyStringReturningMethod(); + fake.MethodWithLazyOut(out _); } }); @@ -43,6 +49,9 @@ public static void SerializeVoidCall( var fake = playbackFakeService.Object; fake.VoidMethod("blah", out voidMethodOutInteger, ref voidMethodRefDateTime); guidMethodResult = fake.GuidReturningMethod(); + lazyIntMethodResult = fake.LazyIntReturningMethod(); + lazyStringMethodResult = fake.LazyStringReturningMethod(); + fake.MethodWithLazyOut(out lazyOutResult); } }); @@ -51,7 +60,17 @@ public static void SerializeVoidCall( { voidMethodOutInteger.Should().Be(17); voidMethodRefDateTime.Should().Be(new DateTime(2017, 1, 24)); + guidMethodResult.Should().Be(new Guid("5b61d48f-e9e5-49ad-9c51-a9aae056aa84")); + + lazyIntMethodResult.IsValueCreated.Should().BeFalse(); + lazyIntMethodResult.Value.Should().Be(3); + + lazyStringMethodResult.IsValueCreated.Should().BeFalse(); + lazyStringMethodResult.Value.Should().Be("three"); + + lazyOutResult.IsValueCreated.Should().BeFalse(); + lazyOutResult.Value.Should().Be(-14); }); } From 5197577b7932b283aca134104b072c284e45c92c Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Sun, 21 Jun 2020 06:48:48 -0400 Subject: [PATCH 4/9] Extract common test cases for binary- and XML-based serialization --- tests/Acceptance/BinarySerialization.cs | 52 ++++--------- tests/Acceptance/TypeSerializationTestBase.cs | 74 +++++++++++++++++++ tests/Acceptance/XmlSerialization.cs | 73 +----------------- 3 files changed, 91 insertions(+), 108 deletions(-) create mode 100644 tests/Acceptance/TypeSerializationTestBase.cs diff --git a/tests/Acceptance/BinarySerialization.cs b/tests/Acceptance/BinarySerialization.cs index e7e58ad..06f4df8 100644 --- a/tests/Acceptance/BinarySerialization.cs +++ b/tests/Acceptance/BinarySerialization.cs @@ -8,76 +8,50 @@ using SelfInitializingFakes.Tests.Acceptance.Helpers; using Xbehave; - public static class BinarySerialization + public class BinarySerialization : TypeSerializationTestBase { [Scenario] - public static void SerializeVoidCall( - string path, + public void SerializeCallWithDictionary( IRecordedCallRepository repository, - int voidMethodOutInteger, - DateTime voidMethodRefDateTime, - IDictionary dictionaryMethodResult, - Lazy lazyIntMethodResult, - Lazy lazyStringMethodResult, - Lazy lazyOutResult) + IDictionary dictionaryMethodResult) { - "Given a file path" - .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + "Given a recorded call repository" + .x(() => repository = this.CreateRepository()); - "And a BinaryFileRecordedCallRepository targeting that path" - .x(() => repository = new BinaryFileRecordedCallRepository(path)); - - "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(() => new SampleService(), repository)) { - DateTime discardDateTime = DateTime.MaxValue; var fake = fakeService.Object; - fake.VoidMethod("firstCallKey", out _, ref discardDateTime); _ = fake.DictionaryReturningMethod(); - _ = fake.LazyIntReturningMethod(); - _ = fake.LazyStringReturningMethod(); - fake.MethodWithLazyOut(out _); } }); - "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)) { var fake = playbackFakeService.Object; - fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); dictionaryMethodResult = fake.DictionaryReturningMethod(); - lazyIntMethodResult = fake.LazyIntReturningMethod(); - lazyStringMethodResult = fake.LazyStringReturningMethod(); - fake.MethodWithLazyOut(out lazyOutResult); } }); "Then 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)); - dictionaryMethodResult.Should() .HaveCount(1).And .ContainKey("key1") .WhichValue.Should().Be(new Guid("6c7d8912-802a-43c0-82a2-cb811058a9bd")); - - lazyIntMethodResult.IsValueCreated.Should().BeFalse(); - lazyIntMethodResult.Value.Should().Be(3); - - lazyStringMethodResult.IsValueCreated.Should().BeFalse(); - lazyStringMethodResult.Value.Should().Be("three"); - - lazyOutResult.IsValueCreated.Should().BeFalse(); - lazyOutResult.Value.Should().Be(-14); }); } - private static ISampleService 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/TypeSerializationTestBase.cs b/tests/Acceptance/TypeSerializationTestBase.cs new file mode 100644 index 0000000..14e437d --- /dev/null +++ b/tests/Acceptance/TypeSerializationTestBase.cs @@ -0,0 +1,74 @@ +namespace SelfInitializingFakes.Tests.Acceptance +{ + using System; + using System.Collections.Generic; + + using FluentAssertions; + using SelfInitializingFakes.Tests.Acceptance.Helpers; + using Xbehave; + + public abstract class TypeSerializationTestBase + { + [Scenario] + public void SerializeCommonCalls( + IRecordedCallRepository repository, + int voidMethodOutInteger, + DateTime voidMethodRefDateTime, + Lazy lazyIntMethodResult, + Lazy lazyStringMethodResult, + Lazy lazyOutResult, + Task taskResult, + Task taskIntResult) + { + "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)) + { + DateTime discardDateTime = DateTime.MaxValue; + var fake = fakeService.Object; + fake.VoidMethod("firstCallKey", out _, ref discardDateTime); + _ = fake.LazyIntReturningMethod(); + _ = fake.LazyStringReturningMethod(); + fake.MethodWithLazyOut(out _); + } + }); + + "And I use a self-initializing fake in playback mode" + .x(() => + { + using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) + { + var fake = playbackFakeService.Object; + fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); + lazyIntMethodResult = fake.LazyIntReturningMethod(); + lazyStringMethodResult = fake.LazyStringReturningMethod(); + fake.MethodWithLazyOut(out lazyOutResult); + } + }); + + "Then 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)); + + lazyIntMethodResult.IsValueCreated.Should().BeFalse(); + lazyIntMethodResult.Value.Should().Be(3); + + lazyStringMethodResult.IsValueCreated.Should().BeFalse(); + lazyStringMethodResult.Value.Should().Be("three"); + + lazyOutResult.IsValueCreated.Should().BeFalse(); + lazyOutResult.Value.Should().Be(-14); + }); + } + + protected static ISampleService UnusedFactory() => null!; + + protected abstract IRecordedCallRepository CreateRepository(); + } +} diff --git a/tests/Acceptance/XmlSerialization.cs b/tests/Acceptance/XmlSerialization.cs index ce1bc05..c16edc0 100644 --- a/tests/Acceptance/XmlSerialization.cs +++ b/tests/Acceptance/XmlSerialization.cs @@ -3,77 +3,12 @@ using System; using System.IO; - using FluentAssertions; - using SelfInitializingFakes.Tests.Acceptance.Helpers; - using Xbehave; - - public static class XmlSerialization + public class XmlSerialization : TypeSerializationTestBase { - [Scenario] - public static void SerializeVoidCall( - string path, - IRecordedCallRepository repository, - int voidMethodOutInteger, - DateTime voidMethodRefDateTime, - Guid guidMethodResult, - Lazy lazyIntMethodResult, - Lazy lazyStringMethodResult, - Lazy lazyOutResult) + protected override IRecordedCallRepository CreateRepository() { - "Given a file path" - .x(() => path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xml")); - - "And a XmlFileRecordedCallRepository targeting that path" - .x(() => repository = new XmlFileRecordedCallRepository(path)); - - "When I use a self-initializing fake in recording mode" - .x(() => - { - using (var fakeService = SelfInitializingFake.For(() => new SampleService(), repository)) - { - DateTime discardDateTime = DateTime.MaxValue; - var fake = fakeService.Object; - fake.VoidMethod("recordingCallKey", out _, ref discardDateTime); - _ = fake.GuidReturningMethod(); - _ = fake.LazyIntReturningMethod(); - _ = fake.LazyStringReturningMethod(); - fake.MethodWithLazyOut(out _); - } - }); - - "And I use a self-initializing fake in playback mode" - .x(() => - { - using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) - { - var fake = playbackFakeService.Object; - fake.VoidMethod("blah", out voidMethodOutInteger, ref voidMethodRefDateTime); - guidMethodResult = fake.GuidReturningMethod(); - lazyIntMethodResult = fake.LazyIntReturningMethod(); - lazyStringMethodResult = fake.LazyStringReturningMethod(); - fake.MethodWithLazyOut(out lazyOutResult); - } - }); - - "Then 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)); - - guidMethodResult.Should().Be(new Guid("5b61d48f-e9e5-49ad-9c51-a9aae056aa84")); - - lazyIntMethodResult.IsValueCreated.Should().BeFalse(); - lazyIntMethodResult.Value.Should().Be(3); - - lazyStringMethodResult.IsValueCreated.Should().BeFalse(); - lazyStringMethodResult.Value.Should().Be("three"); - - lazyOutResult.IsValueCreated.Should().BeFalse(); - lazyOutResult.Value.Should().Be(-14); - }); + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xml"); + return new XmlFileRecordedCallRepository(path); } - - private static ISampleService UnusedFactory() => null!; } } From 0b8a79ea4fb3b7c7dacb16a845062b1786dd7ecc Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Sun, 21 Jun 2020 06:22:24 -0400 Subject: [PATCH 5/9] Add failing tests for Task serialization --- tests/Acceptance/Helpers/ISampleService.cs | 5 +++++ tests/Acceptance/Helpers/SampleService.cs | 5 +++++ tests/Acceptance/TypeSerializationTestBase.cs | 13 +++++++++++++ 3 files changed, 23 insertions(+) diff --git a/tests/Acceptance/Helpers/ISampleService.cs b/tests/Acceptance/Helpers/ISampleService.cs index b65a5ff..4c28f40 100644 --- a/tests/Acceptance/Helpers/ISampleService.cs +++ b/tests/Acceptance/Helpers/ISampleService.cs @@ -2,6 +2,7 @@ namespace SelfInitializingFakes.Tests.Acceptance.Helpers { using System; using System.Collections.Generic; + using System.Threading.Tasks; public interface ISampleService { @@ -16,5 +17,9 @@ public interface ISampleService Lazy LazyStringReturningMethod(); void MethodWithLazyOut(out Lazy lazyInt); + + Task TaskReturningMethod(); + + Task TaskIntReturningMethod(); } } \ No newline at end of file diff --git a/tests/Acceptance/Helpers/SampleService.cs b/tests/Acceptance/Helpers/SampleService.cs index 4876aac..30f1029 100644 --- a/tests/Acceptance/Helpers/SampleService.cs +++ b/tests/Acceptance/Helpers/SampleService.cs @@ -2,6 +2,7 @@ namespace SelfInitializingFakes.Tests.Acceptance.Helpers { using System; using System.Collections.Generic; + using System.Threading.Tasks; public class SampleService : ISampleService { @@ -23,5 +24,9 @@ public void VoidMethod(string s, out int i, ref DateTime dt) public Lazy LazyStringReturningMethod() => new Lazy(() => "three"); public void MethodWithLazyOut(out Lazy lazyInt) => lazyInt = new Lazy(() => -14); + + public Task TaskReturningMethod() => Task.FromResult("void Task"); + + public Task TaskIntReturningMethod() => Task.FromResult(5); } } \ No newline at end of file diff --git a/tests/Acceptance/TypeSerializationTestBase.cs b/tests/Acceptance/TypeSerializationTestBase.cs index 14e437d..e4cec2d 100644 --- a/tests/Acceptance/TypeSerializationTestBase.cs +++ b/tests/Acceptance/TypeSerializationTestBase.cs @@ -2,6 +2,7 @@ namespace SelfInitializingFakes.Tests.Acceptance { using System; using System.Collections.Generic; + using System.Threading.Tasks; using FluentAssertions; using SelfInitializingFakes.Tests.Acceptance.Helpers; @@ -29,11 +30,15 @@ public void SerializeCommonCalls( using (var fakeService = SelfInitializingFake.For(() => new SampleService(), repository)) { DateTime discardDateTime = DateTime.MaxValue; + var fake = fakeService.Object; + fake.VoidMethod("firstCallKey", out _, ref discardDateTime); _ = fake.LazyIntReturningMethod(); _ = fake.LazyStringReturningMethod(); fake.MethodWithLazyOut(out _); + _ = fake.TaskReturningMethod(); + _ = fake.TaskIntReturningMethod(); } }); @@ -43,10 +48,13 @@ public void SerializeCommonCalls( using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) { var fake = playbackFakeService.Object; + fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); lazyIntMethodResult = fake.LazyIntReturningMethod(); lazyStringMethodResult = fake.LazyStringReturningMethod(); fake.MethodWithLazyOut(out lazyOutResult); + taskResult = fake.TaskReturningMethod(); + taskIntResult = fake.TaskIntReturningMethod(); } }); @@ -64,6 +72,11 @@ public void SerializeCommonCalls( lazyOutResult.IsValueCreated.Should().BeFalse(); lazyOutResult.Value.Should().Be(-14); + + taskResult.IsCompleted.Should().BeTrue(); + + taskIntResult.IsCompleted.Should().BeTrue(); + taskIntResult.Result.Should().Be(5); }); } From d7249eb3a239e1256917a5d5a4267b80698842ce Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Sun, 21 Jun 2020 08:09:02 -0400 Subject: [PATCH 6/9] Convert Tasks and Tasks when recording and playing back --- .../Infrastructure/CompoundTypeConverter.cs | 46 +++++++++++ .../Infrastructure/TaskTypeConverter.cs | 81 +++++++++++++++++++ .../SelfInitializingFake.of.T.cs | 7 +- tests/Acceptance/Helpers/SampleService.cs | 2 +- 4 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs create mode 100644 src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs diff --git a/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs b/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs new file mode 100644 index 0000000..8b54773 --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs @@ -0,0 +1,46 @@ +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. + /// 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, out object? output) => + this.first.ConvertForRecording(input, out output) || + this.second.ConvertForRecording(input, out output); + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// 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, out object? output) => + this.first.ConvertForPlayback(deserializedType, input, out output) || + this.second.ConvertForPlayback(deserializedType, input, out output); + } +} diff --git a/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs b/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs new file mode 100644 index 0000000..e3323fd --- /dev/null +++ b/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs @@ -0,0 +1,81 @@ +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. + /// 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, 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); + return true; + } + else if (inputType == typeof(Task)) + { + output = "{void Task}"; + return true; + } + + return false; + } + + /// + /// Potentially converts the serializable form of an object back to its unserializable form. + /// + /// The desired deserialized type. + /// An input object. + /// 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, out object? output) + { + if (deserializedType.IsInstanceOf(typeof(Task<>))) + { + var typeOfTaskResult = deserializedType.GetGenericArguments()[0]; + var method = CreateTaskGenericDefinition.MakeGenericMethod(typeOfTaskResult); + output = method.Invoke(null, new object?[] { input }); + 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/SelfInitializingFake.of.T.cs b/src/SelfInitializingFakes/SelfInitializingFake.of.T.cs index 9548578..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; @@ -31,13 +33,12 @@ internal SelfInitializingFake(Func serviceFactory, IRecordedCallReposi this.repository = repository ?? throw new ArgumentNullException(nameof(repository)); - var typeConverter = new LazyTypeConverter(); var callsFromRepository = this.repository.Load(); if (callsFromRepository == null) { var wrappedService = serviceFactory.Invoke(); this.Object = A.Fake(); - this.recordingRule = new RecordingRule(wrappedService, typeConverter); + this.recordingRule = new RecordingRule(wrappedService, TypeConverter); Fake.GetFakeManager(this.Object).AddRuleFirst(this.recordingRule); } else @@ -45,7 +46,7 @@ internal SelfInitializingFake(Func serviceFactory, IRecordedCallReposi this.Object = A.Fake(); Fake.GetFakeManager(this.Object).AddRuleFirst(new PlaybackRule( new Queue(callsFromRepository), - typeConverter)); + TypeConverter)); } } diff --git a/tests/Acceptance/Helpers/SampleService.cs b/tests/Acceptance/Helpers/SampleService.cs index 30f1029..b0de0d4 100644 --- a/tests/Acceptance/Helpers/SampleService.cs +++ b/tests/Acceptance/Helpers/SampleService.cs @@ -25,7 +25,7 @@ public void VoidMethod(string s, out int i, ref DateTime dt) public void MethodWithLazyOut(out Lazy lazyInt) => lazyInt = new Lazy(() => -14); - public Task TaskReturningMethod() => Task.FromResult("void Task"); + public Task TaskReturningMethod() => Task.Delay(0); public Task TaskIntReturningMethod() => Task.FromResult(5); } From 5c883337799df203ec162dba3702e42b3c502c58 Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Sun, 21 Jun 2020 14:11:38 -0400 Subject: [PATCH 7/9] Add failing test to serialize Lazy> --- tests/Acceptance/Helpers/ISampleService.cs | 2 ++ tests/Acceptance/Helpers/SampleService.cs | 2 ++ tests/Acceptance/TypeSerializationTestBase.cs | 9 ++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/Acceptance/Helpers/ISampleService.cs b/tests/Acceptance/Helpers/ISampleService.cs index 4c28f40..6920dfb 100644 --- a/tests/Acceptance/Helpers/ISampleService.cs +++ b/tests/Acceptance/Helpers/ISampleService.cs @@ -21,5 +21,7 @@ public interface ISampleService Task TaskReturningMethod(); Task TaskIntReturningMethod(); + + Lazy> LazyTaskIntReturningMethod(); } } \ No newline at end of file diff --git a/tests/Acceptance/Helpers/SampleService.cs b/tests/Acceptance/Helpers/SampleService.cs index b0de0d4..60eee9e 100644 --- a/tests/Acceptance/Helpers/SampleService.cs +++ b/tests/Acceptance/Helpers/SampleService.cs @@ -28,5 +28,7 @@ public void VoidMethod(string s, out int i, ref DateTime dt) public Task TaskReturningMethod() => Task.Delay(0); public Task TaskIntReturningMethod() => Task.FromResult(5); + + public Lazy> LazyTaskIntReturningMethod() => new Lazy>(() => Task.FromResult(19)); } } \ No newline at end of file diff --git a/tests/Acceptance/TypeSerializationTestBase.cs b/tests/Acceptance/TypeSerializationTestBase.cs index e4cec2d..73d8e43 100644 --- a/tests/Acceptance/TypeSerializationTestBase.cs +++ b/tests/Acceptance/TypeSerializationTestBase.cs @@ -19,7 +19,8 @@ public void SerializeCommonCalls( Lazy lazyStringMethodResult, Lazy lazyOutResult, Task taskResult, - Task taskIntResult) + Task taskIntResult, + Lazy> lazyTaskIntResult) { "Given a recorded call repository" .x(() => repository = this.CreateRepository()); @@ -39,6 +40,7 @@ public void SerializeCommonCalls( fake.MethodWithLazyOut(out _); _ = fake.TaskReturningMethod(); _ = fake.TaskIntReturningMethod(); + _ = fake.LazyTaskIntReturningMethod(); } }); @@ -55,6 +57,7 @@ public void SerializeCommonCalls( fake.MethodWithLazyOut(out lazyOutResult); taskResult = fake.TaskReturningMethod(); taskIntResult = fake.TaskIntReturningMethod(); + lazyTaskIntResult = fake.LazyTaskIntReturningMethod(); } }); @@ -77,6 +80,10 @@ public void SerializeCommonCalls( taskIntResult.IsCompleted.Should().BeTrue(); taskIntResult.Result.Should().Be(5); + + lazyTaskIntResult.IsValueCreated.Should().BeFalse(); + lazyTaskIntResult.Value.IsCompleted.Should().BeTrue(); + lazyTaskIntResult.Value.Result.Should().Be(19); }); } From adfd06181b563ab02094523c45e2d0032983262b Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Sun, 21 Jun 2020 15:49:16 -0400 Subject: [PATCH 8/9] Support converting Lazy Tasks and Tasky Lazes --- release_notes.md | 2 ++ .../Infrastructure/CompoundTypeConverter.cs | 14 ++++---- .../Infrastructure/ITypeConverter.cs | 6 ++-- .../Infrastructure/LazyTypeConverter.cs | 28 +++++++++++++--- .../Infrastructure/PlaybackRule.cs | 3 +- .../Infrastructure/RecordingRule.cs | 4 +-- .../Infrastructure/TaskTypeConverter.cs | 33 ++++++++++++++++--- tests/Acceptance/Helpers/ISampleService.cs | 2 ++ tests/Acceptance/Helpers/SampleService.cs | 2 ++ tests/Acceptance/TypeSerializationTestBase.cs | 9 ++++- 10 files changed, 81 insertions(+), 22 deletions(-) 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 index 8b54773..fab90fd 100644 --- a/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs +++ b/src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs @@ -26,21 +26,23 @@ public CompoundTypeConverter(ITypeConverter first, ITypeConverter 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, out object? output) => - this.first.ConvertForRecording(input, out output) || - this.second.ConvertForRecording(input, out output); + 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, out object? output) => - this.first.ConvertForPlayback(deserializedType, input, out output) || - this.second.ConvertForPlayback(deserializedType, input, out output); + 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 index ee6900e..868acae 100644 --- a/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs +++ b/src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs @@ -11,17 +11,19 @@ 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, out object? output); + 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, out object? output); + 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 index a966651..34664ea 100644 --- a/src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs +++ b/src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs @@ -15,9 +15,10 @@ internal class LazyTypeConverter : 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. - public bool ConvertForRecording(object? input, out object? output) + public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output) { output = null; if (input is null) @@ -29,6 +30,11 @@ public bool ConvertForRecording(object? input, out object? output) 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; } @@ -40,16 +46,28 @@ public bool ConvertForRecording(object? input, out object? output) /// /// 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, out object? output) + public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output) { if (deserializedType.IsInstanceOf(typeof(Lazy<>))) { var typeOfLazyResult = deserializedType.GetGenericArguments()[0]; - var method = CreateLazyGenericDefinition.MakeGenericMethod(typeOfLazyResult); - output = method.Invoke(null, new object?[] { input }); - return true; + + 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; diff --git a/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs b/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs index d437a6e..d193ad5 100644 --- a/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs +++ b/src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs @@ -50,7 +50,7 @@ public void Apply(IInterceptedFakeObjectCall fakeObjectCall) private void SetReturnValue(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall) { var returnValue = recordedCall.ReturnValue; - if (this.typeConverter.ConvertForPlayback(fakeObjectCall.Method.ReturnType, returnValue, out object? convertedReturnValue)) + if (this.typeConverter.ConvertForPlayback(fakeObjectCall.Method.ReturnType, returnValue, this.typeConverter, out object? convertedReturnValue)) { returnValue = convertedReturnValue; } @@ -70,6 +70,7 @@ private void SetOutAndRefValues(IInterceptedFakeObjectCall fakeObjectCall, Recor if (this.typeConverter.ConvertForPlayback( parameter.ParameterType.GetElementType(), parameterValue, + this.typeConverter, out object? convertedParameterValue)) { parameterValue = convertedParameterValue; diff --git a/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs b/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs index 98d6332..8b655c7 100644 --- a/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs +++ b/src/SelfInitializingFakes/Infrastructure/RecordingRule.cs @@ -125,14 +125,14 @@ private RecordedCall BuildRecordedCall(IFakeObjectCall call) private void ConvertRecordedCallForSerialization(RecordedCall call) { - if (this.typeConverter.ConvertForRecording(call.ReturnValue, out object? convertedReturnValue)) + 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], out object? convertedValue)) + 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 index e3323fd..cb0f6e8 100644 --- a/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs +++ b/src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs @@ -16,9 +16,10 @@ internal class TaskTypeConverter : 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. - public bool ConvertForRecording(object? input, out object? output) + public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output) { output = null; if (input is null) @@ -30,11 +31,21 @@ public bool ConvertForRecording(object? input, out object? output) 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; } @@ -46,16 +57,28 @@ public bool ConvertForRecording(object? input, out object? output) /// /// 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, out object? output) + public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output) { if (deserializedType.IsInstanceOf(typeof(Task<>))) { var typeOfTaskResult = deserializedType.GetGenericArguments()[0]; - var method = CreateTaskGenericDefinition.MakeGenericMethod(typeOfTaskResult); - output = method.Invoke(null, new object?[] { input }); - return true; + + 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)) { diff --git a/tests/Acceptance/Helpers/ISampleService.cs b/tests/Acceptance/Helpers/ISampleService.cs index 6920dfb..3321bbd 100644 --- a/tests/Acceptance/Helpers/ISampleService.cs +++ b/tests/Acceptance/Helpers/ISampleService.cs @@ -23,5 +23,7 @@ public interface ISampleService 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 index 60eee9e..8507e5a 100644 --- a/tests/Acceptance/Helpers/SampleService.cs +++ b/tests/Acceptance/Helpers/SampleService.cs @@ -30,5 +30,7 @@ public void VoidMethod(string s, out int i, ref DateTime dt) 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/TypeSerializationTestBase.cs b/tests/Acceptance/TypeSerializationTestBase.cs index 73d8e43..d29c22d 100644 --- a/tests/Acceptance/TypeSerializationTestBase.cs +++ b/tests/Acceptance/TypeSerializationTestBase.cs @@ -20,7 +20,8 @@ public void SerializeCommonCalls( Lazy lazyOutResult, Task taskResult, Task taskIntResult, - Lazy> lazyTaskIntResult) + Lazy> lazyTaskIntResult, + Task> taskLazyIntResult) { "Given a recorded call repository" .x(() => repository = this.CreateRepository()); @@ -41,6 +42,7 @@ public void SerializeCommonCalls( _ = fake.TaskReturningMethod(); _ = fake.TaskIntReturningMethod(); _ = fake.LazyTaskIntReturningMethod(); + _ = fake.TaskLazyIntReturningMethod(); } }); @@ -58,6 +60,7 @@ public void SerializeCommonCalls( taskResult = fake.TaskReturningMethod(); taskIntResult = fake.TaskIntReturningMethod(); lazyTaskIntResult = fake.LazyTaskIntReturningMethod(); + taskLazyIntResult = fake.TaskLazyIntReturningMethod(); } }); @@ -84,6 +87,10 @@ public void SerializeCommonCalls( lazyTaskIntResult.IsValueCreated.Should().BeFalse(); lazyTaskIntResult.Value.IsCompleted.Should().BeTrue(); lazyTaskIntResult.Value.Result.Should().Be(19); + + taskLazyIntResult.IsCompleted.Should().BeTrue(); + taskLazyIntResult.Result.IsValueCreated.Should().BeFalse(); + taskLazyIntResult.Result.Value.Should().Be(18); }); } From 6fb3f254241ab13b0e40e9514c27997b9d804ff3 Mon Sep 17 00:00:00 2001 From: Blair Conrad Date: Mon, 22 Jun 2020 07:29:56 -0400 Subject: [PATCH 9/9] Split common serialization test into test cases --- tests/Acceptance/TypeSerializationTestBase.cs | 259 ++++++++++++++---- tests/Directory.Build.props | 4 + 2 files changed, 211 insertions(+), 52 deletions(-) diff --git a/tests/Acceptance/TypeSerializationTestBase.cs b/tests/Acceptance/TypeSerializationTestBase.cs index d29c22d..7a30a24 100644 --- a/tests/Acceptance/TypeSerializationTestBase.cs +++ b/tests/Acceptance/TypeSerializationTestBase.cs @@ -2,26 +2,34 @@ 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( - IRecordedCallRepository repository, - int voidMethodOutInteger, - DateTime voidMethodRefDateTime, - Lazy lazyIntMethodResult, - Lazy lazyStringMethodResult, - Lazy lazyOutResult, - Task taskResult, - Task taskIntResult, - Lazy> lazyTaskIntResult, - Task> taskLazyIntResult) + public void SerializeCommonCalls(TestCase testCase, IRecordedCallRepository repository) { "Given a recorded call repository" .x(() => repository = this.CreateRepository()); @@ -31,18 +39,8 @@ public void SerializeCommonCalls( { using (var fakeService = SelfInitializingFake.For(() => new SampleService(), repository)) { - DateTime discardDateTime = DateTime.MaxValue; - var fake = fakeService.Object; - - fake.VoidMethod("firstCallKey", out _, ref discardDateTime); - _ = fake.LazyIntReturningMethod(); - _ = fake.LazyStringReturningMethod(); - fake.MethodWithLazyOut(out _); - _ = fake.TaskReturningMethod(); - _ = fake.TaskIntReturningMethod(); - _ = fake.LazyTaskIntReturningMethod(); - _ = fake.TaskLazyIntReturningMethod(); + testCase.Record(fake); } }); @@ -52,50 +50,207 @@ public void SerializeCommonCalls( using (var playbackFakeService = SelfInitializingFake.For(UnusedFactory, repository)) { var fake = playbackFakeService.Object; - - fake.VoidMethod("firstCallKey", out voidMethodOutInteger, ref voidMethodRefDateTime); - lazyIntMethodResult = fake.LazyIntReturningMethod(); - lazyStringMethodResult = fake.LazyStringReturningMethod(); - fake.MethodWithLazyOut(out lazyOutResult); - taskResult = fake.TaskReturningMethod(); - taskIntResult = fake.TaskIntReturningMethod(); - lazyTaskIntResult = fake.LazyTaskIntReturningMethod(); - taskLazyIntResult = fake.TaskLazyIntReturningMethod(); + testCase.Playback(fake); } }); "Then 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)); + testCase.Verify(); + }); + } - lazyIntMethodResult.IsValueCreated.Should().BeFalse(); - lazyIntMethodResult.Value.Should().Be(3); + protected static ISampleService UnusedFactory() => null!; - lazyStringMethodResult.IsValueCreated.Should().BeFalse(); - lazyStringMethodResult.Value.Should().Be("three"); + protected abstract IRecordedCallRepository CreateRepository(); - lazyOutResult.IsValueCreated.Should().BeFalse(); - lazyOutResult.Value.Should().Be(-14); + public abstract class TestCase + { + public abstract void Record(ISampleService service); - taskResult.IsCompleted.Should().BeTrue(); + public abstract void Playback(ISampleService service); - taskIntResult.IsCompleted.Should().BeTrue(); - taskIntResult.Result.Should().Be(5); + public abstract void Verify(); + } - lazyTaskIntResult.IsValueCreated.Should().BeFalse(); - lazyTaskIntResult.Value.IsCompleted.Should().BeTrue(); - lazyTaskIntResult.Value.Result.Should().Be(19); + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodWithOutIntegerAndRefDateTime : TestCase + { + private int voidMethodOutInteger; + private DateTime voidMethodRefDateTime; - taskLazyIntResult.IsCompleted.Should().BeTrue(); - taskLazyIntResult.Result.IsValueCreated.Should().BeFalse(); - taskLazyIntResult.Result.Value.Should().Be(18); - }); + 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)); + } } - protected static ISampleService UnusedFactory() => null!; + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Required for testing.")] + private class MethodThatReturnsLazyInt : TestCase + { + private Lazy? result; - protected abstract IRecordedCallRepository CreateRepository(); + 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/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 + +