diff --git a/GalaxyCheck.sln b/GalaxyCheck.sln index d638ca75..adc6cb1d 100644 --- a/GalaxyCheck.sln +++ b/GalaxyCheck.sln @@ -27,6 +27,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit.CodeAnaly EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GalaxyCheck.Tests.V3", "src\GalaxyCheck.Tests.V3\GalaxyCheck.Tests.V3.csproj", "{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{AAADE7B3-18E7-42F9-BFA8-5D8F97AE0088}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GalaxyCheck.Samples.EverythingIsSerializable", "samples\GalaxyCheck.Samples.EverythingIsSerializable\GalaxyCheck.Samples.EverythingIsSerializable.csproj", "{9D0FC288-5DFC-4659-A157-1E5DC4CDC137}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +73,10 @@ Global {0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Release|Any CPU.Build.0 = Release|Any CPU + {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -76,4 +84,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {15E48146-1EC3-4A95-8482-8E21408F43E4} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9D0FC288-5DFC-4659-A157-1E5DC4CDC137} = {AAADE7B3-18E7-42F9-BFA8-5D8F97AE0088} + EndGlobalSection EndGlobal diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/Events.cs b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Events.cs new file mode 100644 index 00000000..9c20d44b --- /dev/null +++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Events.cs @@ -0,0 +1,24 @@ +namespace GalaxyCheck.Samples.EverythingIsSerializable; + +public static class Events +{ + public interface IEvent + { + string Type { get; } + } + + public record InvoiceCreatedEvent(string SupplierId, string CustomerId, string InvoiceId) : IEvent + { + public string Type => "InvoiceCreatedEvent"; + } + + public record InvoiceUpdatedEvent(string SupplierId, string InvoiceId) : IEvent + { + public string Type => "InvoiceUpdatedEvent"; + } + + public record InvoicePaidEvent(string SupplierId, string InvoiceId, decimal PaidAmount) : IEvent + { + public string Type => "InvoicePaidEvent"; + } +} diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/GalaxyCheck.Samples.EverythingIsSerializable.csproj b/samples/GalaxyCheck.Samples.EverythingIsSerializable/GalaxyCheck.Samples.EverythingIsSerializable.csproj new file mode 100644 index 00000000..cdb4ce82 --- /dev/null +++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/GalaxyCheck.Samples.EverythingIsSerializable.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/Tests.cs b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Tests.cs new file mode 100644 index 00000000..e41973b5 --- /dev/null +++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Tests.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using FluentAssertions; +using Xunit; +using static GalaxyCheck.Samples.EverythingIsSerializable.Events; + +namespace GalaxyCheck.Samples.EverythingIsSerializable; + +/** + * Demonstrates how to pass "control data" through to properties using Xunit's DataAttributes, and when you might want to do that instead of + * generating the value in the property itself. + * + * + * Scenario: + * + * I have an event-driven system. Commands are sent into the system, and command handlers may emit events. Events are then handled by a separate + * worker process, so events need to be serializable so they can be pushed into a queue service. It's not practical to write an end-to-end test + * covering every event - we have over 100 events right now, and we're adding more all the time. Because of this, sometimes we push new events that + * haven't had their serialization exercised end-to-end. Worse still, sometimes we modify an existing event and completely break a workflow in + * production! + * + * I want to make sure that all events are serializable. And when we add a new event, I want that to be covered by a test without needing to + * remember to do it. + * + * I can use a property for that! + */ +public class Tests +{ + private static readonly IEnumerable _eventTypes = + typeof(IEvent).Assembly.GetTypes().Where(it => it.IsAssignableTo(typeof(IEvent)) && it.IsClass); // Get all event types by reflection + + /** + * This is how properties are normally written. All variable values are generated, which means that any test-case generated by the property might + * receive any of our events. + * + * This is fine for most properties. But in this case, we actually want a property foreach event - as the serialization of each event is a + * distinct behaviour that we want to ensure is well-tested. + * + * Properties generate 100 tests by default, and the input to the property varies randomly. If we had more than 100 events, then it'd be + * impossible for a single run of the test suite to test each event. Also, each event would not be exercised all that thoroughly. + */ + [Property] + public Property AllEventsAreSerializable() + { + var gen = + from eventType in Gen.Element(_eventTypes) // Generate a random event type + from ev in Gen.Create(eventType) // Then, generate a random instance of that event type + select ev; + + return Property.ForAll(gen, event0 => + { + var event1 = SerializeDeserialize(event0); + event1.Should().BeEquivalentTo(event0); + }); + } + + public static TheoryData EventTypesTheoryData => _eventTypes.ToTheoryData(); + + /** + * Alternatively, we can use "control data" to express our tests. + * + * This treats the "event type" as a control variable, and we can create a property foreach event type. Each property then generates 100 tests (by + * default, but we mightn't need that many now!). This is similar to the relationship between Facts and Theories in Xunit - adding control data + * (using MemberData, or InlineData) will multiply a property by each control value. + * + * As a bonus effect, each property that is created by this method are treated as individual "test-cases" in your test explorer. This means that + * you can easily run a single test for a single event type, and you can see which event types are covered by your test suite. + */ + [Property] + [MemberData(nameof(EventTypesTheoryData))] + public Property EventIsSerializable(Type eventType) + { + var gen = Gen.Create(eventType); + + return Property.ForAll(gen, event0 => + { + var event1 = SerializeDeserialize(event0); + event1.Should().BeEquivalentTo(event0); + }); + } + + private static object? SerializeDeserialize(object value) + { + var str = JsonSerializer.Serialize(value); + return JsonSerializer.Deserialize(str, value.GetType()); + } +} diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/XunitExtensions.cs b/samples/GalaxyCheck.Samples.EverythingIsSerializable/XunitExtensions.cs new file mode 100644 index 00000000..7bedb55c --- /dev/null +++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/XunitExtensions.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace GalaxyCheck.Samples.EverythingIsSerializable; + +public static class XunitExtensions +{ + public static TheoryData ToTheoryData(this IEnumerable enumerable) + { + var theoryData = new TheoryData(); + + foreach (var item in enumerable) + { + theoryData.Add(item); + } + + return theoryData; + } +}