Skip to content

Commit 34d47a4

Browse files
committed
📖 Sample: "control" data and dynamic generation
1 parent caf0d21 commit 34d47a4

File tree

5 files changed

+170
-0
lines changed

5 files changed

+170
-0
lines changed

GalaxyCheck.sln

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit.CodeAnaly
2727
EndProject
2828
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GalaxyCheck.Tests.V3", "src\GalaxyCheck.Tests.V3\GalaxyCheck.Tests.V3.csproj", "{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}"
2929
EndProject
30+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{AAADE7B3-18E7-42F9-BFA8-5D8F97AE0088}"
31+
EndProject
32+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GalaxyCheck.Samples.EverythingIsSerializable", "samples\GalaxyCheck.Samples.EverythingIsSerializable\GalaxyCheck.Samples.EverythingIsSerializable.csproj", "{9D0FC288-5DFC-4659-A157-1E5DC4CDC137}"
33+
EndProject
3034
Global
3135
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3236
Debug|Any CPU = Debug|Any CPU
@@ -69,11 +73,18 @@ Global
6973
{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
7074
{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
7175
{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Release|Any CPU.Build.0 = Release|Any CPU
76+
{9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
77+
{9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Debug|Any CPU.Build.0 = Debug|Any CPU
78+
{9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Release|Any CPU.ActiveCfg = Release|Any CPU
79+
{9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Release|Any CPU.Build.0 = Release|Any CPU
7280
EndGlobalSection
7381
GlobalSection(SolutionProperties) = preSolution
7482
HideSolutionNode = FALSE
7583
EndGlobalSection
7684
GlobalSection(ExtensibilityGlobals) = postSolution
7785
SolutionGuid = {15E48146-1EC3-4A95-8482-8E21408F43E4}
7886
EndGlobalSection
87+
GlobalSection(NestedProjects) = preSolution
88+
{9D0FC288-5DFC-4659-A157-1E5DC4CDC137} = {AAADE7B3-18E7-42F9-BFA8-5D8F97AE0088}
89+
EndGlobalSection
7990
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace GalaxyCheck.Samples.EverythingIsSerializable;
2+
3+
public static class Events
4+
{
5+
public interface IEvent
6+
{
7+
string Type { get; }
8+
}
9+
10+
public record InvoiceCreatedEvent(string SupplierId, string CustomerId, string InvoiceId) : IEvent
11+
{
12+
public string Type => "InvoiceCreatedEvent";
13+
}
14+
15+
public record InvoiceUpdatedEvent(string SupplierId, string InvoiceId) : IEvent
16+
{
17+
public string Type => "InvoiceUpdatedEvent";
18+
}
19+
20+
public record InvoicePaidEvent(string SupplierId, string InvoiceId, decimal PaidAmount) : IEvent
21+
{
22+
public string Type => "InvoicePaidEvent";
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="FluentAssertions" Version="6.11.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
15+
<PackageReference Include="xunit" Version="2.4.2" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
<PackageReference Include="coverlet.collector" Version="3.2.0">
21+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
22+
<PrivateAssets>all</PrivateAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<ProjectReference Include="..\..\src\GalaxyCheck.Xunit\GalaxyCheck.Xunit.csproj" />
28+
<ProjectReference Include="..\..\src\GalaxyCheck\GalaxyCheck.csproj" />
29+
</ItemGroup>
30+
31+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Xunit;
2+
3+
namespace GalaxyCheck.Samples.EverythingIsSerializable;
4+
5+
public static class XunitExtensions
6+
{
7+
public static TheoryData<T> ToTheoryData<T>(this IEnumerable<T> enumerable)
8+
{
9+
var theoryData = new TheoryData<T>();
10+
11+
foreach (var item in enumerable)
12+
{
13+
theoryData.Add(item);
14+
}
15+
16+
return theoryData;
17+
}
18+
}

0 commit comments

Comments
 (0)