Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions GalaxyCheck.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,11 +73,18 @@ 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
EndGlobalSection
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
24 changes: 24 additions & 0 deletions samples/GalaxyCheck.Samples.EverythingIsSerializable/Events.cs
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\GalaxyCheck.Xunit\GalaxyCheck.Xunit.csproj" />
<ProjectReference Include="..\..\src\GalaxyCheck\GalaxyCheck.csproj" />
</ItemGroup>

</Project>
86 changes: 86 additions & 0 deletions samples/GalaxyCheck.Samples.EverythingIsSerializable/Tests.cs
Original file line number Diff line number Diff line change
@@ -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<Type> _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<Type> 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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Xunit;

namespace GalaxyCheck.Samples.EverythingIsSerializable;

public static class XunitExtensions
{
public static TheoryData<T> ToTheoryData<T>(this IEnumerable<T> enumerable)
{
var theoryData = new TheoryData<T>();

foreach (var item in enumerable)
{
theoryData.Add(item);
}

return theoryData;
}
}