Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing improvements #242

Merged
merged 2 commits into from
Jun 10, 2024
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
15 changes: 15 additions & 0 deletions DotNET.SDK.sln
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectionsTests", "Tests\P
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events", "Tests\Events\Events.csproj", "{56E57E45-CFFB-42F2-A378-E155D9534CE8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ASP.NET.Tests", "Samples\ASP.NET.Tests\ASP.NET.Tests.csproj", "{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -848,6 +850,18 @@ Global
{56E57E45-CFFB-42F2-A378-E155D9534CE8}.Release|x64.Build.0 = Release|Any CPU
{56E57E45-CFFB-42F2-A378-E155D9534CE8}.Release|x86.ActiveCfg = Release|Any CPU
{56E57E45-CFFB-42F2-A378-E155D9534CE8}.Release|x86.Build.0 = Release|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Debug|x64.ActiveCfg = Debug|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Debug|x64.Build.0 = Debug|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Debug|x86.ActiveCfg = Debug|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Debug|x86.Build.0 = Debug|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Release|Any CPU.Build.0 = Release|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Release|x64.ActiveCfg = Release|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Release|x64.Build.0 = Release|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Release|x86.ActiveCfg = Release|Any CPU
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CCA92423-6099-4712-8599-46426D7895BC} = {4A67BEB1-4496-4C90-96C2-4D6E518C84C5}
Expand Down Expand Up @@ -909,5 +923,6 @@ Global
{25333224-9D85-4998-8D89-AFC3950801E1} = {4A67BEB1-4496-4C90-96C2-4D6E518C84C5}
{D4DBB1C0-6BBB-429B-BCB7-9962F5E8C7D6} = {E1103442-E3E5-43AE-B5B5-9F1965309E24}
{56E57E45-CFFB-42F2-A378-E155D9534CE8} = {E1103442-E3E5-43AE-B5B5-9F1965309E24}
{BF5E5AC9-C082-41E3-B2CF-2C49D2E8AE04} = {22418224-6C53-4187-A7FE-BDB8C1763764}
EndGlobalSection
EndGlobal
30 changes: 30 additions & 0 deletions Samples/ASP.NET.Tests/ASP.NET.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"/>
<PackageReference Include="xunit" Version="2.8.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Source\Testing\Testing.csproj" />
<ProjectReference Include="..\ASP.NET\ASP.NET.csproj" />
</ItemGroup>

</Project>
54 changes: 54 additions & 0 deletions Samples/ASP.NET.Tests/DishesPreparedByKitchenTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Dolittle.SDK.Testing.Projections;
using FluentAssertions;
using Kitchen;

namespace ASP.NET.Tests;

public class DishesPreparedByKitchenTests : ProjectionTests<DishesPreparedByKitchen>
{
const string KitchenId = "1";

[Fact]
public void WhenSingleDishPrepared()
{
var dish = "Some dish";
var chef = "Some chef";
WhenAggregateMutated<Kitchen.Kitchen>(KitchenId, it => it.PrepareDish(chef, dish));

AssertThat.HasReadModel(KitchenId)
.AndThat(it =>
{
it.DishesPrepared.Should().ContainKey(dish);
it.DishesPrepared[dish].Should().Be(1);
});
}

[Fact]
public void WhenMultipleDishesPrepared()
{
var dish = "Pizza";
var dish2 = "Burgers";
var chef = "Some chef";
WhenAggregateMutated<Kitchen.Kitchen>(KitchenId, it =>
{
it.Restock(8, "SomeSupplier");
it.PrepareDish(chef, dish);
it.PrepareDish(chef, dish2);
it.PrepareDish(chef, dish2);

});

AssertThat.HasReadModel(KitchenId)
.AndThat(it =>
{
it.DishesPrepared.Should().ContainKey(dish);
it.DishesPrepared[dish].Should().Be(1);

it.DishesPrepared.Should().ContainKey(dish2);
it.DishesPrepared[dish2].Should().Be(2);
});
}
}
92 changes: 92 additions & 0 deletions Samples/ASP.NET.Tests/KitchenTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Dolittle.SDK.Testing.Aggregates;
using FluentAssertions;
using Kitchen;

namespace ASP.NET.Tests;

public class KitchenTests : AggregateRootTests<Kitchen.Kitchen>
{
private const string Id = "SomeKitchenId";
private const string Chef = "SomeChef";
private const string Dish = "Pizza";

public KitchenTests() : base(Id)
{
}

[Fact]
public void WhenPreparingDish()
{
WhenPerforming(it => it.PrepareDish(Chef, Dish));

AssertThat.ShouldHaveSingleEvent<DishPrepared>().AndThat(evt =>
{
evt.Chef.Should().Be(Chef);
evt.Dish.Should().Be(Dish);
});
}

[Fact]
public void WhenPreparingDishWithoutIngredients()
{
WithAggregateInState(it =>
{
it.PrepareDish(Chef, Dish);
it.PrepareDish(Chef, Dish);
});

VerifyThrowsExactly<OutOfIngredients>(it => it.PrepareDish(Chef, Dish));
}

[Fact]
public void WhenPreparingAfterRestock()
{
WithAggregateInState(it =>
{
it.PrepareDish(Chef, Dish);
it.PrepareDish(Chef, Dish);
it.Restock(10, "SomeSupplier");
});

WhenPerforming(it => it.PrepareDish(Chef, Dish));

AssertThat.ShouldHaveSingleEvent<DishPrepared>().AndThat(evt =>
{
evt.Chef.Should().Be(Chef);
evt.Dish.Should().Be(Dish);
});
}

[Fact]
public void WhenRestocking()
{
WhenPerforming(it => it.Restock(5, "SomeSupplier"));

AssertThat.ShouldHaveSingleEvent<Restocked>().AndThat(evt =>
{
evt.Amount.Should().Be(5);
evt.Supplier.Should().Be("SomeSupplier");
});
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(int.MinValue)]
public void WhenRestockingWithInvalidAmount(int amount)
{
var exception = VerifyThrowsExactly<ArgumentException>(it => it.Restock(amount, "SomeSupplier"));

exception.ParamName.Should().Be("amount");
}

[Theory]
[InlineData(null, null)]
[InlineData("", "")]
[InlineData("", "ADish")]
[InlineData("Chefo", "")]
public void WhenPreparingDishWithInvalidArguments(string? chef, string? dish)
{
VerifyThrows(it => it.PrepareDish(chef, dish));
}
}
1 change: 1 addition & 0 deletions Samples/ASP.NET.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
26 changes: 26 additions & 0 deletions Samples/ASP.NET/Kitchen/DishesPrepared.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using Dolittle.SDK.Projections;

namespace Kitchen;

[Projection("65bb96a8-b4e0-45dd-a1b3-89c3dbbbb173")]
public class DishesPreparedByKitchen : ReadModel
{
private readonly Dictionary<string, int> _dishesPrepared = new();
private int _totalDishesPrepared;

public IReadOnlyDictionary<string, int> DishesPrepared => _dishesPrepared;
public int TotalDishesPrepared => _totalDishesPrepared;

public void On(DishPrepared evt, ProjectionContext context)
{
_totalDishesPrepared++;
if (!_dishesPrepared.TryAdd(evt.Dish, 1))
{
_dishesPrepared[evt.Dish]++;
}
}
}
16 changes: 14 additions & 2 deletions Samples/ASP.NET/Kitchen/Kitchen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,28 @@ public Kitchen(EventSourceId eventSource)

string Name => EventSourceId;

public void Restock(int amount, string supplier)
{
if (amount <= 0) throw new ArgumentException("Amount must be greater than 0", nameof(amount));
ArgumentException.ThrowIfNullOrEmpty(supplier, nameof(supplier));

Apply(new Restocked(amount, supplier));
Console.WriteLine($"Kitchen {EventSourceId} restocked with {amount} ingredients.");
}

public int PrepareDish(string chef, string dish)
{
ArgumentException.ThrowIfNullOrEmpty(chef, nameof(chef));
ArgumentException.ThrowIfNullOrEmpty(dish, nameof(dish));

if (_ingredients <= 0) throw new OutOfIngredients($"Kitchen {Name} has run out of ingredients, sorry!");
Apply(new DishPrepared(dish, chef));
Console.WriteLine($"Kitchen {EventSourceId} prepared a {dish}, there are {_ingredients} ingredients left.");
return _ingredients;
}

void On(DishPrepared @event)
=> _ingredients--;
void On(DishPrepared _) => _ingredients--;
void On(Restocked evt) => _ingredients += evt.Amount;
}

public class OutOfIngredients : Exception
Expand Down
9 changes: 9 additions & 0 deletions Samples/ASP.NET/Kitchen/Restocked.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Dolittle.SDK.Events;

namespace Kitchen;

[EventType("76faefee-102e-4c6f-b65d-0b47fedece42")]
public record Restocked(int Amount, string Supplier);
52 changes: 48 additions & 4 deletions Source/Testing/Aggregates/AggregateRootTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,53 @@ protected void WithAggregateInState(Action<T> action)
/// <returns>The <see cref="AggregateRootAssertion"/> to assert on</returns>
protected AggregateRootAssertion WhenPerforming(Action<T> action)
{
// last perform is the one that is asserted on
Aggregate.Perform(action).GetAwaiter().GetResult();
AssertThat = _aggregateOf.AfterLastOperationOn(_eventSourceId);
return AssertThat;
try
{
// last perform is the one that is asserted on
Aggregate.Perform(action).GetAwaiter().GetResult();
AssertThat = _aggregateOf.AfterLastOperationOn(_eventSourceId);
return AssertThat;
}
catch (AggregateRootOperationFailed e)
{
// Allow callers to assert on the domain exception instead of the wrapper.
throw e.InnerException ?? e;
}
}

/// <summary>
/// Verify that an exception is thrown when performing an action on the aggregate.
/// </summary>
/// <param name="action">Aggregate callback</param>
/// <exception cref="DolittleAssertionFailed">Thrown when expectation is not met.</exception>
protected Exception VerifyThrows(Action<T> action) => VerifyThrowsExactly<Exception>(action);

/// <summary>
/// Verify that an exception of the specific type is thrown when performing an action on the aggregate.
/// </summary>
/// <param name="action">Aggregate callback</param>
/// <typeparam name="TException">The expected Exception type to be thrown</typeparam>
/// <exception cref="DolittleAssertionFailed">Thrown when expectation is not met.</exception>
protected TException VerifyThrowsExactly<TException>(Action<T> action) where TException : Exception
{
try
{
WhenPerforming(action);
throw new DolittleAssertionFailed(
$"Expected exception of type {typeof(TException).Name} but no exception was thrown");
}
catch (Exception e)
{
if (e is TException exception)
{
// Expectation met
return exception;
}

throw new DolittleAssertionFailed(
$"Expected exception of type {typeof(TException).Name} but got {e.GetType().Name}");
}


}
}
Loading