From 5ee8b1b6422d2652e23b6d5c1f53d42349ad5827 Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Tue, 14 May 2024 15:36:13 -0400 Subject: [PATCH 1/8] save progress --- DapperAutoData.Examples/UnitTest1.cs | 3 + ...DapperAutoData.IntegrationUtilities.csproj | 9 + .../DapperServiceClientBase.cs | 221 ++++++++++++++++++ .../TokenProvider.cs | 8 + DapperAutoData.sln | 8 +- 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj create mode 100644 DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs create mode 100644 DapperAutoData.IntegrationUtilities/TokenProvider.cs diff --git a/DapperAutoData.Examples/UnitTest1.cs b/DapperAutoData.Examples/UnitTest1.cs index 1d4d7ab..e7579fa 100644 --- a/DapperAutoData.Examples/UnitTest1.cs +++ b/DapperAutoData.Examples/UnitTest1.cs @@ -1,4 +1,5 @@ using AutoFixture; +using FluentAssertions; namespace DapperAutoData.Examples; @@ -10,6 +11,8 @@ public class UnitTest1 public void Test1(TestClass a) { var b = a; + + a.Name.Should().Be("John"); } } diff --git a/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj b/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs b/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs new file mode 100644 index 0000000..aa16128 --- /dev/null +++ b/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs @@ -0,0 +1,221 @@ +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; +using System.Text; + +namespace DapperAutoData.IntegrationUtilities; + +public class DapperServiceClientBase +{ + protected readonly TokenProvider _tokenProvider; + protected readonly JsonSerializerOptions _jsonSerializerOptions; + public HttpClient _client; + + public DapperServiceClientBase(HttpClient httpclient, JsonSerializerOptions jsonSerializerOptions, TokenProvider tokenProvider) + { + _client = httpclient; + _jsonSerializerOptions = jsonSerializerOptions; + _tokenProvider = tokenProvider; + } + + #region Delete + + public async Task Delete(string endpoint, Dictionary headers, CancellationToken token) + { + SetHeaderRequest(headers); + + var response = await _client.DeleteAsync(endpoint, token).ConfigureAwait(false); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + public TResponse DeleteSync(string endpoint, Dictionary headers, CancellationToken token) + { + SetHeaderRequest(headers); + + var webRequest = new HttpRequestMessage(HttpMethod.Delete, endpoint); + + var response = _client.Send(webRequest); + + using var reader = new StreamReader(response.Content.ReadAsStream()); + string responseContent = reader.ReadToEnd(); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + #endregion + + #region Get + + /// + /// Utility for making async Get requests with url and query parameters + /// + public async Task Get(string endpoint, Dictionary headers, CancellationToken token) + { + + SetHeaderRequest(headers); + HttpResponseMessage response = await _client.GetAsync(endpoint, token).ConfigureAwait(false); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + public TResponse GetSync(string endpoint, Dictionary headers) + { + + SetHeaderRequest(headers); + var webRequest = new HttpRequestMessage(HttpMethod.Get, endpoint); + + var response = _client.Send(webRequest); + + using var reader = new StreamReader(response.Content.ReadAsStream()); + string responseContent = reader.ReadToEnd(); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + #endregion + + #region Post + public async Task Post(string endpoint, TRequest request, Dictionary headers, CancellationToken token) + { + + var requestContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, MediaTypeNames.Application.Json); + SetHeaderRequest(headers); + + + var response = await _client.PostAsync(endpoint, requestContent, token).ConfigureAwait(false); + + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + /// + /// Utility for making sync Post requests with body, url, and query parameters + /// + public TResponse PostSync(string endpoint, TRequest request, Dictionary headers) + { + SetHeaderRequest(headers); + + var webRequest = new HttpRequestMessage(HttpMethod.Post, endpoint) + { + Content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, MediaTypeNames.Application.Json) + }; + + var response = _client.Send(webRequest); + + using var reader = new StreamReader(response.Content.ReadAsStream()); + string responseContent = reader.ReadToEnd(); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + #endregion + + #region Put + + public async Task Put(string endpoint, TRequest request, Dictionary headers, CancellationToken token) + { + + SetHeaderRequest(headers); + + var requestContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, MediaTypeNames.Application.Json); + + var response = await _client.PutAsync(endpoint, requestContent, token).ConfigureAwait(false); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + /// + /// Utility for making sync Post requests with body, url, and query parameters + /// + public TResponse PutSync(string endpoint, TRequest request, Dictionary headers) + { + SetHeaderRequest(headers); + + var webRequest = new HttpRequestMessage(HttpMethod.Put, endpoint) + { + Content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, MediaTypeNames.Application.Json) + }; + + var response = _client.Send(webRequest); + + using var reader = new StreamReader(response.Content.ReadAsStream()); + string responseContent = reader.ReadToEnd(); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + + #endregion + + #region Patch + + public async Task Patch(string endpoint, TRequest request, Dictionary headers, CancellationToken token) + { + + SetHeaderRequest(headers); + + var requestContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, MediaTypeNames.Application.Json); + + var response = await _client.PatchAsync(endpoint, requestContent, token).ConfigureAwait(false); + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + /// + /// Utility for making sync Post requests with body, url, and query parameters + /// + public TResponse PatchSync(string endpoint, TRequest request, Dictionary headers) + { + SetHeaderRequest(headers); + + var webRequest = new HttpRequestMessage(HttpMethod.Patch, endpoint) + { + Content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, MediaTypeNames.Application.Json) + }; + + var response = _client.Send(webRequest); + + using var reader = new StreamReader(response.Content.ReadAsStream()); + string responseContent = reader.ReadToEnd(); + var model = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + + return model; + } + + + #endregion + + public void SetHeaderRequest(Dictionary headers) + { + + _client.DefaultRequestHeaders.Clear(); + + if (_client.DefaultRequestHeaders.Authorization != null && _tokenProvider.AccessToken != null) + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _tokenProvider.AccessToken); + + if (headers != null && headers.Any()) + { + foreach (var item in headers) + { + _client.DefaultRequestHeaders.Add(item.Key, item.Value); + } + } + + } +} diff --git a/DapperAutoData.IntegrationUtilities/TokenProvider.cs b/DapperAutoData.IntegrationUtilities/TokenProvider.cs new file mode 100644 index 0000000..efcd570 --- /dev/null +++ b/DapperAutoData.IntegrationUtilities/TokenProvider.cs @@ -0,0 +1,8 @@ +namespace DapperAutoData.IntegrationUtilities; + +public class TokenProvider +{ + public string? AccessToken { get; set; } + public DateTimeOffset? ExpiresAt { get; set; } + +} \ No newline at end of file diff --git a/DapperAutoData.sln b/DapperAutoData.sln index 1c67140..b3ad6ab 100644 --- a/DapperAutoData.sln +++ b/DapperAutoData.sln @@ -10,7 +10,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{ .github\workflows\main.yml = .github\workflows\main.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperAutoData.Examples", "DapperAutoData.Examples\DapperAutoData.Examples.csproj", "{939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperAutoData.Examples", "DapperAutoData.Examples\DapperAutoData.Examples.csproj", "{939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperAutoData.IntegrationUtilities", "DapperAutoData.IntegrationUtilities\DapperAutoData.IntegrationUtilities.csproj", "{B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,6 +28,10 @@ Global {939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}.Release|Any CPU.Build.0 = Release|Any CPU + {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 27f504d14735724eeb8955a27013ad3b1e484010 Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Wed, 15 May 2024 08:24:05 -0400 Subject: [PATCH 2/8] fix token issue --- .../DapperAutoData.IntegrationUtilities.csproj | 7 +++++++ .../DapperServiceClientBase.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj b/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj index fa71b7a..218f5ae 100644 --- a/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj +++ b/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj @@ -6,4 +6,11 @@ enable + + + \ + True + + + diff --git a/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs b/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs index aa16128..12a0ebc 100644 --- a/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs +++ b/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs @@ -206,7 +206,7 @@ public void SetHeaderRequest(Dictionary headers) _client.DefaultRequestHeaders.Clear(); - if (_client.DefaultRequestHeaders.Authorization != null && _tokenProvider.AccessToken != null) + if (_client.DefaultRequestHeaders.Authorization == null && _tokenProvider.AccessToken != null) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _tokenProvider.AccessToken); if (headers != null && headers.Any()) From ef101d4407cc6049cd905a15351c57e59a1e8a50 Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Wed, 15 May 2024 17:15:11 -0400 Subject: [PATCH 3/8] Fix project name --- .../DapperAutoData.SystemIntegrationUtilities.csproj | 7 ------- .../DapperServiceClientBase.cs | 2 +- .../TokenProvider.cs | 2 +- DapperAutoData.sln | 10 +++++----- 4 files changed, 7 insertions(+), 14 deletions(-) rename DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj => DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj (57%) rename {DapperAutoData.IntegrationUtilities => DapperAutoData.SystemIntegrationUtilities}/DapperServiceClientBase.cs (99%) rename {DapperAutoData.IntegrationUtilities => DapperAutoData.SystemIntegrationUtilities}/TokenProvider.cs (69%) diff --git a/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj similarity index 57% rename from DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj rename to DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj index 218f5ae..fa71b7a 100644 --- a/DapperAutoData.IntegrationUtilities/DapperAutoData.IntegrationUtilities.csproj +++ b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj @@ -6,11 +6,4 @@ enable - - - \ - True - - - diff --git a/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs b/DapperAutoData.SystemIntegrationUtilities/DapperServiceClientBase.cs similarity index 99% rename from DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs rename to DapperAutoData.SystemIntegrationUtilities/DapperServiceClientBase.cs index 12a0ebc..a4b53a1 100644 --- a/DapperAutoData.IntegrationUtilities/DapperServiceClientBase.cs +++ b/DapperAutoData.SystemIntegrationUtilities/DapperServiceClientBase.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Text; -namespace DapperAutoData.IntegrationUtilities; +namespace DapperAutoData.SystemIntegrationUtilities; public class DapperServiceClientBase { diff --git a/DapperAutoData.IntegrationUtilities/TokenProvider.cs b/DapperAutoData.SystemIntegrationUtilities/TokenProvider.cs similarity index 69% rename from DapperAutoData.IntegrationUtilities/TokenProvider.cs rename to DapperAutoData.SystemIntegrationUtilities/TokenProvider.cs index efcd570..7f1cd53 100644 --- a/DapperAutoData.IntegrationUtilities/TokenProvider.cs +++ b/DapperAutoData.SystemIntegrationUtilities/TokenProvider.cs @@ -1,4 +1,4 @@ -namespace DapperAutoData.IntegrationUtilities; +namespace DapperAutoData.SystemIntegrationUtilities; public class TokenProvider { diff --git a/DapperAutoData.sln b/DapperAutoData.sln index b3ad6ab..14ae498 100644 --- a/DapperAutoData.sln +++ b/DapperAutoData.sln @@ -12,7 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperAutoData.Examples", "DapperAutoData.Examples\DapperAutoData.Examples.csproj", "{939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperAutoData.IntegrationUtilities", "DapperAutoData.IntegrationUtilities\DapperAutoData.IntegrationUtilities.csproj", "{B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DapperAutoData.SystemIntegrationUtilities", "DapperAutoData.SystemIntegrationUtilities\DapperAutoData.SystemIntegrationUtilities.csproj", "{6A360AC6-E36B-4F44-BF0A-5FC9D132197C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -28,10 +28,10 @@ Global {939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {939D1ABE-0B1E-4FCB-BAFC-75483925A5CC}.Release|Any CPU.Build.0 = Release|Any CPU - {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B14AB889-66BD-4E1D-9851-07CEA0CAE5DF}.Release|Any CPU.Build.0 = Release|Any CPU + {6A360AC6-E36B-4F44-BF0A-5FC9D132197C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A360AC6-E36B-4F44-BF0A-5FC9D132197C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A360AC6-E36B-4F44-BF0A-5FC9D132197C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A360AC6-E36B-4F44-BF0A-5FC9D132197C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From bd995d17a878522ce91efefd8abe518cb1a3f693 Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Wed, 15 May 2024 17:43:21 -0400 Subject: [PATCH 4/8] save --- .../DapperAutoData.SystemIntegrationUtilities.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj index fa71b7a..b30c9b9 100644 --- a/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj +++ b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + 1.0.1 From 50ae7a61b67ec28b63b5124f7d2d201314651741 Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Tue, 21 May 2024 10:36:52 -0400 Subject: [PATCH 5/8] Added Read Me --- ...AutoData.SystemIntegrationUtilities.csproj | 7 + README.md | 308 +++++++----------- 2 files changed, 119 insertions(+), 196 deletions(-) diff --git a/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj index b30c9b9..8e52dbd 100644 --- a/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj +++ b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj @@ -7,4 +7,11 @@ 1.0.1 + + + \ + True + + + diff --git a/README.md b/README.md index e38028a..0d768c7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dapper Testing Library +# Dapper System Integration Testing Library As a fan of TDD, BDD, and anonymous testing I've made heavy use of AutoFixture, AutoMoq, and Fluent Assertions in my projects since around 2015. I've been lucky enough to work with a few other developers across these companies, and we found we kept needing to implement the same patterns and utilites every time we moved to a new project. We even lost some work and knowledge along the way. This library is an attempt to consolidate all of that knowledge into a single library that can be used across all of our projects. It's a work in progress, but I hope it can be useful to others as well. @@ -13,9 +13,6 @@ To be clear, I in no way intend for this to be seen as novel work or take credit - **AutoFixture** to generate anonymous data for your tests. - **AutoMoq** for automocking in your unit tests. - **Fluent Assertions** for more readable assertions. -- **Moq** for manual mocking. -- **Custom Data Generators** for generating specific types of data. -- **Bosus** for generating fake data. ## Useful Links @@ -26,249 +23,168 @@ To be clear, I in no way intend for this to be seen as novel work or take credit ## Installation -In package manager console run the command: `Install-Package DapperAutoData` +In package manager console run the command: `Install-Package DapperAutoData.SystemIntegrationUtilities` -The library is distributed as a nuget package. After installing the package all you need to do is add the `DapperAutoData` attribute to your `Theory` based test methods. Behavior can be modified by adding implementations of `IDataGenerator` and `IDapperProjectCustomization` to your test project. They will be automatically picked up by the library. +The library is distributed as a nuget package. After installing the package all you need to do is add the `DapperServiceClientBase` as base class to you client Implementation. This base class wrapper for http client and include TokenProvider if available. -The DataGeneratorInstaller will scan for, and use any implementation of IDataGenerator in the current assembly. ## How to Use -### Behavior Customization +### ExampleService -Autofixture can normally be customized by calling passing in an ICustimization to the fixture. The DapperAutoData will attribute will automatically apply any customizations that implement the IDapperProjectCustomization interface, which is a wrapper for ICustomization. ``` -public class DapperDataCustomizations : IDapperProjectCustomization +public interface IExampleService { - public void Customize(IFixture fixture) + Task> CreateExampleAsync(CreateExampleModel model, CancellationToken token); +} + +public class ExampleServiceClient : DapperServiceClientBase, IExampleService +{ + public ExampleServiceClient(HttpClient httpclient, JsonSerializerOptions jsonSerializerOptions, TokenProvider tokenProvider) : base(httpclient, jsonSerializerOptions, tokenProvider) { - fixture.RepeatCount = 3; - fixture.Behaviors.Remove(new ThrowingRecursionBehavior()); - fixture.Behaviors.Add(new OmitOnRecursionBehavior()); } -} -``` -### Creating a Data Generator + public async Task> ChangeNameAsync(string id, ChangeExampleNameModel model, CancellationToken token) + { + var endpoint = $"api/Example/{id}/ChangeName"; + var response = await this.Put>(endpoint, model, null, token).ConfigureAwait(false); -A major reason for using the Dapper Testing Library is to create custom data generators. As a project grows, it will build up a library of generators that can be used to create complex objects for testing. Each generator can be self-contained and updated individually, which allows for a team to focus only on the test cases they are working on, knowing that any data the test needs will be provided by the library in a valid state. Developers should take on the responsibility for creating and updating generators for the objects they are working with. They will be automatically picked up for use in the tests. + return response; + } +} -Generally, we follow one of two patterns for creating data generators. Initially the pattern was to to create a generator that explicitly defines EVERYTHING about an object. In practice, we've found this to be a bit cumbersome. +``` -So we have moved to a second pattern, where we create a generator that defines the minimum required fields that need to be customized. This makes it easier to refactor classes when there aren't a ton of customized fields. +### Creating a Client Generator and TokenProvider -Here is an example of a generator that defines the minimum required fields, and then allows the developer to customize the rest of the object as needed. This is the preferred pattern where possible: +Next, you need to create a Client Generator. This is a class that will generate a client for you. +Here is also an example on how to pregenerate a token for the token provider and include the token provider in the client generator. ``` -public class ExampleInfoGenerator : IDataGenerator +public class ClientServiceGenerators : IDataGenerator { + private static string _baseUri = "https://api.example.com"; + + private TokenProvider _tokenProvider; + public void RegisterGenerators(IFixture fixture) { - fixture.Customize(c => c - .With(x => x.UserId, fixture.Create().Value.ToString) - .With(x => x.Name, fixture.Create()) - ); - } -} -``` + RegisterJsonSerializer(fixture); + + var preGeneratedTokenProvider = new PreGeneratedTokenProvider(fixture); + _tokenProvider = preGeneratedTokenProvider.tokenProvider; -And here is an example of a generator that defines everything about an object. This can be a bit cumbersome for larger data objects, like POCOs and DTOs. Note that this is still a valuable pattern when defining more complex objects. For example, if a related object needs to be generated first and the data linked, or when there are dependencies between properties. + RegisterExampleService(fixture); + } -``` -// Notice that in this example we have to define Date and AccountBalance, even though we are not meaningfully customizing them. -// However, for this particular object the UserId must always be prefixed with the AccountType.Code. -// So despite the extra overhead, in this example it is still the best pattern to use, as it ensures that the UserId is always correct. -public class ExampleInfoGenerator -{ - public ExampleInfo GenerateExampleInfo() + private void RegisterJsonSerializer(IFixture fixture) { - var accountType = fixture.Create(); - fixture.Register(() => new ExampleInfo + fixture.Register(() => + { + + JsonSerializerOptions jsonSerializerOptions = new() + { + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + + return jsonSerializerOptions; + }); + } + private void RegisterExampleService(IFixture fixture) + { + fixture.Register(() => { - UserId = $"{accountType.Code}{fixture.Create().Value.ToString()}, - Name = fixture.Create(), - Date = fixture.Create(), - AccountBalance = fixture.Create(), - AccountType = accountType + var httpClient = new HttpClient + { + BaseAddress = new Uri(_baseUri) + }; + + return new ExampleServiceClient(httpClient, fixture.Create(), _tokenProvider); }); - } + } + } ``` -### Using Dapper Testing Library in your Tests - -The Dapper Testing Library is designed to be easy to use. It is effectively a wrapper around AutoFixture, AutoMoq, and Fluent Assertions that simplifies the process of setting up tests and generating data. You can start using it in your tests by adding the `DapperAutoData` attribute to your test methods. This will automatically generate data for your tests using AutoFixture and AutoMoq. - -Keep in mind that the `DapperAutoData` attribute is designed to work with `Theory` based tests. You can learn more about how to set up - -You can read more about how to use AutoFixture [here](https://autofixture.github.io/docs/quick-start/). - -Here are some examples of tests using the Dapper Testing Library: - -#### Example of a Unit Test ``` -/// This test will create an account with the minimum required fields and assert that no validation errors are returned. -[Theory] -[DapperAutoData] -public void Create_MinimumRequiredFields_NoValidationErrors(CreateAccountRequest request) +public class PreGeneratedTokenProvider { - // Arrange - - // Act - var account = new AccountEntity(request); - - // Assert - account.Validate().Items.Should().BeNullOrEmpty(); - account.LoginEmail.Should().Be(request.LoginEmail); - account.Address.Should().Be(request.Address); - account.ContactInfo.Should().Be(request.ContactInfo); + internal IAuthenticationServiceClient client; + public TokenProvider? tokenProvider; + internal IFixture fixture; + public PreGeneratedTokenProvider(IFixture fixture) + { + tokenProvider = TokenProviderTrackerCollection.GeTokenProviderSync(fixture); + } } -``` - -#### Example of a Theory Driven Test -``` -/// This test will create an account with a missing company name and assert that the address is successfully changed. -[Theory] -[DapperAutoData("")] -[DapperAutoData(" ")] -public void ChangeAddress_MissingCompany_ChangeSuccessful(StringCompanyName companyName, AccountEntity account, Address address) +public class TokenProviderTrackerCollection : IDisposable { - // Arrange - address.CompanyName = companyName; // address.CompanyName is a string field. Notice that StringCompanyName can be used in place of a string type. - - // Act - var result = account.ChangeAddress(address); - - // Assert - result.Items.Should().BeNullOrEmpty(); - account.Address.Should().Be(address); + private static TokenProvider? tokenProvider = null; + private static IAuthenticationServiceClient? client; + public void Dispose() + { + tokenProvider = null; + client = null; + } + + public static TokenProvider GeTokenProviderSync(IFixture fixture) + { + if (client == null) + { + client = fixture.Create(); + } + + if (tokenProvider == null) + { + var authenticationResponse = client.AuthenticateSync(); + tokenProvider = new TokenProvider + { + AccessToken = authenticationResponse.AccessToken + }; + } + return tokenProvider; + } } -``` - -#### Example of a Test with Injected Services in an Async Class ``` -/// This test demonstrates how you would test an class that uses injected interfaces. -/// The [Frozen] attribute marks the interface as a singleton, so it will be the same instance for all tests. -/// The AccountService class may have many injected services, but they will all be automatically mocked by AutoMoq. -/// This test will only need to deal with the specific interface that effects this test. -[Theory] -[DapperAutoData] -public async Task CreateAsync_CreateDuplicateAccount_ReturnsDuplicateAccountErrorAsync( CreateAccountRequest request, - [Frozen] Mock repo, - AccountService service ) -{ - // Arrange - repo.Setup(x => x.DoesEmailExistAsync(request.LoginEmail, It.IsAny())) - .ReturnsAsync(true); // To test this properly we need to ensure the repo reports that the email already exists. - - // Act - var response = await service.CreateAsync(request, CancellationToken.None); - // Assert - response.ErrorCode.Should().Be(ErrorCode.Duplicate); - response.IsSuccess.Should().BeFalse(); - response.ErrorMessage.Should().Contain(BrokerResponse.DuplicateEmailError); - repo.Verify(x => x.CreateAsync(It.IsAny(), It.IsAny()), Times.Never); -} -``` +#### Example of System integration test -#### Testing for synchronous Exceptions ``` -/// This test shows how you can test for exceptions in synchronus methods. -[Theory] -[DapperAutoData] -public void Create_InvalidAccount_ThrowsException(CreateAccountRequest request) +public class ExampleServiceClientTests { - // Arrange - var service = new AccountService(); - // Act - Action act = () => service.Create(request); + [Theory] + [DapperAutoData()] + public async Task CreateExampleAsync_ValidData_Success( + CreateExampleModel request, + IExampleService client) + { + // Arrange - // Assert - act.Should().Throw(); -} -``` -This will assert that calling the `Create` method with the provided `request` will throw an `InvalidAccountException`. + // Act + var response = await client.CreateExampleAsync(request, CancellationToken.None).ConfigureAwait(true); -#### Testing for asynchronous Exceptions + // Assert + response.Data.Name.Should().Be(request.Name); + response.Data.UserId.Should().Be(request.UserId); + } +} ``` -/// This test shows how you can test for exceptions in asynchronous methods. -[Theory] -[DapperAutoData] -public async Task CreateAsync_InvalidAccount_ThrowsExceptionAsync(CreateAccountRequest request) -{ - // Arrange - var service = new AccountService(); - // Act - Func act = async () => await service.CreateAsync(request, CancellationToken.None); - // Assert - await act.Should().ThrowAsync(); -} -``` +#### Example of a Test with Injected Services in an Async Class + + -## Existing Generator Types - -The Dapper Testing Library has a variety of built-in types that can be used to specify ranges and types of auto data. These can generally be used interchangeably with the types they are generating for, or you can grab the specific value from the `.Value` attribute. - -### String Generators - -- `StringParagraph` (string) -- `StringPersonFullName` (string) -- `StringPhoneNumber` (string) -- `StringSentence` (string) -- `StringSsn` (string) -- `StringWord` (string) -- `StringCompanyName` (string) -- `StringEmailTest` (string) -- `StringFirstName` (string) -- `StringInternetUrl` (string) -- `StringInternetUsername` (string) -- `StringCityName` (string) -- `StringCountryName` (string) -- `StringPostalCode` (string) -- `StringStateAbbreviation` (string) -- `StringStateFullName` (string) -- `StringStreetAddress` (string) -- `StringJobTitle` (string) -- `StringProductCategory` (string) -- `StringProductDescription` (string) -- `StringCurrency` (string) -- `StringFileExtension` (string) -- `StringIPAddress` (string) -- `StringHtmlTag` (string) -- `StringPassword` (string) -- `StringGuid` (string) - -### Number Generators - -- `NumberPositive` (decimal) -- `NumberNegative` (decimal) -- `NumberMoney` (decimal) -- `NumberFraction` (decimal) - -### DateTime Generators - -- `DateTimeFuture` (DateTime) -- `DateTimeOffsetFuture` (DateTimeOffset) -- `DateTimeOfBirth` (DateTime) -- `DateTimeOffsetOfBirth` (DateTimeOffset) -- `DateTimePast` (DateTime) -- `DateTimeOffsetPast` (DateTimeOffset) -- `CurrentDate` (DateTime) -- `DateSpecialEvent` (DateTime) -- `DateWeekday` (DateTime) -- `DateWeekend` (DateTime) -- `DateLeapYear` (DateTime) -- `DateCustomFormat` (DateTime) ## Community From 71a50ab6667fa57ec8755a9c98e5ad2a59ff941c Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Tue, 21 May 2024 10:39:17 -0400 Subject: [PATCH 6/8] save --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 0d768c7..57d9c71 100644 --- a/README.md +++ b/README.md @@ -180,12 +180,6 @@ public class ExampleServiceClientTests ``` - -#### Example of a Test with Injected Services in an Async Class - - - - ## Community Feel free to submit issues and enhancement requests. Contributions are welcome! From c79f28ba9db6a7d81e518bf9b041324e50e178c7 Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Tue, 21 May 2024 17:08:18 -0400 Subject: [PATCH 7/8] remove Read me reference --- .../DapperAutoData.SystemIntegrationUtilities.csproj | 7 ------- 1 file changed, 7 deletions(-) diff --git a/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj index 8e52dbd..b30c9b9 100644 --- a/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj +++ b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj @@ -7,11 +7,4 @@ 1.0.1 - - - \ - True - - - From 45be381ff0b3e9607eea50825821bbcbeebdfc08 Mon Sep 17 00:00:00 2001 From: Willy Mariteragi Date: Tue, 21 May 2024 17:13:12 -0400 Subject: [PATCH 8/8] fix readme --- .../README.md | 204 ++++++++++++ README.md | 308 +++++++++++------- 2 files changed, 403 insertions(+), 109 deletions(-) create mode 100644 DapperAutoData.SystemIntegrationUtilities/README.md diff --git a/DapperAutoData.SystemIntegrationUtilities/README.md b/DapperAutoData.SystemIntegrationUtilities/README.md new file mode 100644 index 0000000..57d9c71 --- /dev/null +++ b/DapperAutoData.SystemIntegrationUtilities/README.md @@ -0,0 +1,204 @@ +# Dapper System Integration Testing Library + +As a fan of TDD, BDD, and anonymous testing I've made heavy use of AutoFixture, AutoMoq, and Fluent Assertions in my projects since around 2015. I've been lucky enough to work with a few other developers across these companies, and we found we kept needing to implement the same patterns and utilites every time we moved to a new project. We even lost some work and knowledge along the way. This library is an attempt to consolidate all of that knowledge into a single library that can be used across all of our projects. It's a work in progress, but I hope it can be useful to others as well. + +DapperAutoData (yes, unrelated to Dapper ORM. I was using the name first!) is a library that simplifies the process of setting up tests and generating data for your tests. It is built on top of AutoFixture, AutoMoq, and Fluent Assertions, and provides a set of attributes that you can use to automatically generate data for your tests. + +It sets up patterns for creating data generators, customizing data generation, and testing for exceptions. It also provides a set of built-in data generators that you can use to generate specific types of data for your tests. You can also create your own data generators to generate custom data for your tests. + +To be clear, I in no way intend for this to be seen as novel work or take credit for anything done by Mark Seemann, or any of the other developers who have worked on these projects. This is simply a consolidation of the work they have done, and a way to make it easier to use across all of our projects, with the hope that it might provide some value to others. If any of the authors of the libraries used here have ANY concerns with this use, please reach out to me and I'll do my best to rectify it, or even take this repo down. + +## Features + +- **AutoFixture** to generate anonymous data for your tests. +- **AutoMoq** for automocking in your unit tests. +- **Fluent Assertions** for more readable assertions. + +## Useful Links + +- [AutoFixture Documentation](https://github.com/AutoFixture/AutoFixture/wiki) +- [Fluent Assertions Documentation](https://fluentassertions.com/documentation) +- [Faker.Net Documentation](https://github.com/bchavez/Bogus) +- [AutoMoq Documentation](https://github.com/Moq/moq4/wiki/Quickstart) + +## Installation + +In package manager console run the command: `Install-Package DapperAutoData.SystemIntegrationUtilities` + +The library is distributed as a nuget package. After installing the package all you need to do is add the `DapperServiceClientBase` as base class to you client Implementation. This base class wrapper for http client and include TokenProvider if available. + + +## How to Use + +### ExampleService + +``` +public interface IExampleService +{ + Task> CreateExampleAsync(CreateExampleModel model, CancellationToken token); +} + +public class ExampleServiceClient : DapperServiceClientBase, IExampleService +{ + public ExampleServiceClient(HttpClient httpclient, JsonSerializerOptions jsonSerializerOptions, TokenProvider tokenProvider) : base(httpclient, jsonSerializerOptions, tokenProvider) + { + } + + public async Task> ChangeNameAsync(string id, ChangeExampleNameModel model, CancellationToken token) + { + var endpoint = $"api/Example/{id}/ChangeName"; + var response = await this.Put>(endpoint, model, null, token).ConfigureAwait(false); + + return response; + } +} + +``` + +### Creating a Client Generator and TokenProvider + +Next, you need to create a Client Generator. This is a class that will generate a client for you. +Here is also an example on how to pregenerate a token for the token provider and include the token provider in the client generator. + +``` +public class ClientServiceGenerators : IDataGenerator +{ + private static string _baseUri = "https://api.example.com"; + + private TokenProvider _tokenProvider; + + public void RegisterGenerators(IFixture fixture) + { + RegisterJsonSerializer(fixture); + + var preGeneratedTokenProvider = new PreGeneratedTokenProvider(fixture); + _tokenProvider = preGeneratedTokenProvider.tokenProvider; + + RegisterExampleService(fixture); + } + + private void RegisterJsonSerializer(IFixture fixture) + { + fixture.Register(() => + { + + JsonSerializerOptions jsonSerializerOptions = new() + { + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + + return jsonSerializerOptions; + }); + } + private void RegisterExampleService(IFixture fixture) + { + fixture.Register(() => + { + var httpClient = new HttpClient + { + BaseAddress = new Uri(_baseUri) + }; + + return new ExampleServiceClient(httpClient, fixture.Create(), _tokenProvider); + }); + } + +} +``` + + +``` +public class PreGeneratedTokenProvider +{ + internal IAuthenticationServiceClient client; + public TokenProvider? tokenProvider; + internal IFixture fixture; + public PreGeneratedTokenProvider(IFixture fixture) + { + tokenProvider = TokenProviderTrackerCollection.GeTokenProviderSync(fixture); + } +} + +public class TokenProviderTrackerCollection : IDisposable +{ + private static TokenProvider? tokenProvider = null; + private static IAuthenticationServiceClient? client; + public void Dispose() + { + tokenProvider = null; + client = null; + } + + public static TokenProvider GeTokenProviderSync(IFixture fixture) + { + if (client == null) + { + client = fixture.Create(); + } + + if (tokenProvider == null) + { + var authenticationResponse = client.AuthenticateSync(); + tokenProvider = new TokenProvider + { + AccessToken = authenticationResponse.AccessToken + }; + } + return tokenProvider; + } +} + +``` + + +#### Example of System integration test + +``` +public class ExampleServiceClientTests +{ + + [Theory] + [DapperAutoData()] + public async Task CreateExampleAsync_ValidData_Success( + CreateExampleModel request, + IExampleService client) + { + // Arrange + + + // Act + var response = await client.CreateExampleAsync(request, CancellationToken.None).ConfigureAwait(true); + + // Assert + response.Data.Name.Should().Be(request.Name); + response.Data.UserId.Should().Be(request.UserId); + } +} + +``` + +## Community + +Feel free to submit issues and enhancement requests. Contributions are welcome! + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/myNewFeature`) +3. Commit your changes (`git commit -m 'Add some feature'`) +4. Push to the branch (`git push origin feature/myNewFeature`) +5. Create a new Pull Request + +## Acknowledgements + +We want to express our gratitude to all the projects that made this library possible. + +- [AutoFixture](https://github.com/AutoFixture/AutoFixture) +- [Fluent Assertions](https://github.com/fluentassertions/fluentassertions) +- [Faker.Net](https://github.com/bchavez/Bogus) +- [AutoMoq](https://github.com/Moq/moq4/wiki/Quickstart) + +## License + +Dapper Testing Library is released under the MIT License. See the bundled `LICENSE` file for details. \ No newline at end of file diff --git a/README.md b/README.md index 57d9c71..e38028a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dapper System Integration Testing Library +# Dapper Testing Library As a fan of TDD, BDD, and anonymous testing I've made heavy use of AutoFixture, AutoMoq, and Fluent Assertions in my projects since around 2015. I've been lucky enough to work with a few other developers across these companies, and we found we kept needing to implement the same patterns and utilites every time we moved to a new project. We even lost some work and knowledge along the way. This library is an attempt to consolidate all of that knowledge into a single library that can be used across all of our projects. It's a work in progress, but I hope it can be useful to others as well. @@ -13,6 +13,9 @@ To be clear, I in no way intend for this to be seen as novel work or take credit - **AutoFixture** to generate anonymous data for your tests. - **AutoMoq** for automocking in your unit tests. - **Fluent Assertions** for more readable assertions. +- **Moq** for manual mocking. +- **Custom Data Generators** for generating specific types of data. +- **Bosus** for generating fake data. ## Useful Links @@ -23,162 +26,249 @@ To be clear, I in no way intend for this to be seen as novel work or take credit ## Installation -In package manager console run the command: `Install-Package DapperAutoData.SystemIntegrationUtilities` +In package manager console run the command: `Install-Package DapperAutoData` -The library is distributed as a nuget package. After installing the package all you need to do is add the `DapperServiceClientBase` as base class to you client Implementation. This base class wrapper for http client and include TokenProvider if available. +The library is distributed as a nuget package. After installing the package all you need to do is add the `DapperAutoData` attribute to your `Theory` based test methods. Behavior can be modified by adding implementations of `IDataGenerator` and `IDapperProjectCustomization` to your test project. They will be automatically picked up by the library. +The DataGeneratorInstaller will scan for, and use any implementation of IDataGenerator in the current assembly. ## How to Use -### ExampleService +### Behavior Customization +Autofixture can normally be customized by calling passing in an ICustimization to the fixture. The DapperAutoData will attribute will automatically apply any customizations that implement the IDapperProjectCustomization interface, which is a wrapper for ICustomization. ``` -public interface IExampleService +public class DapperDataCustomizations : IDapperProjectCustomization { - Task> CreateExampleAsync(CreateExampleModel model, CancellationToken token); -} - -public class ExampleServiceClient : DapperServiceClientBase, IExampleService -{ - public ExampleServiceClient(HttpClient httpclient, JsonSerializerOptions jsonSerializerOptions, TokenProvider tokenProvider) : base(httpclient, jsonSerializerOptions, tokenProvider) + public void Customize(IFixture fixture) { + fixture.RepeatCount = 3; + fixture.Behaviors.Remove(new ThrowingRecursionBehavior()); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); } +} +``` - public async Task> ChangeNameAsync(string id, ChangeExampleNameModel model, CancellationToken token) - { - var endpoint = $"api/Example/{id}/ChangeName"; - var response = await this.Put>(endpoint, model, null, token).ConfigureAwait(false); +### Creating a Data Generator - return response; - } -} +A major reason for using the Dapper Testing Library is to create custom data generators. As a project grows, it will build up a library of generators that can be used to create complex objects for testing. Each generator can be self-contained and updated individually, which allows for a team to focus only on the test cases they are working on, knowing that any data the test needs will be provided by the library in a valid state. Developers should take on the responsibility for creating and updating generators for the objects they are working with. They will be automatically picked up for use in the tests. -``` +Generally, we follow one of two patterns for creating data generators. Initially the pattern was to to create a generator that explicitly defines EVERYTHING about an object. In practice, we've found this to be a bit cumbersome. -### Creating a Client Generator and TokenProvider +So we have moved to a second pattern, where we create a generator that defines the minimum required fields that need to be customized. This makes it easier to refactor classes when there aren't a ton of customized fields. -Next, you need to create a Client Generator. This is a class that will generate a client for you. -Here is also an example on how to pregenerate a token for the token provider and include the token provider in the client generator. +Here is an example of a generator that defines the minimum required fields, and then allows the developer to customize the rest of the object as needed. This is the preferred pattern where possible: ``` -public class ClientServiceGenerators : IDataGenerator +public class ExampleInfoGenerator : IDataGenerator { - private static string _baseUri = "https://api.example.com"; - - private TokenProvider _tokenProvider; - public void RegisterGenerators(IFixture fixture) { - RegisterJsonSerializer(fixture); - - var preGeneratedTokenProvider = new PreGeneratedTokenProvider(fixture); - _tokenProvider = preGeneratedTokenProvider.tokenProvider; - - RegisterExampleService(fixture); + fixture.Customize(c => c + .With(x => x.UserId, fixture.Create().Value.ToString) + .With(x => x.Name, fixture.Create()) + ); } +} +``` + +And here is an example of a generator that defines everything about an object. This can be a bit cumbersome for larger data objects, like POCOs and DTOs. Note that this is still a valuable pattern when defining more complex objects. For example, if a related object needs to be generated first and the data linked, or when there are dependencies between properties. - private void RegisterJsonSerializer(IFixture fixture) +``` +// Notice that in this example we have to define Date and AccountBalance, even though we are not meaningfully customizing them. +// However, for this particular object the UserId must always be prefixed with the AccountType.Code. +// So despite the extra overhead, in this example it is still the best pattern to use, as it ensures that the UserId is always correct. +public class ExampleInfoGenerator +{ + public ExampleInfo GenerateExampleInfo() { - fixture.Register(() => - { - - JsonSerializerOptions jsonSerializerOptions = new() - { - IgnoreReadOnlyFields = true, - IgnoreReadOnlyProperties = true, - PropertyNameCaseInsensitive = true, - WriteIndented = true - }; - - return jsonSerializerOptions; - }); - } - private void RegisterExampleService(IFixture fixture) - { - fixture.Register(() => + var accountType = fixture.Create(); + fixture.Register(() => new ExampleInfo { - var httpClient = new HttpClient - { - BaseAddress = new Uri(_baseUri) - }; - - return new ExampleServiceClient(httpClient, fixture.Create(), _tokenProvider); + UserId = $"{accountType.Code}{fixture.Create().Value.ToString()}, + Name = fixture.Create(), + Date = fixture.Create(), + AccountBalance = fixture.Create(), + AccountType = accountType }); - } - + } } ``` +### Using Dapper Testing Library in your Tests + +The Dapper Testing Library is designed to be easy to use. It is effectively a wrapper around AutoFixture, AutoMoq, and Fluent Assertions that simplifies the process of setting up tests and generating data. You can start using it in your tests by adding the `DapperAutoData` attribute to your test methods. This will automatically generate data for your tests using AutoFixture and AutoMoq. + +Keep in mind that the `DapperAutoData` attribute is designed to work with `Theory` based tests. You can learn more about how to set up + +You can read more about how to use AutoFixture [here](https://autofixture.github.io/docs/quick-start/). + +Here are some examples of tests using the Dapper Testing Library: + +#### Example of a Unit Test ``` -public class PreGeneratedTokenProvider +/// This test will create an account with the minimum required fields and assert that no validation errors are returned. +[Theory] +[DapperAutoData] +public void Create_MinimumRequiredFields_NoValidationErrors(CreateAccountRequest request) { - internal IAuthenticationServiceClient client; - public TokenProvider? tokenProvider; - internal IFixture fixture; - public PreGeneratedTokenProvider(IFixture fixture) - { - tokenProvider = TokenProviderTrackerCollection.GeTokenProviderSync(fixture); - } + // Arrange + + // Act + var account = new AccountEntity(request); + + // Assert + account.Validate().Items.Should().BeNullOrEmpty(); + account.LoginEmail.Should().Be(request.LoginEmail); + account.Address.Should().Be(request.Address); + account.ContactInfo.Should().Be(request.ContactInfo); } +``` -public class TokenProviderTrackerCollection : IDisposable +#### Example of a Theory Driven Test + +``` +/// This test will create an account with a missing company name and assert that the address is successfully changed. +[Theory] +[DapperAutoData("")] +[DapperAutoData(" ")] +public void ChangeAddress_MissingCompany_ChangeSuccessful(StringCompanyName companyName, AccountEntity account, Address address) { - private static TokenProvider? tokenProvider = null; - private static IAuthenticationServiceClient? client; - public void Dispose() - { - tokenProvider = null; - client = null; - } - - public static TokenProvider GeTokenProviderSync(IFixture fixture) - { - if (client == null) - { - client = fixture.Create(); - } - - if (tokenProvider == null) - { - var authenticationResponse = client.AuthenticateSync(); - tokenProvider = new TokenProvider - { - AccessToken = authenticationResponse.AccessToken - }; - } - return tokenProvider; - } + // Arrange + address.CompanyName = companyName; // address.CompanyName is a string field. Notice that StringCompanyName can be used in place of a string type. + + // Act + var result = account.ChangeAddress(address); + + // Assert + result.Items.Should().BeNullOrEmpty(); + account.Address.Should().Be(address); } +``` + +#### Example of a Test with Injected Services in an Async Class ``` +/// This test demonstrates how you would test an class that uses injected interfaces. +/// The [Frozen] attribute marks the interface as a singleton, so it will be the same instance for all tests. +/// The AccountService class may have many injected services, but they will all be automatically mocked by AutoMoq. +/// This test will only need to deal with the specific interface that effects this test. +[Theory] +[DapperAutoData] +public async Task CreateAsync_CreateDuplicateAccount_ReturnsDuplicateAccountErrorAsync( CreateAccountRequest request, + [Frozen] Mock repo, + AccountService service ) +{ + // Arrange + repo.Setup(x => x.DoesEmailExistAsync(request.LoginEmail, It.IsAny())) + .ReturnsAsync(true); // To test this properly we need to ensure the repo reports that the email already exists. + + // Act + var response = await service.CreateAsync(request, CancellationToken.None); + // Assert + response.ErrorCode.Should().Be(ErrorCode.Duplicate); + response.IsSuccess.Should().BeFalse(); + response.ErrorMessage.Should().Contain(BrokerResponse.DuplicateEmailError); -#### Example of System integration test + repo.Verify(x => x.CreateAsync(It.IsAny(), It.IsAny()), Times.Never); +} +``` +#### Testing for synchronous Exceptions ``` -public class ExampleServiceClientTests +/// This test shows how you can test for exceptions in synchronus methods. +[Theory] +[DapperAutoData] +public void Create_InvalidAccount_ThrowsException(CreateAccountRequest request) { + // Arrange + var service = new AccountService(); - [Theory] - [DapperAutoData()] - public async Task CreateExampleAsync_ValidData_Success( - CreateExampleModel request, - IExampleService client) - { - // Arrange + // Act + Action act = () => service.Create(request); + // Assert + act.Should().Throw(); +} +``` - // Act - var response = await client.CreateExampleAsync(request, CancellationToken.None).ConfigureAwait(true); +This will assert that calling the `Create` method with the provided `request` will throw an `InvalidAccountException`. - // Assert - response.Data.Name.Should().Be(request.Name); - response.Data.UserId.Should().Be(request.UserId); - } -} +#### Testing for asynchronous Exceptions ``` +/// This test shows how you can test for exceptions in asynchronous methods. +[Theory] +[DapperAutoData] +public async Task CreateAsync_InvalidAccount_ThrowsExceptionAsync(CreateAccountRequest request) +{ + // Arrange + var service = new AccountService(); + + // Act + Func act = async () => await service.CreateAsync(request, CancellationToken.None); + + // Assert + await act.Should().ThrowAsync(); +} +``` + +## Existing Generator Types + +The Dapper Testing Library has a variety of built-in types that can be used to specify ranges and types of auto data. These can generally be used interchangeably with the types they are generating for, or you can grab the specific value from the `.Value` attribute. + +### String Generators + +- `StringParagraph` (string) +- `StringPersonFullName` (string) +- `StringPhoneNumber` (string) +- `StringSentence` (string) +- `StringSsn` (string) +- `StringWord` (string) +- `StringCompanyName` (string) +- `StringEmailTest` (string) +- `StringFirstName` (string) +- `StringInternetUrl` (string) +- `StringInternetUsername` (string) +- `StringCityName` (string) +- `StringCountryName` (string) +- `StringPostalCode` (string) +- `StringStateAbbreviation` (string) +- `StringStateFullName` (string) +- `StringStreetAddress` (string) +- `StringJobTitle` (string) +- `StringProductCategory` (string) +- `StringProductDescription` (string) +- `StringCurrency` (string) +- `StringFileExtension` (string) +- `StringIPAddress` (string) +- `StringHtmlTag` (string) +- `StringPassword` (string) +- `StringGuid` (string) + +### Number Generators + +- `NumberPositive` (decimal) +- `NumberNegative` (decimal) +- `NumberMoney` (decimal) +- `NumberFraction` (decimal) + +### DateTime Generators + +- `DateTimeFuture` (DateTime) +- `DateTimeOffsetFuture` (DateTimeOffset) +- `DateTimeOfBirth` (DateTime) +- `DateTimeOffsetOfBirth` (DateTimeOffset) +- `DateTimePast` (DateTime) +- `DateTimeOffsetPast` (DateTimeOffset) +- `CurrentDate` (DateTime) +- `DateSpecialEvent` (DateTime) +- `DateWeekday` (DateTime) +- `DateWeekend` (DateTime) +- `DateLeapYear` (DateTime) +- `DateCustomFormat` (DateTime) ## Community