Skip to content

Commit da8b029

Browse files
authored
Add Unit and Integration Tests (#34)
* add first test * ✨ Add unit tests for DispatchR configuration and handler registration * ✨ Add tests for handler inclusion/exclusion and exceptions for empty handler arrays * ✨ Add unit test for generic pipeline registration in DispatchR configuration * ✨ Add StreamRequestHandler tests and update Fixture with stream request handlers * ✨ Rename tests for clarity and add notification registration tests * ✨ Add unit tests for new pipeline behaviors and update service registration logic * ✨ Update build-release.yml to conditionally execute steps for versioned tags * ✨ Update build-release.yml to include coverage report file in Codecov action * ✨ Update Codecov action to version 5 and adjust token handling * ✨ Update build-release.yml to specify coverage report file for Codecov action * ✨ Update build-release.yml to change coverage report file path for Codecov action * ✨ Update build-release.yml to ensure consistent code coverage collection for unit and integration tests * ✨ Update build-release.yml to adjust coverage report file path for Codecov action * ✨ Update build-release.yml to consolidate unit and integration tests into a single test step with coverage collection * ✨ Update build-release.yml to remove redundant coverage report file specification for Codecov action * ✨ Update build-release.yml to add a step for listing files before uploading coverage to Codecov * ✨ Update build-release.yml to list files in the tests directory before uploading coverage to Codecov * ✨ Update test project files to upgrade coverlet packages to version 6.0.4 with updated asset settings * ✨ Update build-release.yml to list files in the DispatchR.IntegrationTest directory before uploading coverage to Codecov * ✨ Update build-release.yml to install Coverlet tool and modify test command for improved coverage collection * ✨ Update build-release.yml to remove unnecessary build step from test command * ✨ Update README.md to add Codecov badge for coverage tracking
1 parent 58e1b9b commit da8b029

47 files changed

Lines changed: 1245 additions & 42 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-release.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
tags:
66
- '*.*.*'
7+
pull_request:
8+
branches:
9+
- main
710

811
jobs:
912
build:
@@ -23,18 +26,35 @@ jobs:
2326
with:
2427
dotnet-version: '9.x'
2528

29+
- name: Install Coverlet
30+
run: dotnet tool install --global coverlet.console
31+
2632
- name: Restore
2733
run: dotnet restore src/DispatchR/DispatchR.csproj
2834

2935
- name: Build
3036
run: dotnet build src/DispatchR/DispatchR.csproj --configuration Release --no-restore
3137

38+
- name: Run Tests
39+
run: dotnet test --collect:"XPlat Code Coverage"
40+
41+
- name: List files
42+
run: ls -alh tests/DispatchR.IntegrationTest
43+
44+
- name: Upload Coverage to Codecov
45+
uses: codecov/codecov-action@v5
46+
env:
47+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
48+
3249
- name: Extract version from tag
3350
id: get_version
51+
if: startsWith(github.ref, 'refs/tags/v')
3452
run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
3553

3654
- name: Pack project
55+
if: startsWith(github.ref, 'refs/tags/v')
3756
run: dotnet pack src/DispatchR/DispatchR.csproj --configuration Release --no-build -o ./nupkgs /p:PackageVersion=${{ steps.get_version.outputs.version }}
3857

3958
- name: Push to NuGet
59+
if: startsWith(github.ref, 'refs/tags/v')
4060
run: dotnet nuget push "./nupkgs/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json

DispatchR.sln

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireModularSample.Service
3737
EndProject
3838
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireModularSample.ServiceB", "src\AspireModularExample\AspireModularSample.ServiceB\AspireModularSample.ServiceB.csproj", "{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}"
3939
EndProject
40+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.UnitTest", "tests\DispatchR.UnitTest\DispatchR.UnitTest.csproj", "{806030F5-86B1-4EFC-923C-94FF7D32DFC9}"
41+
EndProject
42+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.IntegrationTest", "tests\DispatchR.IntegrationTest\DispatchR.IntegrationTest.csproj", "{D8646A62-9FE7-4E79-861C-49391007F98A}"
43+
EndProject
44+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.TestCommon", "tests\DispatchR.TestCommon\DispatchR.TestCommon.csproj", "{F01B6563-64D0-4316-947C-AB75426D9924}"
45+
EndProject
4046
Global
4147
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4248
Debug|Any CPU = Debug|Any CPU
@@ -131,6 +137,42 @@ Global
131137
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x64.Build.0 = Release|Any CPU
132138
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x86.ActiveCfg = Release|Any CPU
133139
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x86.Build.0 = Release|Any CPU
140+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
141+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
142+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x64.ActiveCfg = Debug|Any CPU
143+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x64.Build.0 = Debug|Any CPU
144+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x86.ActiveCfg = Debug|Any CPU
145+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x86.Build.0 = Debug|Any CPU
146+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
147+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|Any CPU.Build.0 = Release|Any CPU
148+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x64.ActiveCfg = Release|Any CPU
149+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x64.Build.0 = Release|Any CPU
150+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x86.ActiveCfg = Release|Any CPU
151+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x86.Build.0 = Release|Any CPU
152+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
153+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|Any CPU.Build.0 = Debug|Any CPU
154+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x64.ActiveCfg = Debug|Any CPU
155+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x64.Build.0 = Debug|Any CPU
156+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x86.ActiveCfg = Debug|Any CPU
157+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x86.Build.0 = Debug|Any CPU
158+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|Any CPU.ActiveCfg = Release|Any CPU
159+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|Any CPU.Build.0 = Release|Any CPU
160+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x64.ActiveCfg = Release|Any CPU
161+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x64.Build.0 = Release|Any CPU
162+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x86.ActiveCfg = Release|Any CPU
163+
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x86.Build.0 = Release|Any CPU
164+
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
165+
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|Any CPU.Build.0 = Debug|Any CPU
166+
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x64.ActiveCfg = Debug|Any CPU
167+
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x64.Build.0 = Debug|Any CPU
168+
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x86.ActiveCfg = Debug|Any CPU
169+
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x86.Build.0 = Debug|Any CPU
170+
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|Any CPU.ActiveCfg = Release|Any CPU
171+
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|Any CPU.Build.0 = Release|Any CPU
172+
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x64.ActiveCfg = Release|Any CPU
173+
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x64.Build.0 = Release|Any CPU
174+
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x86.ActiveCfg = Release|Any CPU
175+
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x86.Build.0 = Release|Any CPU
134176
EndGlobalSection
135177
GlobalSection(SolutionProperties) = preSolution
136178
HideSolutionNode = FALSE
@@ -146,5 +188,8 @@ Global
146188
{3416F900-58F9-4AB6-AC8A-95B03C7BD9A3} = {BA3021C0-B64E-B700-D62A-004419E20C36}
147189
{7D2890FF-66F7-4870-BB89-952167AB0681} = {BA3021C0-B64E-B700-D62A-004419E20C36}
148190
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA} = {BA3021C0-B64E-B700-D62A-004419E20C36}
191+
{806030F5-86B1-4EFC-923C-94FF7D32DFC9} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529}
192+
{D8646A62-9FE7-4E79-861C-49391007F98A} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529}
193+
{F01B6563-64D0-4316-947C-AB75426D9924} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529}
149194
EndGlobalSection
150195
EndGlobal

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# DispatchR 🚀
22

33
![CI](https://github.com/hasanxdev/DispatchR/workflows/Release/badge.svg)
4+
[![codecov](https://codecov.io/github/hasanxdev/dispatchr/graph/badge.svg?token=1FUG5DPUOE)](https://codecov.io/github/hasanxdev/dispatchr)
45
[![NuGet](https://img.shields.io/nuget/dt/DispatchR.Mediator.svg)](https://www.nuget.org/packages/DispatchR.Mediator)
56
[![NuGet](https://img.shields.io/nuget/vpre/DispatchR.Mediator.svg)](https://www.nuget.org/packages/DispatchR.Mediator)
67

src/Benchmark/Program.cs

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.ComponentModel.DataAnnotations;
21
using Benchmark;
32
using Benchmark.Notification;
43
using Benchmark.SendRequest;
@@ -19,33 +18,36 @@
1918
.AddColumn(new OperationsColumn())
2019
);
2120

22-
public class OperationsColumn : IColumn
21+
namespace Benchmark
2322
{
24-
public string Id => nameof(OperationsColumn);
25-
public string ColumnName => "OpsCount";
26-
public bool AlwaysShow => true;
27-
public ColumnCategory Category => ColumnCategory.Custom;
28-
public int PriorityInCategory => -10;
29-
public bool IsNumeric => true;
30-
public UnitType UnitType => UnitType.Dimensionless;
31-
public string Legend => "Number of operations per invoke";
32-
33-
public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
23+
public class OperationsColumn : IColumn
3424
{
35-
return benchmarkCase.Descriptor.WorkloadMethod
36-
.GetCustomAttributes(typeof(BenchmarkAttribute), false)
37-
.Cast<BenchmarkAttribute>()
38-
.FirstOrDefault()?.OperationsPerInvoke.ToString() ?? "1";
39-
}
25+
public string Id => nameof(OperationsColumn);
26+
public string ColumnName => "OpsCount";
27+
public bool AlwaysShow => true;
28+
public ColumnCategory Category => ColumnCategory.Custom;
29+
public int PriorityInCategory => -10;
30+
public bool IsNumeric => true;
31+
public UnitType UnitType => UnitType.Dimensionless;
32+
public string Legend => "Number of operations per invoke";
4033

41-
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
42-
=> GetValue(summary, benchmarkCase);
34+
public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
35+
{
36+
return benchmarkCase.Descriptor.WorkloadMethod
37+
.GetCustomAttributes(typeof(BenchmarkAttribute), false)
38+
.Cast<BenchmarkAttribute>()
39+
.FirstOrDefault()?.OperationsPerInvoke.ToString() ?? "1";
40+
}
4341

44-
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase)
45-
{
46-
return true;
47-
}
42+
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
43+
=> GetValue(summary, benchmarkCase);
44+
45+
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase)
46+
{
47+
return true;
48+
}
4849

49-
public bool IsAvailable(Summary summary) => true;
50-
public bool IsDefault(Summary summary) => false;
50+
public bool IsAvailable(Summary summary) => true;
51+
public bool IsDefault(Summary summary) => false;
52+
}
5153
}

src/DispatchR/Configuration/ServiceRegistrator.cs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,22 @@ public static void RegisterHandlers(IServiceCollection services, List<Type> allT
5353
.Where(p =>
5454
{
5555
var interfaces = p.GetInterfaces();
56+
if (p.IsGenericType)
57+
{
58+
// handle generic pipelines
59+
return interfaces
60+
.FirstOrDefault(inter =>
61+
inter.IsGenericType &&
62+
inter.GetGenericTypeDefinition() == behaviorType)
63+
?.GetInterfaces().First().GetGenericTypeDefinition() ==
64+
handlerInterface.GetGenericTypeDefinition();
65+
}
66+
5667
return interfaces
57-
.FirstOrDefault(inter =>
58-
inter.IsGenericType &&
59-
inter.GetGenericTypeDefinition() == behaviorType)
60-
?.GetInterfaces().First().GetGenericTypeDefinition() ==
61-
handlerInterface.GetGenericTypeDefinition();
68+
.FirstOrDefault(inter =>
69+
inter.IsGenericType &&
70+
inter.GetGenericTypeDefinition() == behaviorType)
71+
?.GetInterfaces().First() == handlerInterface;
6272
}).ToList();
6373

6474
// Sort pipelines by the specified order passed via ConfigurationOptions
@@ -92,17 +102,28 @@ public static void RegisterHandlers(IServiceCollection services, List<Type> allT
92102
var responseTypeArg = handlerInterface.GenericTypeArguments[1];
93103
if (genericHandlerResponseIsAwaitable && handlerResponseTypeIsAwaitable)
94104
{
95-
if (genericHandlerResponseType.GetGenericTypeDefinition() !=
96-
handlerInterface.GenericTypeArguments[1].GetGenericTypeDefinition())
105+
var areGenericTypeArgumentsInHandlerInterfaceMismatched =
106+
genericHandlerResponseType.IsGenericType &&
107+
handlerInterface.GenericTypeArguments[1].IsGenericType &&
108+
genericHandlerResponseType.GetGenericTypeDefinition() !=
109+
handlerInterface.GenericTypeArguments[1].GetGenericTypeDefinition();
110+
111+
if (areGenericTypeArgumentsInHandlerInterfaceMismatched ||
112+
genericHandlerResponseType.IsGenericType ^
113+
handlerInterface.GenericTypeArguments[1].IsGenericType)
97114
{
98115
continue;
99116
}
100117

101118
// register async generic pipelines
102-
responseTypeArg = responseTypeArg.GenericTypeArguments[0];
119+
if (responseTypeArg.GenericTypeArguments.Any())
120+
{
121+
responseTypeArg = responseTypeArg.GenericTypeArguments[0];
122+
}
103123
}
104124

105-
var closedGenericType = pipeline.MakeGenericType(handlerInterface.GenericTypeArguments[0], responseTypeArg);
125+
var closedGenericType = pipeline.MakeGenericType(handlerInterface.GenericTypeArguments[0],
126+
responseTypeArg);
106127
services.AddKeyedScoped(typeof(IRequestHandler), key, closedGenericType);
107128
}
108129
else
@@ -130,7 +151,8 @@ public static void RegisterHandlers(IServiceCollection services, List<Type> allT
130151
}
131152
}
132153

133-
public static void RegisterNotification(IServiceCollection services, List<Type> allTypes, Type syncNotificationHandlerType)
154+
public static void RegisterNotification(IServiceCollection services, List<Type> allTypes,
155+
Type syncNotificationHandlerType)
134156
{
135157
var allNotifications = allTypes
136158
.Where(p =>
@@ -140,7 +162,7 @@ public static void RegisterNotification(IServiceCollection services, List<Type>
140162
.Select(i => i.GetGenericTypeDefinition())
141163
.Any(i => new[]
142164
{
143-
syncNotificationHandlerType
165+
syncNotificationHandlerType
144166
}.Contains(i));
145167
})
146168
.GroupBy(p =>
@@ -149,7 +171,7 @@ public static void RegisterNotification(IServiceCollection services, List<Type>
149171
.Where(i => i.IsGenericType)
150172
.First(i => new[]
151173
{
152-
syncNotificationHandlerType
174+
syncNotificationHandlerType
153175
}.Contains(i.GetGenericTypeDefinition()));
154176
return @interface.GenericTypeArguments.First();
155177
})
@@ -172,7 +194,8 @@ private static bool IsAwaitable(Type type)
172194
if (type.IsGenericType)
173195
{
174196
var genericDef = type.GetGenericTypeDefinition();
175-
return genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>) || genericDef == typeof(IAsyncEnumerable<>);
197+
return genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>) ||
198+
genericDef == typeof(IAsyncEnumerable<>);
176199
}
177200

178201
return false;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace DispatchR.Exceptions;
2+
3+
public class ExcludeHandlersCannotBeArrayEmptyException() : Exception("Exclude handlers cannot be array empty.")
4+
{
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace DispatchR.Exceptions;
2+
3+
public class HandlerNotFoundException<TRequest, TResponse>() : Exception(
4+
$"""
5+
Handler for request of type '{typeof(TRequest).Name}' returning '{typeof(TResponse).Name}' was not found.
6+
Make sure you have registered a handler that implements IRequestHandler<{typeof(TRequest).Name}, {typeof(TResponse).Name}> in the DI container.
7+
""");
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace DispatchR.Exceptions;
2+
3+
public class IncludeHandlersCannotBeArrayEmptyException() : Exception("Include handlers cannot be array empty.")
4+
{
5+
}

src/DispatchR/Extensions/ServiceCollectionExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using DispatchR.Requests.Stream;
66
using Microsoft.Extensions.DependencyInjection;
77
using System.Reflection;
8+
using DispatchR.Exceptions;
89

910
namespace DispatchR.Extensions;
1011

@@ -15,6 +16,16 @@ public static IServiceCollection AddDispatchR(this IServiceCollection services,
1516
var config = new ConfigurationOptions();
1617
configuration(config);
1718

19+
if (config is {IncludeHandlers.Count:0})
20+
{
21+
throw new IncludeHandlersCannotBeArrayEmptyException();
22+
}
23+
24+
if (config is {ExcludeHandlers.Count:0})
25+
{
26+
throw new ExcludeHandlersCannotBeArrayEmptyException();
27+
}
28+
1829
return services.AddDispatchR(config);
1930
}
2031

src/DispatchR/Requests/IMediator.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Runtime.CompilerServices;
2+
using DispatchR.Exceptions;
23
using DispatchR.Requests.Notification;
34
using DispatchR.Requests.Send;
45
using DispatchR.Requests.Stream;
@@ -23,8 +24,15 @@ public sealed class Mediator(IServiceProvider serviceProvider) : IMediator
2324
public TResponse Send<TRequest, TResponse>(IRequest<TRequest, TResponse> request,
2425
CancellationToken cancellationToken) where TRequest : class, IRequest
2526
{
26-
return serviceProvider.GetRequiredService<IRequestHandler<TRequest, TResponse>>()
27-
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
27+
try
28+
{
29+
return serviceProvider.GetRequiredService<IRequestHandler<TRequest, TResponse>>()
30+
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
31+
}
32+
catch (Exception e) when (e.Message.Contains("No service for type", StringComparison.OrdinalIgnoreCase))
33+
{
34+
throw new HandlerNotFoundException<TRequest, TResponse>();
35+
}
2836
}
2937

3038
public IAsyncEnumerable<TResponse> CreateStream<TRequest, TResponse>(IStreamRequest<TRequest, TResponse> request,

0 commit comments

Comments
 (0)