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.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj new file mode 100644 index 0000000..b30c9b9 --- /dev/null +++ b/DapperAutoData.SystemIntegrationUtilities/DapperAutoData.SystemIntegrationUtilities.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + enable + enable + 1.0.1 + + + diff --git a/DapperAutoData.SystemIntegrationUtilities/DapperServiceClientBase.cs b/DapperAutoData.SystemIntegrationUtilities/DapperServiceClientBase.cs new file mode 100644 index 0000000..a4b53a1 --- /dev/null +++ b/DapperAutoData.SystemIntegrationUtilities/DapperServiceClientBase.cs @@ -0,0 +1,221 @@ +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; +using System.Text; + +namespace DapperAutoData.SystemIntegrationUtilities; + +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.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/DapperAutoData.SystemIntegrationUtilities/TokenProvider.cs b/DapperAutoData.SystemIntegrationUtilities/TokenProvider.cs new file mode 100644 index 0000000..7f1cd53 --- /dev/null +++ b/DapperAutoData.SystemIntegrationUtilities/TokenProvider.cs @@ -0,0 +1,8 @@ +namespace DapperAutoData.SystemIntegrationUtilities; + +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..14ae498 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.SystemIntegrationUtilities", "DapperAutoData.SystemIntegrationUtilities\DapperAutoData.SystemIntegrationUtilities.csproj", "{6A360AC6-E36B-4F44-BF0A-5FC9D132197C}" 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 + {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