|
| 1 | +using System.Text.Json; |
| 2 | +using FluentAssertions; |
| 3 | +using Xunit; |
| 4 | +using static GalaxyCheck.Samples.EverythingIsSerializable.Events; |
| 5 | + |
| 6 | +namespace GalaxyCheck.Samples.EverythingIsSerializable; |
| 7 | + |
| 8 | +/** |
| 9 | + * Demonstrates how to pass "control data" through to properties using Xunit's DataAttributes, and when you might want to do that instead of |
| 10 | + * generating the value in the property itself. |
| 11 | + * |
| 12 | + * |
| 13 | + * Scenario: |
| 14 | + * |
| 15 | + * 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 |
| 16 | + * 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 |
| 17 | + * 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 |
| 18 | + * haven't had their serialization exercised end-to-end. Worse still, sometimes we modify an existing event and completely break a workflow in |
| 19 | + * production! |
| 20 | + * |
| 21 | + * 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 |
| 22 | + * remember to do it. |
| 23 | + * |
| 24 | + * I can use a property for that! |
| 25 | + */ |
| 26 | +public class Tests |
| 27 | +{ |
| 28 | + private static readonly IEnumerable<Type> _eventTypes = |
| 29 | + typeof(IEvent).Assembly.GetTypes().Where(it => it.IsAssignableTo(typeof(IEvent)) && it.IsClass); // Get all event types by reflection |
| 30 | + |
| 31 | + /** |
| 32 | + * This is how properties are normally written. All variable values are generated, which means that any test-case generated by the property might |
| 33 | + * receive any of our events. |
| 34 | + * |
| 35 | + * 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 |
| 36 | + * distinct behaviour that we want to ensure is well-tested. |
| 37 | + * |
| 38 | + * 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 |
| 39 | + * impossible for a single run of the test suite to test each event. Also, each event would not be exercised all that thoroughly. |
| 40 | + */ |
| 41 | + [Property] |
| 42 | + public Property AllEventsAreSerializable() |
| 43 | + { |
| 44 | + var gen = |
| 45 | + from eventType in Gen.Element(_eventTypes) // Generate a random event type |
| 46 | + from ev in Gen.Create(eventType) // Then, generate a random instance of that event type |
| 47 | + select ev; |
| 48 | + |
| 49 | + return Property.ForAll(gen, event0 => |
| 50 | + { |
| 51 | + var event1 = SerializeDeserialize(event0); |
| 52 | + event1.Should().BeEquivalentTo(event0); |
| 53 | + }); |
| 54 | + } |
| 55 | + |
| 56 | + public static TheoryData<Type> EventTypesTheoryData => _eventTypes.ToTheoryData(); |
| 57 | + |
| 58 | + /** |
| 59 | + * Alternatively, we can use "control data" to express our tests. |
| 60 | + * |
| 61 | + * 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 |
| 62 | + * default, but we mightn't need that many now!). This is similar to the relationship between Facts and Theories in Xunit - adding control data |
| 63 | + * (using MemberData, or InlineData) will multiply a property by each control value. |
| 64 | + * |
| 65 | + * 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 |
| 66 | + * 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. |
| 67 | + */ |
| 68 | + [Property] |
| 69 | + [MemberData(nameof(EventTypesTheoryData))] |
| 70 | + public Property EventIsSerializable(Type eventType) |
| 71 | + { |
| 72 | + var gen = Gen.Create(eventType); |
| 73 | + |
| 74 | + return Property.ForAll(gen, event0 => |
| 75 | + { |
| 76 | + var event1 = SerializeDeserialize(event0); |
| 77 | + event1.Should().BeEquivalentTo(event0); |
| 78 | + }); |
| 79 | + } |
| 80 | + |
| 81 | + private static object? SerializeDeserialize(object value) |
| 82 | + { |
| 83 | + var str = JsonSerializer.Serialize(value); |
| 84 | + return JsonSerializer.Deserialize(str, value.GetType()); |
| 85 | + } |
| 86 | +} |
0 commit comments