├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Common.targets ├── LICENSE ├── README.md ├── SECURITY.md ├── UnitTestEx.sln ├── images └── Logo256x256.png ├── nuget-publish.ps1 ├── src ├── UnitTestEx.Azure.Functions │ ├── Azure │ │ └── Functions │ │ │ ├── FunctionTesterBase.cs │ │ │ ├── FunctionTesterT.cs │ │ │ ├── HttpTriggerTester.cs │ │ │ ├── RouteCheckOption.cs │ │ │ ├── ServiceBusMessageActionStatus.cs │ │ │ ├── ServiceBusTriggerTester.cs │ │ │ ├── TestSetUp.cs │ │ │ ├── WebJobsServiceBusMessageActionsAssertor.cs │ │ │ ├── WebJobsServiceBusSessionMessageActionsAssertor.cs │ │ │ └── WorkerServiceBusMessageActionsAssertor.cs │ ├── ExtensionMethods.cs │ ├── FunctionTester.cs │ ├── UnitTestEx.Azure.Functions.csproj │ └── strong-name-key.snk ├── UnitTestEx.Azure.ServiceBus │ ├── ExtensionMethods.cs │ ├── UnitTestEx.Azure.ServiceBus.csproj │ └── strong-name-key.snk ├── UnitTestEx.MSTest │ ├── Internal │ │ ├── MSTestImplementor.cs │ │ └── OneOffTestSetUp.cs │ ├── UnitTestEx.MSTest.csproj │ ├── WithApiTester.cs │ └── strong-name-key.snk ├── UnitTestEx.NUnit │ ├── Internal │ │ ├── NUnitTestImplementor.cs │ │ └── OneOffTestSetUp.cs │ ├── UnitTestEx.NUnit.csproj │ ├── WithApiTester.cs │ └── strong-name-key.snk ├── UnitTestEx.Xunit │ ├── ApiTestFixture.cs │ ├── Internal │ │ ├── XunitLocalTestImplementor.cs │ │ └── XunitTestImplementor.cs │ ├── UnitTestBase.cs │ ├── UnitTestEx.Xunit.csproj │ ├── WithApiTester.cs │ └── strong-name-key.snk └── UnitTestEx │ ├── Abstractions │ ├── OneOffTestSetUpAttribute.cs │ ├── OneOffTestSetUpBase.cs │ ├── TestFrameworkImplementor.cs │ ├── TestSetUpException.cs │ ├── TestSharedState.cs │ ├── TesterBase.cs │ ├── TesterBaseT.cs │ └── TesterExtensionsConfig.cs │ ├── ApiError.cs │ ├── ApiTester.cs │ ├── AspNetCore │ ├── ApiTesterBase.cs │ ├── ApiTesterT.cs │ ├── BypassAuthorizationHandler.cs │ ├── ControllerTester.cs │ ├── HttpTester.cs │ ├── HttpTesterBase.cs │ ├── HttpTesterBaseT.cs │ ├── HttpTesterBaseT2.cs │ └── HttpTesterT.cs │ ├── Assertors │ ├── ActionResultAssertor.cs │ ├── Assertor.cs │ ├── AssertorBase.cs │ ├── AssertorBaseT.cs │ ├── HttpResponseMessageAssertor.cs │ ├── HttpResponseMessageAssertorBase.cs │ ├── HttpResponseMessageAssertorBaseT.cs │ ├── HttpResponseMessageAssertorT.cs │ ├── ValueAssertor.cs │ └── VoidAssertor.cs │ ├── Expectations │ ├── AssertArgs.cs │ ├── ErrorExpectations.cs │ ├── ExceptionExpectations.cs │ ├── ExpectationsArranger.cs │ ├── ExpectationsBase.cs │ ├── ExpectationsBaseT.cs │ ├── ExpectationsExtensions.cs │ ├── HttpResponseMessageExpectations.cs │ ├── IExpectations.cs │ ├── IHttpResponseMessageExpectations.cs │ ├── IValueExpectations.cs │ ├── LoggerExpectations.cs │ └── ValueExpectations.cs │ ├── ExtensionMethods.cs │ ├── Generic │ ├── GenericTesterBase.cs │ ├── GenericTesterBaseT.cs │ ├── GenericTesterCore.cs │ ├── GenericTesterT.cs │ └── GenericTesterT2.cs │ ├── GenericTester.cs │ ├── Hosting │ ├── EntryPoint.cs │ ├── HostTesterBase.cs │ └── TypeTester.cs │ ├── Json │ ├── IJsonSerializer.cs │ ├── JsonElementComparer.cs │ ├── JsonElementComparerOptions.cs │ ├── JsonElementComparerResult.cs │ ├── JsonElementComparison.cs │ ├── JsonElementDifference.cs │ ├── JsonElementDifferenceType.cs │ ├── JsonExtensions.cs │ ├── JsonSerializer.cs │ └── JsonWriteFormat.cs │ ├── Logging │ ├── LoggerBase.cs │ ├── SharedStateLogger.cs │ └── TestFrameworkImplementerLogger.cs │ ├── MockHttpClientException.cs │ ├── MockHttpClientFactory.cs │ ├── Mocking │ ├── MockHttpClient.cs │ ├── MockHttpClientFactory.cs │ ├── MockHttpClientHandler.cs │ ├── MockHttpClientRequest.cs │ ├── MockHttpClientRequestBody.cs │ ├── MockHttpClientRequestRule.cs │ ├── MockHttpClientResponse.cs │ └── MockHttpClientResponseSequence.cs │ ├── ObjectComparer.cs │ ├── Resource.cs │ ├── Schema │ └── mock.unittestex.json │ ├── SystemExtensionMethods.cs │ ├── TestSetUp.cs │ ├── UnitTestEx.csproj │ └── strong-name-key.snk └── tests ├── UnitTestEx.Api ├── Controllers │ ├── PersonController.cs │ ├── ProductController.cs │ └── TestController.cs ├── Models │ └── Person.cs ├── Program.cs ├── Startup.cs ├── UnitTestEx.Api.csproj ├── appsettings.Development.json └── appsettings.json ├── UnitTestEx.Function ├── .gitignore ├── PersonFunction.cs ├── ProductFunction.cs ├── Properties │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── ServiceBusFunction.cs ├── Startup.cs ├── UnitTestEx.Function.csproj └── host.json ├── UnitTestEx.IsolatedFunction ├── .gitignore ├── HttpFunction.cs ├── Program.cs ├── Properties │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── ServiceBusFunction.cs ├── Startup.cs ├── TimerFunction.cs ├── UnitTestEx.IsolatedFunction.csproj └── host.json ├── UnitTestEx.MSTest.Test ├── ExpressionTest.cs ├── MockHttpClientTest.cs ├── Model │ ├── Person.cs │ └── Person2.cs ├── NewtonsoftJsonSerializer.cs ├── Other │ ├── GenericTest.cs │ ├── JsonElementComparerTest.cs │ ├── LoggerTest.cs │ └── ObjectComparerTest.cs ├── PersonControllerTest.cs ├── PersonFunctionTest.cs ├── ProductControllerTest.cs ├── ProductFunctionTest.cs ├── Resources │ ├── FunctionTest-ValidJsonResource.json │ └── MockHttpClientTest-UriAndBody_WithJsonResponse3.json ├── ServiceBusFunctionTest.cs ├── UnitTestEx.MSTest.Test.csproj └── appsettings.unittest.json ├── UnitTestEx.NUnit.Test ├── IsolatedFunctionTest.cs ├── MockHttpClientTest.cs ├── Model │ ├── Person.cs │ └── Person2.cs ├── Other │ ├── ExpectationsTest.cs │ ├── GenericTest.cs │ ├── LoggerTest.cs │ ├── ObjectComparerTest.cs │ └── OneOffTestSetUpTest.cs ├── PersonControllerTest.cs ├── PersonFunctionTest.cs ├── ProductControllerTest.cs ├── ProductFunctionTest.cs ├── Resources │ ├── FunctionTest-ValidJsonResource.json │ ├── MockHttpClientTest-UriAndBody_WithJsonResponse3.json │ ├── mock.unittestex.yaml │ └── sequence.unittestex.yaml ├── ServiceBusFunctionTest.cs ├── UnitTestEx.NUnit.Test.csproj └── appsettings.unittest.json └── UnitTestEx.Xunit.Test ├── MockHttpClientTest.cs ├── Model ├── Person.cs └── Person2.cs ├── Other ├── GenericTest.cs ├── LoggerTest.cs └── ObjectComparerTest.cs ├── PersonControllerTest.cs ├── PersonFunctionTest.cs ├── ProductApiTestFixture.cs ├── ProductControllerTest.cs ├── ProductFunctionTest.cs ├── Resources ├── FunctionTest-ValidJsonResource.json └── MockHttpClientTest-UriAndBody_WithJsonResponse3.json ├── ServiceBusFunctionTest.cs ├── UnitTestEx.Xunit.Test.csproj └── appsettings.unittest.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: | 20 | 6.0.x 21 | 7.0.x 22 | 8.0.x 23 | 9.0.x 24 | 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | 28 | - name: Build 29 | run: dotnet build --no-restore 30 | 31 | - name: Explicit MSTest test 32 | run: | 33 | cp tests/UnitTestEx.Api/bin/Debug/net6.0/UnitTestEx.Api.deps.json tests/UnitTestEx.MSTest.Test/bin/Debug/net6.0 34 | cd tests/UnitTestEx.MSTest.Test/bin/Debug/net6.0 35 | dotnet test UnitTestEx.MSTest.Test.dll --no-build --verbosity normal 36 | 37 | - name: Explicit NUnit test 38 | run: | 39 | cp tests/UnitTestEx.Api/bin/Debug/net8.0/UnitTestEx.Api.deps.json tests/UnitTestEx.NUnit.Test/bin/Debug/net8.0 40 | cd tests/UnitTestEx.NUnit.Test/bin/Debug/net8.0 41 | dotnet test UnitTestEx.NUnit.Test.dll --no-build --verbosity normal 42 | 43 | - name: Explicit Xunit test 44 | run: | 45 | cp tests/UnitTestEx.Api/bin/Debug/net8.0/UnitTestEx.Api.deps.json tests/UnitTestEx.Xunit.Test/bin/Debug/net8.0 46 | cd tests/UnitTestEx.Xunit.Test/bin/Debug/net8.0 47 | dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests MockHttpClientTest 48 | dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests PersonFunctionTest 49 | dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests ProductFunctionTest 50 | dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests ServiceBusFunctionTest 51 | dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests PersonControllerTest 52 | dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests ProductControllerTest -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://avanade.github.io/code-of-conduct/) 2 | 3 | This project adopts the [Contributor Covenant Code of Conduct](https://avanade.github.io/code-of-conduct/). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | This project welcomes contributions and suggestions. By contributing, you confirm that you have the right to, and actually do, grant us the rights to use your contribution. More information below. 4 | 5 | Please feel free to contribute code, ideas, improvements, and patches - we've added some general guidelines and information below, and you can propose changes to this document in a pull request. 6 | 7 | This project has adopted the [Contributor Covenant Code of Conduct](https://avanade.github.io/code-of-conduct/). 8 | 9 |
10 | 11 | ## Rights to your contributions 12 | By contributing to this project, you: 13 | - Agree that you have authored 100% of the content; 14 | - Agree that you have the necessary rights to the content; 15 | - Agree that you have received the necessary permissions from your employer to make the contributions (if applicable); 16 | - Agree that the content you contribute may be provided under the Project license(s); 17 | - Agree that, if you did not author 100% of the content, the appropriate licenses and copyrights have been added along with any other necessary attribution. 18 | 19 |
20 | 21 | ## Code of Conduct 22 | This project, and people participating in it, are governed by our [code of conduct](https://avanade.github.io/code-of-conduct/). By taking part, we expect you to try your best to uphold this code of conduct. If you have concerns about unacceptable behaviour, please contact the community leaders responsible for enforcement at 23 | [ospo@avanade.com](ospo@avanade.com). 24 | 25 |
26 | 27 | ## Coding guidelines 28 | 29 | The most general guideline is that we use all the VS default settings in terms of code formatting; if in doubt, follow the coding convention of the existing code base. 30 | 1. Use four spaces of indentation (no tabs). 31 | 2. Use `_camelCase` for private fields. 32 | 3. Avoid `this.` unless absolutely necessary. 33 | 4. Always specify member visibility, even if it's the default (i.e. `private string _foo;` not `string _foo;`). 34 | 5. Open-braces (`{`) go on a new line (an `if` with single-line statement does not need braces). 35 | 6. Use any language features available to you (expression-bodied members, throw expressions, tuples, etc.) as long as they make for readable, manageable code. 36 | 7. All methods and properties must include the [XML documentation comments](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/xml-documentation-comments). Private methods and properties only need to specifiy the [summary](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/summary) as a minimum. 37 | 8. Breaking changes should be avoided where possible; however, to minimize lifetime costs they are acceptable (also avoid alternatives unless additive to minimize codebase bloat). For the most part be guided by [Semantic Versioning 2.0.0](https://semver.org/). 38 | 39 | For further guidance see ASP.NET Core [Engineering guidelines](https://github.com/aspnet/AspNetCore/wiki/Engineering-guidelines). 40 | 41 |
42 | 43 | ## Tests 44 | 45 | We use [`NUnit`](https://github.com/nunit/nunit) for all unit testing. 46 | 47 | - Tests need to be provided for every bug/feature that is completed. 48 | - Tests only need to be present for issues that need to be verified by QA (for example, not tasks). 49 | - If there is a scenario that is far too hard to test there does not need to be a test for it. 50 | - "Too hard" is determined by the team as a whole. 51 | 52 | We understand there is more work to be performed in generating a higher level of code coverage; this technical debt is on the backlog. 53 | 54 |
55 | 56 | ## Code reviews and checkins 57 | 58 | To help ensure that only the highest quality code makes its way into the project, please submit all your code changes to GitHub as PRs. This includes runtime code changes, unit test updates, and updates to the end-to-end demo. 59 | 60 | For example, sending a PR for just an update to a unit test might seem like a waste of time but the unit tests are just as important as the product code and as such, reviewing changes to them is also just as important. This also helps create visibility for your changes so that others can observe what is going on. 61 | 62 | The advantages are numerous: improving code quality, more visibility on changes and their potential impact, avoiding duplication of effort, and creating general awareness of progress being made in various areas. -------------------------------------------------------------------------------- /Common.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 5.5.0 4 | preview 5 | Avanade 6 | Avanade 7 | Avanade (c) 8 | https://github.com/Avanade/UnitTestEx 9 | https://github.com/Avanade/UnitTestEx 10 | true 11 | false 12 | strong-name-key.snk 13 | git 14 | false 15 | https://github.com/Avanade/UnitTestEx/raw/main/images/Logo256x256.png 16 | Logo256x256.png 17 | true 18 | MIT 19 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 20 | true 21 | enable 22 | true 23 | true 24 | snupkg 25 | true 26 | true 27 | true 28 | true 29 | true 30 | false 31 | 32 | 33 | 34 | 35 | \ 36 | true 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Avanade Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Avanade responsible disclosure 2 | 3 | Avanade operates a 🔐 [Responsible disclosure policy](https://www.avanade.com/en/about-avanade/approach/trust-center/responsible-disclosure). -------------------------------------------------------------------------------- /images/Logo256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avanade/UnitTestEx/d80ff2987e1f9aab282aa1c80638ba6015fbff0a/images/Logo256x256.png -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.Functions/Azure/Functions/FunctionTesterT.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System.Collections.Generic; 4 | using UnitTestEx.Abstractions; 5 | 6 | namespace UnitTestEx.Azure.Functions 7 | { 8 | /// 9 | /// Provides the NUnit implementation. 10 | /// 11 | /// The Function startup . 12 | /// Indicates whether to include 'appsettings.unittest.json' configuration file. 13 | /// Indicates whether to include user secrets. 14 | /// Additional configuration values to add/override. 15 | public class FunctionTester(bool? includeUnitTestConfiguration, bool? includeUserSecrets, IEnumerable>? additionalConfiguration) 16 | : FunctionTesterBase>(TestFrameworkImplementor.Create(), includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration) where TEntryPoint : class, new() 17 | { } 18 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.Functions/Azure/Functions/RouteCheckOption.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTestEx.Azure.Functions 2 | { 3 | /// 4 | /// Represents the route check option for the . 5 | /// 6 | public enum RouteCheckOption 7 | { 8 | /// 9 | /// No route check is required, 10 | /// 11 | None, 12 | 13 | /// 14 | /// The route should match the specified path excluding any query string. 15 | /// 16 | Path, 17 | 18 | /// 19 | /// The route should match the specified path including the query string. 20 | /// 21 | PathAndQuery, 22 | 23 | /// 24 | /// The route should start with the specified path and query string. 25 | /// 26 | PathAndQueryStartsWith, 27 | 28 | /// 29 | /// The route query (ignore path) should match the specified query string. 30 | /// 31 | Query, 32 | 33 | /// 34 | /// The route query (ignore path) should start with the specified query string. 35 | /// 36 | QueryStartsWith 37 | } 38 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.Functions/Azure/Functions/ServiceBusMessageActionStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.Azure.WebJobs.ServiceBus; 4 | 5 | namespace UnitTestEx.Azure.Functions 6 | { 7 | /// 8 | /// Represents the status. 9 | /// 10 | public enum ServiceBusMessageActionStatus 11 | { 12 | /// 13 | /// Indicates that no action occured. 14 | /// 15 | None, 16 | 17 | /// 18 | /// Indicates that the AbandonMessageAsync was invoked. 19 | /// 20 | Abandon, 21 | 22 | /// 23 | /// Indicates that the CompleteMessageAsync was invoked. 24 | /// 25 | Complete, 26 | 27 | /// 28 | /// Indicates that the DeadLetterMessageAsync was invoked. 29 | /// 30 | DeadLetter, 31 | 32 | /// 33 | /// Indicates that the DeferMessageAsync was invoked. 34 | /// 35 | Defer, 36 | 37 | /// 38 | /// Indicates that the RenewMessageLockAsync was invoked. 39 | /// 40 | Renew 41 | } 42 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.Functions/Azure/Functions/TestSetUp.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | 4 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 5 | 6 | namespace UnitTestEx.Azure.Functions 7 | { 8 | /// 9 | /// Extends the . 10 | /// 11 | public static class TestSetUp 12 | { 13 | /// 14 | /// Indicates whether to include 'appsettings.unittest.json' configuration file when the host starts; defaults to true. 15 | /// 16 | public static bool FunctionTesterIncludeUnitTestConfiguration { get; set; } = true; 17 | 18 | /// 19 | /// Indicates whether to include user secrets configuration when the host starts; defaults to false. 20 | /// 21 | public static bool FunctionTesterIncludeUserSecrets { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.Functions/FunctionTester.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using UnitTestEx.Azure.Functions; 6 | 7 | namespace UnitTestEx 8 | { 9 | /// 10 | /// Provides the Function testing capability. 11 | /// 12 | public static class FunctionTester 13 | { 14 | /// 15 | /// Creates a new instance of the class. 16 | /// 17 | /// The Function startup . 18 | /// Indicates whether to include 'appsettings.unittest.json' configuration file. 19 | /// Indicates whether to include user secrets. 20 | /// Additional configuration values to add/override. 21 | /// The . 22 | public static FunctionTester Create(bool? includeUnitTestConfiguration = null, bool? includeUserSecrets = null, IEnumerable>? additionalConfiguration = null) 23 | where TEntryPoint : class, new() 24 | => new(includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration); 25 | } 26 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.Functions/UnitTestEx.Azure.Functions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | UnitTestEx 6 | UnitTestEx 7 | UnitTestEx Azure Functions Test Extensions. 8 | UnitTestEx Test Extensions. 9 | unittestex azure function unit test unittest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.Functions/strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avanade/UnitTestEx/d80ff2987e1f9aab282aa1c80638ba6015fbff0a/src/UnitTestEx.Azure.Functions/strong-name-key.snk -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.ServiceBus/UnitTestEx.Azure.ServiceBus.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | UnitTestEx 6 | UnitTestEx 7 | UnitTestEx Azure Functions Test Extensions. 8 | UnitTestEx Azure ServiceBus Test Extensions. 9 | unittestex azure messaging servicebus unit test unittest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/UnitTestEx.Azure.ServiceBus/strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avanade/UnitTestEx/d80ff2987e1f9aab282aa1c80638ba6015fbff0a/src/UnitTestEx.Azure.ServiceBus/strong-name-key.snk -------------------------------------------------------------------------------- /src/UnitTestEx.MSTest/Internal/MSTestImplementor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; 5 | 6 | namespace UnitTestEx.MSTest.Internal 7 | { 8 | /// 9 | /// Provides the MSTest implementation. 10 | /// 11 | public sealed class MSTestImplementor : Abstractions.TestFrameworkImplementor 12 | { 13 | private const string _notSpecifiedText = "Not specified."; 14 | 15 | /// 16 | public override void AssertFail(string? message) => Assert.Fail(message ?? _notSpecifiedText); 17 | 18 | /// 19 | public override void AssertAreEqual(T? expected, T? actual, string? message = null) where T : default => Assert.AreEqual(expected, actual, message, [expected, actual]); 20 | 21 | /// 22 | public override void AssertInconclusive(string? message) => Assert.Inconclusive(message); 23 | 24 | /// 25 | public override void WriteLine(string? message) => Logger.LogMessage("{0}", message); 26 | } 27 | } -------------------------------------------------------------------------------- /src/UnitTestEx.MSTest/Internal/OneOffTestSetUp.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using UnitTestEx.Abstractions; 4 | 5 | [assembly: OneOffTestSetUp(typeof(UnitTestEx.MSTest.Internal.OneOffTestSetUp))] 6 | 7 | namespace UnitTestEx.MSTest.Internal 8 | { 9 | /// 10 | /// Performs one-off test set up. 11 | /// 12 | public class OneOffTestSetUp : OneOffTestSetUpBase 13 | { 14 | /// 15 | public override void SetUp() => TestFrameworkImplementor.SetGlobalCreateFactory(() => new MSTestImplementor()); 16 | } 17 | } -------------------------------------------------------------------------------- /src/UnitTestEx.MSTest/UnitTestEx.MSTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | UnitTestEx.MSTest 6 | UnitTestEx MSTest 7 | UnitTestEx MSTest Test Extensions. 8 | UnitTestEx MSTest Test Extensions. 9 | unittestex api function unit test unittest mstest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/UnitTestEx.MSTest/WithApiTester.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.AspNetCore; 5 | 6 | namespace UnitTestEx 7 | { 8 | /// 9 | /// Provides a shared to enable usage of the same underlying instance across multiple tests. 10 | /// 11 | /// The API startup . 12 | /// Implements to automatically dispose of the instance to release all resources. 13 | /// Be aware that using may result in cross-test contamination. 14 | public abstract class WithApiTester : IDisposable where TEntryPoint : class 15 | { 16 | private bool _disposed; 17 | private ApiTester? _apiTester = ApiTester.Create(); 18 | 19 | /// 20 | /// Gets the shared for testing. 21 | /// 22 | public ApiTester Test => _apiTester ?? throw new ObjectDisposedException(nameof(Test)); 23 | 24 | /// 25 | /// Dispose of all resources. 26 | /// 27 | /// Indicates whether from the . 28 | protected virtual void Dispose(bool disposing) 29 | { 30 | if (!_disposed) 31 | { 32 | if (disposing) 33 | { 34 | _apiTester?.Dispose(); 35 | _apiTester = null; 36 | } 37 | 38 | _disposed = true; 39 | } 40 | } 41 | 42 | /// 43 | public void Dispose() 44 | { 45 | Dispose(disposing: true); 46 | GC.SuppressFinalize(this); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/UnitTestEx.MSTest/strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avanade/UnitTestEx/d80ff2987e1f9aab282aa1c80638ba6015fbff0a/src/UnitTestEx.MSTest/strong-name-key.snk -------------------------------------------------------------------------------- /src/UnitTestEx.NUnit/Internal/NUnitTestImplementor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using NUnit.Framework; 4 | 5 | namespace UnitTestEx.NUnit.Internal 6 | { 7 | /// 8 | /// Provides the NUnit implementation. 9 | /// 10 | public sealed class NUnitTestImplementor : Abstractions.TestFrameworkImplementor 11 | { 12 | private const string _notSpecifiedText = "Not specified."; 13 | 14 | /// 15 | public override void AssertFail(string? message) => Assert.Fail(message ?? _notSpecifiedText); 16 | 17 | /// 18 | public override void AssertAreEqual(T? expected, T? actual, string? message = null) where T : default => Assert.That(actual, Is.EqualTo(expected), message); 19 | 20 | /// 21 | public override void AssertInconclusive(string? message) => Assert.Inconclusive(message ?? _notSpecifiedText); 22 | 23 | /// 24 | public override void WriteLine(string? message) => TestContext.Out.WriteLine(message); 25 | } 26 | } -------------------------------------------------------------------------------- /src/UnitTestEx.NUnit/Internal/OneOffTestSetUp.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using UnitTestEx.Abstractions; 4 | 5 | [assembly: OneOffTestSetUp(typeof(UnitTestEx.NUnit.Internal.OneOffTestSetUp))] 6 | 7 | namespace UnitTestEx.NUnit.Internal 8 | { 9 | /// 10 | /// Performs one-off test set up. 11 | /// 12 | public class OneOffTestSetUp : OneOffTestSetUpBase 13 | { 14 | /// 15 | public override void SetUp() => TestFrameworkImplementor.SetGlobalCreateFactory(() => new NUnitTestImplementor()); 16 | } 17 | } -------------------------------------------------------------------------------- /src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | UnitTestEx.NUnit 6 | UnitTestEx NUnit 7 | UnitTestEx NUnit Test Extensions. 8 | UnitTestEx NUnit Test Extensions. 9 | unittestex api function unit test unittest nunit 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/UnitTestEx.NUnit/WithApiTester.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.AspNetCore; 5 | 6 | namespace UnitTestEx 7 | { 8 | /// 9 | /// Provides a shared to enable usage of the same underlying instance across multiple tests. 10 | /// 11 | /// The API startup . 12 | /// Implements to automatically dispose of the instance to release all resources. 13 | /// Be aware that using may result in cross-test contamination. 14 | public abstract class WithApiTester : IDisposable where TEntryPoint : class 15 | { 16 | private bool _disposed; 17 | private ApiTester? _apiTester = ApiTester.Create(); 18 | 19 | /// 20 | /// Gets the shared for testing. 21 | /// 22 | public ApiTester Test => _apiTester ?? throw new ObjectDisposedException(nameof(Test)); 23 | 24 | /// 25 | /// Dispose of all resources. 26 | /// 27 | /// Indicates whether from the . 28 | protected virtual void Dispose(bool disposing) 29 | { 30 | if (!_disposed) 31 | { 32 | if (disposing) 33 | { 34 | _apiTester?.Dispose(); 35 | _apiTester = null; 36 | } 37 | 38 | _disposed = true; 39 | } 40 | } 41 | 42 | /// 43 | public void Dispose() 44 | { 45 | Dispose(disposing: true); 46 | GC.SuppressFinalize(this); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/UnitTestEx.NUnit/strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avanade/UnitTestEx/d80ff2987e1f9aab282aa1c80638ba6015fbff0a/src/UnitTestEx.NUnit/strong-name-key.snk -------------------------------------------------------------------------------- /src/UnitTestEx.Xunit/ApiTestFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.Abstractions; 5 | using UnitTestEx.AspNetCore; 6 | using UnitTestEx.Xunit.Internal; 7 | 8 | namespace UnitTestEx 9 | { 10 | /// 11 | /// Provides a shared to enable usage of the same underlying instance across multiple tests. 12 | /// 13 | /// Be aware that using may result in cross-test contamination. 14 | public class ApiTestFixture : IDisposable where TEntryPoint : class 15 | { 16 | private ApiTester? _apiTester = ApiTester.Create(() => new XunitLocalTestImplementor()); 17 | private bool _disposed; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | public ApiTestFixture() 23 | { 24 | TestFrameworkImplementor.SetLocalCreateFactory(() => new XunitLocalTestImplementor()); 25 | OnConfiguration(); 26 | } 27 | 28 | /// 29 | /// Provides an opportunity to perform initial configuration before use. 30 | /// 31 | protected virtual void OnConfiguration() { } 32 | 33 | /// 34 | /// Gets the shared for testing. 35 | /// 36 | public ApiTester Test => _apiTester ?? throw new ObjectDisposedException(nameof(Test)); 37 | 38 | /// 39 | /// Releases the unmanaged resources and optionally releases the managed resources. 40 | /// 41 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 42 | protected virtual void Dispose(bool disposing) 43 | { 44 | if (!_disposed) 45 | { 46 | if (disposing) 47 | { 48 | _apiTester?.Dispose(); 49 | _apiTester = null; 50 | } 51 | 52 | _disposed = true; 53 | } 54 | } 55 | 56 | /// 57 | public void Dispose() 58 | { 59 | Dispose(disposing: true); 60 | GC.SuppressFinalize(this); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Xunit/Internal/XunitLocalTestImplementor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System.Threading; 4 | 5 | namespace UnitTestEx.Xunit.Internal 6 | { 7 | /// 8 | /// Provides a -based proxy to an instance. 9 | /// 10 | public sealed class XunitLocalTestImplementor() : Abstractions.TestFrameworkImplementor 11 | { 12 | private static readonly AsyncLocal _localImplementor = new(); 13 | 14 | /// 15 | /// Sets the . 16 | /// 17 | /// The . 18 | public static void SetLocalImplementor(XunitTestImplementor implementor) => _localImplementor.Value = implementor; 19 | 20 | /// 21 | /// Gets the . 22 | /// 23 | private static XunitTestImplementor? Implementor => _localImplementor.Value; 24 | 25 | /// 26 | public override void AssertAreEqual(T? expected, T? actual, string? message = null) where T : default => Implementor?.AssertAreEqual(expected, actual, message); 27 | 28 | /// 29 | public override void AssertFail(string? message) => Implementor?.AssertFail(message); 30 | 31 | /// 32 | public override void AssertInconclusive(string? message) => Implementor?.AssertInconclusive(message); 33 | 34 | /// 35 | public override void WriteLine(string? message) => Implementor?.WriteLine(message); 36 | } 37 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Xunit/Internal/XunitTestImplementor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | using Xunit.Sdk; 7 | 8 | namespace UnitTestEx.Xunit.Internal 9 | { 10 | /// 11 | /// Provides the Xunit implementation. 12 | /// 13 | /// The . 14 | public sealed class XunitTestImplementor(ITestOutputHelper testOutputHelper) : Abstractions.TestFrameworkImplementor 15 | { 16 | private const string _notSpecifiedText = "Not specified."; 17 | 18 | private readonly ITestOutputHelper _output = testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper)); 19 | 20 | /// 21 | /// Creates a using the . 22 | /// 23 | /// The . 24 | /// The . 25 | public static XunitTestImplementor Create(ITestOutputHelper testOutputHelper) => new(testOutputHelper); 26 | 27 | /// 28 | public override void AssertFail(string? message) => throw new XunitException(message ?? _notSpecifiedText); 29 | 30 | /// 31 | public override void AssertAreEqual(T? expected, T? actual, string? message = null) where T : default 32 | { 33 | try 34 | { 35 | Assert.Equal(expected, actual); 36 | } 37 | catch (XunitException ex) 38 | { 39 | throw new XunitException(message is null ? ex.Message : $"{message}{Environment.NewLine}{ex.Message}", ex); 40 | } 41 | } 42 | 43 | /// 44 | public override void AssertInconclusive(string? message) => AssertFail($"Inconclusive: {message}"); 45 | 46 | /// 47 | public override void WriteLine(string? message) => _output.WriteLine("{0}", message); 48 | } 49 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Xunit/UnitTestBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.Abstractions; 5 | using UnitTestEx.Xunit.Internal; 6 | using Xunit.Abstractions; 7 | 8 | namespace UnitTestEx 9 | { 10 | /// 11 | /// Provides the base Xunit capabilities. 12 | /// 13 | /// Primarily configures the required for the . 14 | public abstract class UnitTestBase : IDisposable 15 | { 16 | private bool _disposed; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The . 22 | protected UnitTestBase(ITestOutputHelper output) 23 | { 24 | Output = output ?? throw new ArgumentNullException(nameof(output)); 25 | TestFrameworkImplementor.SetLocalCreateFactory(() => new XunitTestImplementor(output)); 26 | } 27 | 28 | /// 29 | /// Gets the . 30 | /// 31 | protected ITestOutputHelper Output { get; } 32 | 33 | /// 34 | /// Dispose of all resources. 35 | /// 36 | /// Indicates whether from the . 37 | protected virtual void Dispose(bool disposing) 38 | { 39 | if (!_disposed) 40 | { 41 | if (disposing) 42 | TestFrameworkImplementor.ResetLocalCreateFactory(); 43 | 44 | _disposed = true; 45 | } 46 | } 47 | 48 | /// 49 | public void Dispose() 50 | { 51 | Dispose(disposing: true); 52 | GC.SuppressFinalize(this); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Xunit/UnitTestEx.Xunit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | UnitTestEx.Xunit 6 | UnitTestEx Xunit 7 | UnitTestEx Xunit Test Extensions. 8 | UnitTestEx Xunit Test Extensions. 9 | unittestex api function unit test unittest xunit 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/UnitTestEx.Xunit/WithApiTester.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.AspNetCore; 5 | using UnitTestEx.Xunit.Internal; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace UnitTestEx 10 | { 11 | /// 12 | /// Provides a shared to enable usage of the same underlying instance across multiple tests. 13 | /// 14 | /// The API startup . 15 | /// Implements to automatically dispose of the instance to release all resources. 16 | /// Be aware that using may result in cross-test contamination. 17 | public abstract class WithApiTester : UnitTestBase, IClassFixture> where TEntryPoint : class 18 | { 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The shared . 23 | /// The . 24 | public WithApiTester(ApiTestFixture fixture, ITestOutputHelper output) : base(output) 25 | { 26 | Test = fixture.Test; 27 | XunitLocalTestImplementor.SetLocalImplementor(new XunitTestImplementor(output)); 28 | } 29 | 30 | /// 31 | /// Gets the shared for testing. 32 | /// 33 | public ApiTester Test { get; } 34 | } 35 | } -------------------------------------------------------------------------------- /src/UnitTestEx.Xunit/strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avanade/UnitTestEx/d80ff2987e1f9aab282aa1c80638ba6015fbff0a/src/UnitTestEx.Xunit/strong-name-key.snk -------------------------------------------------------------------------------- /src/UnitTestEx/Abstractions/OneOffTestSetUpAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | 7 | namespace UnitTestEx.Abstractions 8 | { 9 | /// 10 | /// Marks a class to be run once before all tests in the assembly. 11 | /// 12 | /// The to invoke. 13 | /// 14 | [AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)] 15 | public class OneOffTestSetUpAttribute(Type type) : Attribute 16 | { 17 | private static readonly object _oneOffSetUpLock = new(); 18 | private static readonly HashSet _oneOffSetUpTypes = []; 19 | 20 | /// 21 | /// Performs the set up for the specified where configured using the . 22 | /// 23 | /// The . 24 | public static void SetUp(Assembly assembly) 25 | { 26 | var att = assembly.GetCustomAttributes(typeof(OneOffTestSetUpAttribute), false); 27 | if (att.Length > 0 && att[0] is OneOffTestSetUpAttribute sua) 28 | sua.SetUp(); 29 | } 30 | 31 | /// 32 | /// Gets the . 33 | /// 34 | public Type Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 35 | 36 | /// 37 | /// Performs the one-off test set up using the specified . 38 | /// 39 | public void SetUp() => ((Activator.CreateInstance(Type) as OneOffTestSetUpBase) ?? throw new InvalidOperationException($"Type {Type.Name} must inherit from {nameof(OneOffTestSetUpBase)}.")).SetUp(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Abstractions/OneOffTestSetUpBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace UnitTestEx.Abstractions 4 | { 5 | /// 6 | /// Provides a base class for one-off test set up. 7 | /// 8 | /// The inheriting class must also reference via the to enable. 9 | public abstract class OneOffTestSetUpBase 10 | { 11 | /// 12 | /// Executes the one-off test set up. 13 | /// 14 | public abstract void SetUp(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Abstractions/TestSetUpException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | 5 | namespace UnitTestEx.Abstractions 6 | { 7 | /// 8 | /// Represents a exception. 9 | /// 10 | /// The message text. 11 | public class TestSetUpException(string? message = null) : Exception($"The test set up was unsuccessful:{Environment.NewLine}{message ?? "No reason specified."}") { } 12 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Abstractions/TesterExtensionsConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | using UnitTestEx.Expectations; 9 | 10 | namespace UnitTestEx.Abstractions 11 | { 12 | /// 13 | /// Provides the configuration used by the . 14 | /// 15 | public abstract class TesterExtensionsConfig 16 | { 17 | /// 18 | /// Occurs when the is invoked (just prior to invocation). 19 | /// 20 | /// The owning . 21 | public virtual void OnUseSetUp(TesterBase owner) { } 22 | 23 | /// 24 | /// Provides the opportunity to further configure the underlying test host . 25 | /// 26 | /// The owning . 27 | /// The . 28 | public virtual void ConfigureServices(TesterBase owner, IServiceCollection services) { } 29 | 30 | /// 31 | /// Updates the from the (where applicable). 32 | /// 33 | /// The value . 34 | /// The owning . 35 | /// The . 36 | /// The deserialized value (override where applicable). 37 | /// The value will have already been deserialized. 38 | public virtual void UpdateValueFromHttpResponseMessage(TesterBase owner, HttpResponseMessage response, ref TValue? value) { } 39 | 40 | /// 41 | /// Updates the from the (where applicable). 42 | /// 43 | /// The value . 44 | /// The owning . 45 | /// The . 46 | /// The deserialized value (override where applicable). 47 | /// The value will have already been deserialized/converted. 48 | public virtual void UpdateValueFromActionResult(TesterBase owner, IActionResult actionResult, ref TValue? value) { } 49 | 50 | /// 51 | /// Provides the opportunity to extend the functionality. 52 | /// 53 | /// The tester (see ). 54 | /// The being asserted. 55 | /// The . 56 | public virtual Task ExpectationAssertAsync(ExpectationsBase expectation, AssertArgs args) => Task.CompletedTask; 57 | } 58 | } -------------------------------------------------------------------------------- /src/UnitTestEx/ApiError.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace UnitTestEx 4 | { 5 | /// 6 | /// Represents an API-style error being and . 7 | /// 8 | /// The optional field/property name. 9 | /// The error message. 10 | public class ApiError(string? field, string message) 11 | { 12 | /// 13 | /// Gets the optional field/property name. 14 | /// 15 | public string? Field { get; } = field; 16 | 17 | /// 18 | /// Gets the error message. 19 | /// 20 | public string Message { get; } = message; 21 | } 22 | } -------------------------------------------------------------------------------- /src/UnitTestEx/ApiTester.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.Abstractions; 5 | using UnitTestEx.AspNetCore; 6 | 7 | namespace UnitTestEx 8 | { 9 | /// 10 | /// Provides the API testing capability. 11 | /// 12 | public static class ApiTester 13 | { 14 | /// 15 | /// Creates a new instance of the class. 16 | /// 17 | /// The API startup . 18 | /// The optional function to create the instance. 19 | /// The . 20 | public static ApiTester Create(Func? createFactory = null) where TEntryPoint : class => new(createFactory); 21 | } 22 | } -------------------------------------------------------------------------------- /src/UnitTestEx/AspNetCore/ApiTesterT.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.Abstractions; 5 | 6 | namespace UnitTestEx.AspNetCore 7 | { 8 | /// 9 | /// Provides the concrete implementation. 10 | /// 11 | /// The API startup . 12 | /// The optional function to create the instance. 13 | public class ApiTester(Func? createFactory = null) : ApiTesterBase>(TestFrameworkImplementor.Create(createFactory)) where TEntryPoint : class { } 14 | } 15 | -------------------------------------------------------------------------------- /src/UnitTestEx/AspNetCore/BypassAuthorizationHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.AspNetCore.Authorization; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace UnitTestEx.AspNetCore 8 | { 9 | /// 10 | /// An to bypass authorization checking; i.e. act as anonymous. 11 | /// 12 | public class BypassAuthorizationHandler : IAuthorizationHandler 13 | { 14 | /// 15 | /// Handle by succeeding all checks. 16 | /// 17 | /// The . 18 | public Task HandleAsync(AuthorizationHandlerContext context) 19 | { 20 | foreach (IAuthorizationRequirement requirement in context.PendingRequirements.ToList()) 21 | context.Succeed(requirement); 22 | 23 | return Task.CompletedTask; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/UnitTestEx/AspNetCore/HttpTesterBaseT.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.AspNetCore.TestHost; 4 | using System; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using UnitTestEx.Abstractions; 8 | using UnitTestEx.Expectations; 9 | 10 | namespace UnitTestEx.AspNetCore 11 | { 12 | /// 13 | /// Provides the base HTTP testing capabilities. 14 | /// 15 | /// The representing itself. 16 | public abstract class HttpTesterBase : HttpTesterBase, IHttpResponseMessageExpectations where TSelf : HttpTesterBase 17 | { 18 | /// 19 | /// Initializes a new class. 20 | /// 21 | /// The owning . 22 | /// The . 23 | public HttpTesterBase(TesterBase owner, TestServer testServer) : base(owner, testServer) => ExpectationsArranger = new ExpectationsArranger(owner, (TSelf)this); 24 | 25 | /// 26 | /// Gets the . 27 | /// 28 | public ExpectationsArranger ExpectationsArranger { get; } 29 | 30 | /// 31 | /// Sets (overrides) the test user name (defaults to ). 32 | /// 33 | /// The test user name. 34 | /// The instance to support fluent-style method-chaining. 35 | public TSelf WithUser(string? userName) 36 | { 37 | UserName = userName; 38 | return (TSelf)this; 39 | } 40 | 41 | /// 42 | /// Sets (overrides) the test user name (defaults to ). 43 | /// 44 | /// The test user identifier. 45 | /// The instance to support fluent-style method-chaining. 46 | /// The is required for the conversion to take place. 47 | public TSelf WithUser(object? userIdentifier) 48 | { 49 | if (userIdentifier == null) 50 | return WithUser(null); 51 | 52 | if (Owner.SetUp.UserNameConverter == null) 53 | throw new InvalidOperationException($"The {nameof(TestSetUp)}.{nameof(TestSetUp.UserNameConverter)} must be defined to support user identifier conversion."); 54 | 55 | return WithUser(Owner.SetUp.UserNameConverter(userIdentifier)); 56 | } 57 | 58 | /// 59 | /// Perform the assertion of any expectations. 60 | /// 61 | /// The / 62 | protected override Task AssertExpectationsAsync(HttpResponseMessage res) => ExpectationsArranger.AssertAsync(ExpectationsArranger.CreateArgs(LastLogs).AddExtra(res)); 63 | } 64 | } -------------------------------------------------------------------------------- /src/UnitTestEx/AspNetCore/HttpTesterBaseT2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.AspNetCore.TestHost; 4 | using System; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using UnitTestEx.Abstractions; 8 | using UnitTestEx.Expectations; 9 | 10 | namespace UnitTestEx.AspNetCore 11 | { 12 | /// 13 | /// Provides the base HTTP testing capabilities. 14 | /// 15 | /// The response value . 16 | /// The representing itself. 17 | public abstract class HttpTesterBase : HttpTesterBase, IHttpResponseMessageExpectations, IValueExpectations where TSelf : HttpTesterBase 18 | { 19 | /// 20 | /// Initializes a new class. 21 | /// 22 | /// The owning . 23 | /// The . 24 | public HttpTesterBase(TesterBase owner, TestServer testServer) : base(owner, testServer) => ExpectationsArranger = new ExpectationsArranger(owner, (TSelf)this); 25 | 26 | /// 27 | /// Gets the . 28 | /// 29 | public ExpectationsArranger ExpectationsArranger { get; } 30 | 31 | /// 32 | /// Sets (overrides) the test user name (defaults to ). 33 | /// 34 | /// The test user name. 35 | /// The instance to support fluent-style method-chaining. 36 | public TSelf WithUser(string? userName) 37 | { 38 | UserName = userName; 39 | return (TSelf)this; 40 | } 41 | 42 | /// 43 | /// Sets (overrides) the test user name (defaults to ). 44 | /// 45 | /// The test user identifier. 46 | /// The instance to support fluent-style method-chaining. 47 | /// The is required for the conversion to take place. 48 | public TSelf WithUser(object? userIdentifier) 49 | { 50 | if (userIdentifier == null) 51 | return WithUser(null); 52 | 53 | if (Owner.SetUp.UserNameConverter == null) 54 | throw new InvalidOperationException($"The {nameof(TestSetUp)}.{nameof(TestSetUp.UserNameConverter)} must be defined to support user identifier conversion."); 55 | 56 | return WithUser(Owner.SetUp.UserNameConverter(userIdentifier)); 57 | } 58 | 59 | /// 60 | protected override Task AssertExpectationsAsync(HttpResponseMessage res) => ExpectationsArranger.AssertAsync(ExpectationsArranger.CreateArgs(LastLogs).AddExtra(res)); 61 | } 62 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Assertors/Assertor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace UnitTestEx.Assertors 9 | { 10 | /// 11 | /// Provides assertion helper methods. 12 | /// 13 | public static class Assertor 14 | { 15 | /// 16 | /// Converts the dictionary to an array. 17 | /// 18 | /// The errors. 19 | /// The array. 20 | public static ApiError[] ConvertToApiErrors(IDictionary? errors) 21 | { 22 | List? actual = []; 23 | 24 | if (errors != null) 25 | { 26 | foreach (var kvp in errors.Where(x => !string.IsNullOrEmpty(x.Key))) 27 | { 28 | foreach (var message in kvp.Value.Where(x => !string.IsNullOrEmpty(x))) 29 | { 30 | (actual ??= []).Add(new ApiError(kvp.Key, message)); 31 | } 32 | } 33 | } 34 | 35 | return actual?.ToArray() ?? []; 36 | } 37 | 38 | /// 39 | /// Trys to match the and contents. 40 | /// 41 | /// The expected list. 42 | /// The actual list. 43 | /// The error message where they do not match. 44 | /// true where matched; otherwise, false. 45 | public static bool TryAreErrorsMatched(IEnumerable? expected, IEnumerable? actual, out string? errorMessage) 46 | { 47 | var exp = (from e in (expected ?? Array.Empty()) 48 | where !(actual ?? Array.Empty()).Any(a => a.Message == e.Message && (e.Field == null || a.Field == e.Field)) 49 | select e).ToList(); 50 | 51 | var act = (from a in (actual ?? Array.Empty()) 52 | where !(expected ?? Array.Empty()).Any(e => a.Message == e.Message && (e.Field == null || a.Field == e.Field)) 53 | select a).ToList(); 54 | 55 | var sb = new StringBuilder(); 56 | if (exp.Count > 0) 57 | { 58 | sb.AppendLine(" Expected messages not matched:"); 59 | exp.ForEach(m => sb.AppendLine($" Error: {m.Message} {(m.Field != null ? $"[{m.Field}]" : null)}")); 60 | } 61 | 62 | if (act.Count > 0) 63 | { 64 | sb.AppendLine(" Actual messages not matched:"); 65 | act.ForEach(m => sb.AppendLine($" Error: {m.Message} {(m.Field != null ? $"[{m.Field}]" : null)}")); 66 | } 67 | 68 | errorMessage = sb.Length > 0 ? $"Error messages mismatch:{System.Environment.NewLine}{sb}" : null; 69 | return sb.Length == 0; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Assertors/AssertorBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using UnitTestEx.Abstractions; 6 | using UnitTestEx.Json; 7 | 8 | namespace UnitTestEx.Assertors 9 | { 10 | /// 11 | /// Represents the base test assert helper. 12 | /// 13 | /// The owning . 14 | /// The (if any). 15 | public abstract class AssertorBase(TesterBase owner, Exception? exception) 16 | { 17 | private static List>? _assertErrorsExtentions; 18 | 19 | /// 20 | /// Adds an AssertErrors extension to support custom error assertions. 21 | /// 22 | /// The assertion extension. 23 | /// The extension functions return a bool; where true indicates that the function asserted the errors and no further assertions need to be performed. 24 | public static void AddAssertErrorsExtension(Func assertErrorsExtension) => (_assertErrorsExtentions ??= []).Add(assertErrorsExtension); 25 | 26 | /// 27 | /// Gets the owning . 28 | /// 29 | public TesterBase Owner { get; } = owner ?? throw new ArgumentNullException(nameof(owner)); 30 | 31 | /// 32 | /// Gets the . 33 | /// 34 | public Exception? Exception { get; } = exception; 35 | 36 | /// 37 | /// Gets the . 38 | /// 39 | protected TestFrameworkImplementor Implementor => Owner.Implementor; 40 | 41 | /// 42 | /// Gets the . 43 | /// 44 | protected IJsonSerializer JsonSerializer => Owner.JsonSerializer; 45 | 46 | /// 47 | /// Asserts the errors using the extensions. 48 | /// 49 | internal void AssertErrorsUsingExtensions(ApiError[] errors) 50 | { 51 | if (_assertErrorsExtentions is null) 52 | throw new NotImplementedException($"AssertErrors is unable to be performed as there are no extensions to perform the assertion; see {nameof(AddAssertErrorsExtension)}."); 53 | else 54 | { 55 | foreach (var ae in _assertErrorsExtentions) 56 | { 57 | if (ae(this, errors)) 58 | return; 59 | } 60 | 61 | if (!Assertor.TryAreErrorsMatched(errors, Array.Empty(), out var errorMessage)) 62 | Implementor.AssertFail(errorMessage); 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Assertors/HttpResponseMessageAssertorBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Net.Http; 5 | using System.Net.Mime; 6 | using UnitTestEx.Abstractions; 7 | using UnitTestEx.Json; 8 | 9 | namespace UnitTestEx.Assertors 10 | { 11 | /// 12 | /// Provdes the base test assert helper capabilities. 13 | /// 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The owning . 18 | /// The . 19 | public abstract class HttpResponseMessageAssertorBase(TesterBase owner, HttpResponseMessage response) 20 | { 21 | /// 22 | /// Gets the owning . 23 | /// 24 | public TesterBase Owner { get; } = owner ?? throw new ArgumentNullException(nameof(owner)); 25 | 26 | /// 27 | /// Gets the . 28 | /// 29 | public HttpResponseMessage Response { get; } = response; 30 | 31 | /// 32 | /// Gets the . 33 | /// 34 | protected internal TestFrameworkImplementor Implementor => Owner.Implementor; 35 | 36 | /// 37 | /// Gets the . 38 | /// 39 | public IJsonSerializer JsonSerializer => Owner.JsonSerializer; 40 | 41 | /// 42 | /// Gets the response content as the deserialized JSON value. 43 | /// 44 | /// The content . 45 | /// The expected content type; where null then the content type will not be validated. 46 | /// The result value. 47 | public T? GetValue(string? expectedContentType = MediaTypeNames.Application.Json) 48 | { 49 | if (expectedContentType is not null) 50 | Implementor.AssertAreEqual(expectedContentType, Response.Content?.Headers?.ContentType?.MediaType); 51 | 52 | if (Response.Content == null) 53 | return default!; 54 | 55 | var value = JsonSerializer.Deserialize(Response.Content.ReadAsStringAsync().GetAwaiter().GetResult()!); 56 | 57 | foreach (var ext in TestSetUp.Extensions) 58 | ext.UpdateValueFromHttpResponseMessage(Owner, Response, ref value); 59 | 60 | return value; 61 | } 62 | 63 | /// 64 | /// Gets the response content as a . 65 | /// 66 | /// The result content . 67 | public string? GetContent() 68 | { 69 | if (Response.Content == null) 70 | return null; 71 | 72 | return Response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Assertors/VoidAssertor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.Abstractions; 5 | 6 | namespace UnitTestEx.Assertors 7 | { 8 | /// 9 | /// Represents the test assert helper where there is no return value; i.e. . 10 | /// 11 | /// The owning . 12 | /// The (if any). 13 | public class VoidAssertor(TesterBase owner, Exception? exception) : AssertorBase(owner, exception) { } 14 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/ExceptionExpectations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using UnitTestEx.Abstractions; 7 | 8 | namespace UnitTestEx.Expectations 9 | { 10 | /// 11 | /// Provides expectations. 12 | /// 13 | /// The owning . 14 | /// The initiating tester. 15 | public class ExceptionExpectations(TesterBase owner, TTester tester) : ExpectationsBase(owner, tester) 16 | { 17 | /// 18 | public override int Order => int.MinValue; 19 | 20 | /// 21 | /// Indicates that the expectation is that the execution is successful. 22 | /// 23 | public bool ExpectSuccess { get; set; } 24 | 25 | /// 26 | /// Indicates that the expectation is that an will be thrown during execution. 27 | /// 28 | public bool ExpectException { get; private set; } 29 | 30 | /// 31 | /// Gets the optional . 32 | /// 33 | public Type? ExceptionType { get; private set; } 34 | 35 | /// 36 | /// Gets the optional expected message to match (contains). 37 | /// 38 | public string? ExceptionMessage { get; private set; } 39 | 40 | /// 41 | /// Expects that an will be thrown during execution. 42 | /// 43 | /// The optional . 44 | /// The optional expected message to match (contains). 45 | public void SetExpectException(Type? exceptionType, string? expectedMessage) 46 | { 47 | ExpectException = true; 48 | ExceptionType = exceptionType; 49 | ExceptionMessage = expectedMessage; 50 | } 51 | 52 | /// 53 | protected override Task OnAssertAsync(AssertArgs args) 54 | { 55 | if (ExpectException) 56 | { 57 | if (args.Exception is null) 58 | args.Tester.Implementor.AssertFail("Expected an exception; however, the execution was successful."); 59 | else 60 | { 61 | if (ExceptionType is not null && ExceptionType != args.Exception.GetType()) 62 | args.Tester.Implementor.AssertFail($"Expected Exception type '{ExceptionType.Name}' not equal to actual '{args.Exception.GetType().Name}'."); 63 | 64 | if (ExceptionMessage is not null && !args.Exception.Message.Contains(ExceptionMessage, StringComparison.InvariantCultureIgnoreCase)) 65 | args.Tester.Implementor.AssertFail($"Expected Exception message '{ExceptionMessage}' is not contained within '{args.Exception.Message}'."); 66 | } 67 | } 68 | 69 | if (ExpectSuccess) 70 | { 71 | if (args.Exception is not null) 72 | args.Tester.Implementor.AssertFail($"Expected success; however, '{args.Exception.GetType().Name}' was thrown: {args.Exception.Message}"); 73 | 74 | if (args.TryGetExtra(out var result) && result is not null && !result.IsSuccessStatusCode) 75 | args.Tester.Implementor.AssertFail($"Expected success; however, the {nameof(HttpResponseMessage)} has an unsuccessful {nameof(HttpResponseMessage.StatusCode)} of {result.StatusCode} ({(int)result.StatusCode})."); 76 | } 77 | 78 | return Task.CompletedTask; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/ExpectationsBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using UnitTestEx.Abstractions; 7 | 8 | namespace UnitTestEx.Expectations 9 | { 10 | /// 11 | /// Provides the base Expectations implementation. 12 | /// 13 | /// The owning 14 | public abstract class ExpectationsBase(TesterBase owner) 15 | { 16 | private List>>? _extendedActions; 17 | 18 | /// 19 | /// Gets the owning . 20 | /// 21 | public TesterBase Owner { get; } = owner ?? throw new ArgumentNullException(nameof(owner)); 22 | 23 | /// 24 | /// Gets or sets the order in which the expectation is asserted. 25 | /// 26 | public virtual int Order { get; } = 100; 27 | 28 | /// 29 | /// Indicates whether to skip the base invocation executing only the extensions. 30 | /// 31 | public bool SkipOnAssert { get; set; } 32 | 33 | /// 34 | /// Adds an extension to the assertion to be executed after the base assertion. 35 | /// 36 | /// The extension. 37 | /// To stop any further extensions being executed, or the , the extension should signal handled with a response of true. 38 | public void AddExtension(Func> extension) 39 | { 40 | _extendedActions ??= []; 41 | _extendedActions.Add(extension ?? throw new ArgumentNullException(nameof(extension))); 42 | } 43 | 44 | /// 45 | /// Performs the base assertion and then any extensions. 46 | /// 47 | /// The . 48 | public async Task AssertAsync(AssertArgs args) 49 | { 50 | ArgumentNullException.ThrowIfNull(args, nameof(args)); 51 | 52 | if (!SkipOnAssert) 53 | await OnAssertAsync(args).ConfigureAwait(false); 54 | 55 | if (_extendedActions is not null) 56 | { 57 | foreach (var ea in _extendedActions) 58 | { 59 | if (!await ea(args).ConfigureAwait(false)) 60 | return; 61 | } 62 | } 63 | 64 | await OnExtensionsAssert(args).ConfigureAwait(false); 65 | await OnLastAssertAsync(args).ConfigureAwait(false); 66 | } 67 | 68 | /// 69 | /// Performs the assertion. 70 | /// 71 | /// The . 72 | protected abstract Task OnAssertAsync(AssertArgs args); 73 | 74 | /// 75 | /// Performs any extension assertions. 76 | /// 77 | /// The . 78 | protected internal abstract Task OnExtensionsAssert(AssertArgs args); 79 | 80 | /// 81 | /// Performs any final assertion after all extensions have executed. 82 | /// 83 | /// The . 84 | protected virtual Task OnLastAssertAsync(AssertArgs args) => Task.CompletedTask; 85 | 86 | /// 87 | /// Resets the expectation back to its orginating assert state to allow for a re-execution. 88 | /// 89 | public virtual void Reset() { } 90 | } 91 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/ExpectationsBaseT.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using UnitTestEx.Abstractions; 6 | 7 | namespace UnitTestEx.Expectations 8 | { 9 | /// 10 | /// Provides the base Expectations implementation. 11 | /// 12 | /// The owning 13 | /// The initiating tester. 14 | public abstract class ExpectationsBase(TesterBase owner, TTester tester) : ExpectationsBase(owner) 15 | { 16 | /// 17 | /// Gets the initiating tester. 18 | /// 19 | public TTester Tester { get; } = tester ?? throw new ArgumentNullException(nameof(tester)); 20 | 21 | /// 22 | protected internal override async Task OnExtensionsAssert(AssertArgs args) 23 | { 24 | foreach (var ext in TestSetUp.Extensions) 25 | await ext.ExpectationAssertAsync(this, args).ConfigureAwait(false); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/HttpResponseMessageExpectations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using UnitTestEx.Abstractions; 8 | 9 | namespace UnitTestEx.Expectations 10 | { 11 | /// 12 | /// Provides expectations. 13 | /// 14 | /// The owning . 15 | /// The initiating tester. 16 | public class HttpResponseMessageExpectations(TesterBase owner, TTester tester) : ExpectationsBase(owner, tester) 17 | { 18 | private HttpStatusCode? _httpStatusCode; 19 | 20 | /// 21 | /// Expects that the is equal to the . 22 | /// 23 | /// The . 24 | public void SetExpectStatusCode(HttpStatusCode httpStatusCode) => _httpStatusCode = httpStatusCode; 25 | 26 | /// 27 | protected override Task OnAssertAsync(AssertArgs args) 28 | { 29 | if (!args.TryGetExtra(out var result)) 30 | throw new InvalidOperationException($"The '{nameof(HttpResponseMessage)}' Extra value must be set for this expectation assertion to function."); 31 | 32 | if (_httpStatusCode.HasValue && result!.StatusCode != _httpStatusCode) 33 | args.Tester.Implementor.AssertFail($"Expected StatusCode value of '{_httpStatusCode.Value} ({(int)_httpStatusCode.Value})'; actual was '{result.StatusCode} ({(int)result.StatusCode})'."); 34 | 35 | return Task.CompletedTask; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/IExpectations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace UnitTestEx.Expectations 4 | { 5 | /// 6 | /// Enables the Expectations fluent-style method-chaining capabilities. 7 | /// 8 | /// The representing itself. 9 | public interface IExpectations where TSelf : IExpectations 10 | { 11 | /// 12 | /// Gets the . 13 | /// 14 | ExpectationsArranger ExpectationsArranger { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/IHttpResponseMessageExpectations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System.Net.Http; 4 | 5 | namespace UnitTestEx.Expectations 6 | { 7 | /// 8 | /// Enables the Expectations fluent-style method-chaining capabilities. 9 | /// 10 | /// The representing itself. 11 | public interface IHttpResponseMessageExpectations : IExpectations where TSelf : IHttpResponseMessageExpectations { } 12 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/IValueExpectations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace UnitTestEx.Expectations 4 | { 5 | /// 6 | /// Enables the Expectations fluent-style method-chaining capabilities. 7 | /// 8 | /// The response value . 9 | /// The representing itself. 10 | public interface IValueExpectations : IExpectations where TSelf : IValueExpectations { } 11 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/LoggerExpectations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using UnitTestEx.Abstractions; 9 | 10 | namespace UnitTestEx.Expectations 11 | { 12 | /// 13 | /// Provides log expectations. 14 | /// 15 | /// The owning . 16 | /// The initiating tester. 17 | public class LoggerExpectations(TesterBase owner, TTester tester) : ExpectationsBase(owner, tester) 18 | { 19 | private readonly List _expectTexts = []; 20 | 21 | /// 22 | /// Expects that the will have logged a message that contains the specified . 23 | /// 24 | /// The text(s) that should appear in at least one log message line. 25 | public void SetExpectLogContains(params string[] texts) => _expectTexts.AddRange(texts.Where(t => !string.IsNullOrEmpty(t))); 26 | 27 | /// 28 | protected override Task OnAssertAsync(AssertArgs args) 29 | { 30 | if ((args.Logs is null || !args.Logs.Any()) && _expectTexts.Count > 0) 31 | args.Tester.Implementor.AssertFail($"Expected one or more log texts that were not found."); 32 | 33 | foreach (var et in _expectTexts) 34 | { 35 | if (args.Logs!.Any(l => !string.IsNullOrEmpty(l) && l.Contains(et, StringComparison.InvariantCultureIgnoreCase))) 36 | continue; 37 | 38 | args.Tester.Implementor.AssertFail($"Expected a log text to contain '{et}' that was not found."); 39 | } 40 | 41 | return Task.CompletedTask; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Expectations/ValueExpectations.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using UnitTestEx.Abstractions; 8 | using UnitTestEx.Json; 9 | 10 | namespace UnitTestEx.Expectations 11 | { 12 | /// 13 | /// Provides value expectations. 14 | /// 15 | /// The owning . 16 | /// The initiating tester. 17 | public class ValueExpectations(TesterBase owner, TTester tester) : ExpectationsBase(owner, tester) 18 | { 19 | private const string NullJson = "null"; 20 | private Func? _json; 21 | private bool _expectNull; 22 | 23 | /// 24 | /// Expects that the result JSON compares to the expected . 25 | /// 26 | /// The expected JSON function. 27 | public void SetExpectJson(Func json) => _json = json ?? throw new ArgumentNullException(nameof(json)); 28 | 29 | /// 30 | /// Expects the the result JSON is null. 31 | /// 32 | public void SetExpectNull() => _expectNull = true; 33 | 34 | /// 35 | /// Indicates whether the expected value was matched. 36 | /// 37 | /// This must be set to true once matched; otherwise, the will assert a failure. 38 | public bool ValueMatched { get; private set; } 39 | 40 | /// 41 | protected async override Task OnAssertAsync(AssertArgs args) 42 | { 43 | if (_json is null) 44 | return; // No value configured to compare. 45 | 46 | JsonElementComparerResult? jcr; 47 | var jc = new JsonElementComparer(new JsonElementComparerOptions { JsonSerializer = args.Tester.JsonSerializer }); 48 | 49 | string expectedJson = _expectNull ? NullJson : (_json?.Invoke(Tester) ?? NullJson); 50 | 51 | // Where there is an explicit value, serialize and compare. 52 | if (args.HasValue) 53 | jcr = jc.Compare(expectedJson, args.Tester.JsonSerializer.Serialize(args.HasValue ? args.Value : null), args.PathsToIgnore.ToArray()); 54 | else 55 | { 56 | // Where there is no explicit value, see if there is an HTTP response. 57 | if (!args.TryGetExtra(out var result) || result is null) 58 | return; 59 | 60 | if (!result.IsSuccessStatusCode) 61 | { 62 | args.Tester.Implementor.AssertFail($"Expected value; however, the {nameof(HttpResponseMessage)} has an unsuccessful {nameof(HttpResponseMessage.StatusCode)} of {result.StatusCode} ({(int)result.StatusCode})."); 63 | return; 64 | } 65 | 66 | jcr = jc.Compare(expectedJson, result.Content.Headers.ContentLength == 0 ? NullJson : await result.Content.ReadAsStringAsync().ConfigureAwait(false), args.PathsToIgnore.ToArray()); 67 | } 68 | 69 | // Assert the differences. 70 | if (jcr.HasDifferences) 71 | args.Tester.Implementor.AssertFail($"Expected and Actual values are not equal:{Environment.NewLine}{jcr}"); 72 | else 73 | ValueMatched = true; 74 | } 75 | 76 | /// 77 | protected override Task OnLastAssertAsync(AssertArgs args) 78 | { 79 | if (!ValueMatched) 80 | args.Tester.Implementor.AssertFail($"Expected value; however, none was returned."); 81 | 82 | return Task.CompletedTask; 83 | } 84 | 85 | /// 86 | public override void Reset() 87 | { 88 | base.Reset(); 89 | ValueMatched = false; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Generic/GenericTesterT.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using UnitTestEx.Abstractions; 4 | 5 | namespace UnitTestEx.Generic 6 | { 7 | /// 8 | /// Provides the NUnit generic testing capability. 9 | /// 10 | public class GenericTester() : GenericTesterBase>(TestFrameworkImplementor.Create()) where TEntryPoint : class { } 11 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Generic/GenericTesterT2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using UnitTestEx.Abstractions; 4 | 5 | namespace UnitTestEx.Generic 6 | { 7 | /// 8 | /// Provides the NUnit generic testing capability. 9 | /// 10 | public class GenericTester() : GenericTesterBase>(TestFrameworkImplementor.Create()) where TEntryPoint : class { } 11 | } -------------------------------------------------------------------------------- /src/UnitTestEx/GenericTester.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using UnitTestEx.Generic; 5 | using UnitTestEx.Hosting; 6 | 7 | namespace UnitTestEx 8 | { 9 | /// 10 | /// Provides the NUnit generic testing capability. 11 | /// 12 | public static class GenericTester 13 | { 14 | /// 15 | /// Creates a new instance of the class. 16 | /// 17 | /// The . 18 | public static GenericTester Create() => Create(); 19 | 20 | /// 21 | /// Creates a new instance of the class. 22 | /// 23 | /// The . 24 | /// The . 25 | public static GenericTester Create() where TEntryPoint : class => new(); 26 | 27 | /// 28 | /// Creates a new instance of the class. 29 | /// 30 | /// The value . 31 | /// The . 32 | public static GenericTester CreateFor() => CreateFor(); 33 | 34 | /// 35 | /// Creates a new instance of the class. 36 | /// 37 | /// The . 38 | /// The value . 39 | /// The . 40 | public static GenericTester CreateFor() where TEntryPoint : class => new(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/IJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | 5 | namespace UnitTestEx.Json 6 | { 7 | /// 8 | /// Provides the core (common) JSON Serialize and Deserialize capabilities. 9 | /// 10 | public interface IJsonSerializer 11 | { 12 | /// 13 | /// Gets the underlying serializer configuration settings/options. 14 | /// 15 | object Options { get; } 16 | 17 | /// 18 | /// Serialize the to a JSON . 19 | /// 20 | /// The . 21 | /// The value to serialize. 22 | /// Where specified overrides the serialization write formatting. 23 | /// The JSON . 24 | string Serialize(T value, JsonWriteFormat? format = null); 25 | 26 | /// 27 | /// Deserialize the JSON to an underlying JSON object. 28 | /// 29 | /// The JSON . 30 | /// The JSON object (as per the underlying implementation). 31 | object? Deserialize(string json); 32 | 33 | /// 34 | /// Deserialize the JSON to the specified . 35 | /// 36 | /// The JSON . 37 | /// The to convert to. 38 | /// The corresponding typed value. 39 | object? Deserialize(string json, Type type); 40 | 41 | /// 42 | /// Deserialize the JSON to the of . 43 | /// 44 | /// The to convert to. 45 | /// The JSON . 46 | /// The corresponding typed value. 47 | T? Deserialize(string json); 48 | } 49 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonElementComparerOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text.Json; 6 | 7 | namespace UnitTestEx.Json 8 | { 9 | /// 10 | /// Provides the options for . 11 | /// 12 | public sealed class JsonElementComparerOptions 13 | { 14 | private static JsonElementComparerOptions? _default; 15 | 16 | /// 17 | /// Gets or sets the default instance. 18 | /// 19 | public static JsonElementComparerOptions Default 20 | { 21 | get => _default ??= new(); 22 | set => _default = value ?? throw new ArgumentNullException(nameof(value)); 23 | } 24 | 25 | /// 26 | /// Gets or sets the to use for comparing JSON paths. 27 | /// 28 | /// Defaults to . 29 | public IEqualityComparer PathComparer { get; set; } = StringComparer.OrdinalIgnoreCase; 30 | 31 | /// 32 | /// Gets or sets the to use for comparing property names. 33 | /// 34 | /// Where not specified will use the native (fast) exact comparison; otherwise, will use 35 | /// which is less performant (however, enables semantic comparison where applicable). 36 | public IEqualityComparer? PropertyNameComparer { get; set; } 37 | 38 | /// 39 | /// Gets or sets the maximum number of differences to detect where performing a comparison. 40 | /// 41 | /// Defaults to . 42 | public int MaxDifferences { get; set; } = int.MaxValue; 43 | 44 | /// 45 | /// Gets or sets the used for property value comparisons. 46 | /// 47 | /// When : a the comparison will be performed using , , and 48 | /// (in order specified) until match found; otherwise, for a the comparison will be performed using and (in order specified) until a match 49 | /// found.Defaults to . 50 | public JsonElementComparison ValueComparison { get; set; } = JsonElementComparison.Semantic; 51 | 52 | /// 53 | /// Gets or sets the used for null value comparisons. 54 | /// 55 | /// When : a where the other property does not exist assumes is equivalent null by default. 56 | public JsonElementComparison NullComparison { get; set; } = JsonElementComparison.Exact; 57 | 58 | /// 59 | /// Gets or sets the . 60 | /// 61 | /// Defaults to where not specified. 62 | public IJsonSerializer? JsonSerializer { get; set; } 63 | 64 | /// 65 | /// Clones the . 66 | /// 67 | /// A new (cloned) instance. 68 | public JsonElementComparerOptions Clone() => new() 69 | { 70 | PathComparer = PathComparer, 71 | PropertyNameComparer = PropertyNameComparer, 72 | MaxDifferences = MaxDifferences, 73 | ValueComparison = ValueComparison, 74 | NullComparison = NullComparison, 75 | JsonSerializer = JsonSerializer 76 | }; 77 | } 78 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonElementComparerResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Text.Json; 7 | 8 | namespace UnitTestEx.Json 9 | { 10 | /// 11 | /// Represents the result of a . 12 | /// 13 | public sealed class JsonElementComparerResult 14 | { 15 | private List? _differences; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The left . 21 | /// The right . 22 | /// The maximum number of differences detect. 23 | internal JsonElementComparerResult(JsonElement left, JsonElement right, int maxDifferences) 24 | { 25 | Left = left; 26 | Right = right; 27 | MaxDifferences = maxDifferences; 28 | } 29 | 30 | /// 31 | /// Gets the left . 32 | /// 33 | public JsonElement Left { get; } 34 | 35 | /// 36 | /// Gets the right . 37 | /// 38 | public JsonElement Right { get; } 39 | 40 | /// 41 | /// Gets the maximum number of differences to detect. 42 | /// 43 | public int MaxDifferences { get; } 44 | 45 | /// 46 | /// Indicates whether the two JSON elements are considered equal. 47 | /// 48 | public bool AreEqual => DifferenceCount == 0; 49 | 50 | /// 51 | /// Indicates whether there are any differences between the two JSON elements based on the specified criteria. 52 | /// 53 | public bool HasDifferences => DifferenceCount != 0; 54 | 55 | /// 56 | /// Gets the current number of differences detected. 57 | /// 58 | public int DifferenceCount => _differences?.Count ?? 0; 59 | 60 | /// 61 | /// Indicates whether the maximum number of differences specified to detect has been found. 62 | /// 63 | public bool IsMaxDifferencesFound => DifferenceCount >= MaxDifferences; 64 | 65 | /// 66 | /// Indicates whether to always replace all array items (where at least one item has changed). 67 | /// 68 | /// The formal specification explictly states that an is to be a replacement operation. 69 | public bool AlwaysReplaceAllArrayItems { get; } 70 | 71 | /// 72 | /// Gets the array. 73 | /// 74 | /// The differences found up to the specified. 75 | public JsonElementDifference[] GetDifferences() => _differences is null ? [] : _differences.ToArray(); 76 | 77 | /// 78 | /// Adds a . 79 | /// 80 | /// The . 81 | internal void AddDifference(JsonElementDifference difference) => (_differences ??= []).Add(difference); 82 | 83 | /// 84 | public override string ToString() 85 | { 86 | if (AreEqual) 87 | return "No differences detected."; 88 | 89 | var sb = new StringBuilder(); 90 | foreach (var d in _differences!) 91 | { 92 | if (sb.Length > 0) 93 | sb.AppendLine(); 94 | 95 | sb.Append(d.ToString()); 96 | } 97 | 98 | if (IsMaxDifferencesFound) 99 | { 100 | sb.AppendLine(); 101 | sb.Append($"Maximum difference count of '{MaxDifferences}' found; comparison stopped."); 102 | } 103 | 104 | return sb.ToString(); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonElementComparison.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System.Text.Json; 4 | 5 | namespace UnitTestEx.Json 6 | { 7 | /// 8 | /// Defines the comparison option where is either a or . 9 | /// 10 | public enum JsonElementComparison 11 | { 12 | /// 13 | /// Indicates that a semantic match is to used for the comparison. 14 | /// 15 | Semantic, 16 | 17 | /// 18 | /// Indicates that an exact match is to used for the comparison. 19 | /// 20 | /// Uses the for the value comparison. 21 | Exact 22 | } 23 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonElementDifference.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System.Text.Json; 4 | 5 | namespace UnitTestEx.Json 6 | { 7 | /// 8 | /// Represents a comparison difference. 9 | /// 10 | public readonly struct JsonElementDifference 11 | { 12 | /// 13 | /// Initializes a the struct. 14 | /// 15 | /// The JSON path. 16 | /// The left where applicable. 17 | /// The right where applicable. 18 | /// The . 19 | internal JsonElementDifference(string path, JsonElement? left, JsonElement? right, JsonElementDifferenceType type) 20 | { 21 | Path = path; 22 | Left = left; 23 | Right = right; 24 | Type = type; 25 | } 26 | 27 | /// 28 | /// Gets the JSON path. 29 | /// 30 | public string Path { get; } 31 | 32 | /// 33 | /// Gets the left where applicable. 34 | /// 35 | public JsonElement? Left { get; } 36 | 37 | /// 38 | /// Gets the right where applicable. 39 | /// 40 | public JsonElement? Right { get; } 41 | 42 | /// 43 | /// Gets the . 44 | /// 45 | public JsonElementDifferenceType Type { get; } 46 | 47 | /// 48 | public override string ToString() => $"Path '{Path}': {Type switch 49 | { 50 | JsonElementDifferenceType.LeftNone => "Does not exist in left JSON.", 51 | JsonElementDifferenceType.RightNone => "Does not exist in right JSON.", 52 | JsonElementDifferenceType.ArrayLength => $"Array lengths are not equal: {Left?.GetArrayLength()} != {Right?.GetArrayLength()}.", 53 | JsonElementDifferenceType.Kind => $"Kind is not equal: {Left?.ValueKind} != {Right?.ValueKind}.", 54 | _ => $"Value is not equal: {Left?.GetRawText()} != {Right?.GetRawText()}.", 55 | }}"; 56 | } 57 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonElementDifferenceType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System.Text.Json; 4 | 5 | namespace UnitTestEx.Json 6 | { 7 | /// 8 | /// Defines the type of difference identified. 9 | /// 10 | public enum JsonElementDifferenceType 11 | { 12 | /// 13 | /// Indicates that the left and right is different. 14 | /// 15 | Kind, 16 | 17 | /// 18 | /// Indicates that the left and right values are different. 19 | /// 20 | Value, 21 | 22 | /// 23 | /// Indicates that the corresponding path does not exist in the left . 24 | /// 25 | LeftNone, 26 | 27 | /// 28 | /// Indicates that the corresponding path does not exist in the right . 29 | /// 30 | RightNone, 31 | 32 | /// 33 | /// Indicates that the left and right array lengths are different. 34 | /// 35 | ArrayLength 36 | } 37 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System.Collections.Generic; 4 | using System.Text.Json; 5 | 6 | namespace UnitTestEx.Json 7 | { 8 | /// 9 | /// Provides JSON extension methods. 10 | /// 11 | public static class JsonExtensions 12 | { 13 | /// 14 | /// Trys to get the with the and . 15 | /// 16 | /// The . 17 | /// The property name. 18 | /// The . 19 | /// The named where found. 20 | /// true indicates the property was found; otherwise, false. 21 | public static bool TryGetProperty(this JsonElement json, string propertyName, IEqualityComparer comparer, out JsonElement value) 22 | { 23 | foreach (var j in json.EnumerateObject()) 24 | { 25 | if (comparer.Equals(j.Name, propertyName)) 26 | { 27 | value = j.Value; 28 | return true; 29 | } 30 | } 31 | 32 | value = default; 33 | return false; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonSerializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | using Stj = System.Text.Json; 5 | 6 | namespace UnitTestEx.Json 7 | { 8 | /// 9 | /// Provides the encapsulated implementation. 10 | /// 11 | public class JsonSerializer : IJsonSerializer 12 | { 13 | private static IJsonSerializer? _default; 14 | 15 | /// 16 | /// Gets or sets the default . 17 | /// 18 | public static Stj.JsonSerializerOptions DefaultOptions { get; set; } = new Stj.JsonSerializerOptions(Stj.JsonSerializerDefaults.Web) 19 | { 20 | DefaultIgnoreCondition = Stj.Serialization.JsonIgnoreCondition.WhenWritingDefault, 21 | WriteIndented = false 22 | }; 23 | 24 | /// 25 | /// Gets or sets the default . 26 | /// 27 | public static IJsonSerializer Default 28 | { 29 | get => _default ??= new JsonSerializer(); 30 | set => _default = value ?? throw new ArgumentNullException(nameof(value)); 31 | } 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// 36 | /// The . Defaults to . 37 | public JsonSerializer(Stj.JsonSerializerOptions? options = null) 38 | { 39 | Options = options ?? DefaultOptions; 40 | IndentedOptions = new Stj.JsonSerializerOptions(Options) { WriteIndented = true }; 41 | } 42 | 43 | /// 44 | object IJsonSerializer.Options => Options; 45 | 46 | /// 47 | /// Gets the . 48 | /// 49 | public Stj.JsonSerializerOptions Options { get; } 50 | 51 | /// 52 | /// Gets or sets the with = true. 53 | /// 54 | public Stj.JsonSerializerOptions? IndentedOptions { get; } 55 | 56 | /// 57 | public object? Deserialize(string json) => Stj.JsonSerializer.Deserialize(json, Options); 58 | 59 | /// 60 | public object? Deserialize(string json, Type type) => Stj.JsonSerializer.Deserialize(json, type, Options); 61 | 62 | /// 63 | public T? Deserialize(string json) => Stj.JsonSerializer.Deserialize(json, Options)!; 64 | 65 | /// 66 | public string Serialize(T value, JsonWriteFormat? format = null) => Stj.JsonSerializer.Serialize(value, format == null || format.Value == JsonWriteFormat.None ? Options : IndentedOptions); 67 | } 68 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Json/JsonWriteFormat.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace UnitTestEx.Json 4 | { 5 | /// 6 | /// Defines the JSON serialization write format. 7 | /// 8 | public enum JsonWriteFormat 9 | { 10 | /// 11 | /// Indicates that no formatting is to occur. 12 | /// 13 | None, 14 | 15 | /// 16 | /// Indicates that pretty-printing indented formatting is to occur. 17 | /// 18 | Indented 19 | } 20 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Logging/SharedStateLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using UnitTestEx.Abstractions; 6 | 7 | namespace UnitTestEx.Logging 8 | { 9 | /// 10 | /// Represents the provider. 11 | /// 12 | /// The . 13 | [ProviderAlias("")] 14 | public sealed class SharedStateLoggerProvider(TestSharedState sharedState) : ILoggerProvider, ISupportExternalScope 15 | { 16 | private IExternalScopeProvider? _scopeProvider; 17 | private readonly TestSharedState _sharedState = sharedState ?? throw new ArgumentNullException(nameof(sharedState)); 18 | 19 | /// 20 | /// Creates a new instance of the . 21 | /// 22 | /// The name of the logger. 23 | /// The . 24 | public ILogger CreateLogger(string name) => new SharedStateLogger(_sharedState, name, _scopeProvider); 25 | 26 | /// 27 | /// Closes and disposes the . 28 | /// 29 | public void Dispose() { } 30 | 31 | /// 32 | public void SetScopeProvider(IExternalScopeProvider scopeProvider) => _scopeProvider = scopeProvider; 33 | } 34 | 35 | /// 36 | /// Provides an that writes to the . 37 | /// 38 | /// The . 39 | /// The name of the logger. 40 | /// The . 41 | public class SharedStateLogger(TestSharedState sharedState, string name, IExternalScopeProvider? scopeProvider = null) : LoggerBase(name, scopeProvider) 42 | { 43 | private readonly TestSharedState _sharedState = sharedState ?? throw new ArgumentNullException(nameof(sharedState)); 44 | 45 | /// 46 | protected override void WriteMessage(string message) => _sharedState.AddLoggerMessage(message); 47 | } 48 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Logging/TestFrameworkImplementerLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using UnitTestEx.Abstractions; 6 | 7 | namespace UnitTestEx.Logging 8 | { 9 | /// 10 | /// Represents the provider. 11 | /// 12 | /// The . 13 | [ProviderAlias("")] 14 | public sealed class TestFrameworkImplementorLoggerProvider(TestFrameworkImplementor testFrameworkImplementor) : ILoggerProvider, ISupportExternalScope 15 | { 16 | private IExternalScopeProvider? _scopeProvider; 17 | private readonly TestFrameworkImplementor _testFrameworkImplementor = testFrameworkImplementor ?? throw new ArgumentNullException(nameof(testFrameworkImplementor)); 18 | 19 | /// 20 | /// Creates a new instance of the . 21 | /// 22 | /// The name of the logger. 23 | /// The . 24 | public ILogger CreateLogger(string name) => new TestFrameworkImplementorLogger(_testFrameworkImplementor, name, _scopeProvider); 25 | 26 | /// 27 | /// Closes and disposes the . 28 | /// 29 | public void Dispose() { } 30 | 31 | /// 32 | public void SetScopeProvider(IExternalScopeProvider scopeProvider) => _scopeProvider = scopeProvider; 33 | } 34 | 35 | /// 36 | /// Provides an that writes to the . 37 | /// 38 | /// The . 39 | /// The name of the logger. 40 | /// The . 41 | public class TestFrameworkImplementorLogger(TestFrameworkImplementor testFrameworkImplementor, string name, IExternalScopeProvider? scopeProvider = null) : LoggerBase(name, scopeProvider) 42 | { 43 | private readonly TestFrameworkImplementor _testFrameworkImplementor = testFrameworkImplementor ?? throw new ArgumentNullException(nameof(testFrameworkImplementor)); 44 | 45 | /// 46 | protected override void WriteMessage(string message) => _testFrameworkImplementor.WriteLine(message); 47 | } 48 | } -------------------------------------------------------------------------------- /src/UnitTestEx/MockHttpClientException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using System; 4 | 5 | namespace UnitTestEx 6 | { 7 | /// 8 | /// Represents a runtime exception. 9 | /// 10 | public class MockHttpClientException : Exception 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | public MockHttpClientException() : this(null!) { } 16 | 17 | /// 18 | /// Initializes a new instance of the class with a specified messsage. 19 | /// 20 | /// The message text. 21 | public MockHttpClientException(string? message) : base(message) { } 22 | 23 | /// 24 | /// Initializes a new instance of the class with a specified messsage and inner exception. 25 | /// 26 | /// The message text. 27 | /// The inner . 28 | public MockHttpClientException(string? message, Exception innerException) : base(message, innerException) { } 29 | } 30 | } -------------------------------------------------------------------------------- /src/UnitTestEx/MockHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using UnitTestEx.Abstractions; 4 | 5 | namespace UnitTestEx 6 | { 7 | /// 8 | /// Provides the NUnit implementation. 9 | /// 10 | public static class MockHttpClientFactory 11 | { 12 | /// 13 | /// Creates the . 14 | /// 15 | /// The . 16 | public static Mocking.MockHttpClientFactory Create() => new(TestFrameworkImplementor.Create()); 17 | } 18 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Mocking/MockHttpClientHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Microsoft.Extensions.Logging; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace UnitTestEx.Mocking 9 | { 10 | /// 11 | /// Provides a that will log success and failure. 12 | /// 13 | public class MockHttpClientHandler : DelegatingHandler 14 | { 15 | private readonly MockHttpClientFactory _factory; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The . 21 | /// The . 22 | internal MockHttpClientHandler(MockHttpClientFactory factory, HttpMessageHandler inner) : base(inner) => _factory = factory; 23 | 24 | /// 25 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 26 | { 27 | var logger = _factory.Logger ?? _factory.Implementor.CreateLoggerProvider().CreateLogger(nameof(MockHttpClientFactory)); 28 | logger.LogDebug($"UnitTestEx > Sending HTTP request {request.Method} {request.RequestUri} {LogContent(request.Content)}"); 29 | 30 | var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false) ?? throw new MockHttpClientException($"No corresponding MockHttpClient response found for HTTP request {request.Method} {request.RequestUri} {LogContent(request.Content)}"); 31 | 32 | logger.LogDebug($"UnitTestEx > Received HTTP response {response.StatusCode} ({(int)response.StatusCode}) {LogContent(response.Content)}"); 33 | return response; 34 | } 35 | 36 | /// 37 | /// Logs (and formats) the content. 38 | /// 39 | private static string LogContent(HttpContent? content) => content == null ? "No content." : $"{content.ReadAsStringAsync().GetAwaiter().GetResult()} ({content?.Headers?.ContentType?.MediaType ?? "?"})"; 40 | } 41 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Mocking/MockHttpClientRequestBody.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace UnitTestEx.Mocking 4 | { 5 | /// 6 | /// Represents the result of adding a body to the and to accordingly. 7 | /// 8 | public sealed class MockHttpClientRequestBody 9 | { 10 | private readonly MockHttpClientRequestRule _rule; 11 | 12 | /// 13 | /// Initializes a new . 14 | /// 15 | /// The . 16 | internal MockHttpClientRequestBody(MockHttpClientRequestRule rule) => _rule = rule; 17 | 18 | /// 19 | /// Gets the . 20 | /// 21 | public MockHttpClientResponse Respond => _rule.Response!; 22 | } 23 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | using Moq; 4 | using System.Collections.Generic; 5 | 6 | namespace UnitTestEx.Mocking 7 | { 8 | /// 9 | /// Represents a rule for managing the . 10 | /// 11 | internal class MockHttpClientRequestRule 12 | { 13 | /// 14 | /// Gets or sets the . 15 | /// 16 | internal MockHttpClientRequestBody? Body { get; set; } 17 | 18 | /// 19 | /// Gets or sets the primary . 20 | /// 21 | internal MockHttpClientResponse? Response { get; set; } 22 | 23 | /// 24 | /// Gets the sequence collection. 25 | /// 26 | internal List? Responses { get; set; } 27 | 28 | /// 29 | /// Gets or sets the sequence index for the . 30 | /// 31 | internal int ResponsesIndex { get; set; } 32 | 33 | /// 34 | /// Gets or sets the number of . 35 | /// 36 | internal Times? Times { get; set; } 37 | } 38 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Mocking/MockHttpClientResponseSequence.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace UnitTestEx.Mocking 4 | { 5 | /// 6 | /// Mocks the within a sequence. 7 | /// 8 | public sealed class MockHttpClientResponseSequence 9 | { 10 | private readonly MockHttpClientRequest _clientRequest; 11 | private readonly MockHttpClientRequestRule _rule; 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The . 17 | /// The . 18 | internal MockHttpClientResponseSequence(MockHttpClientRequest clientRequest, MockHttpClientRequestRule rule) 19 | { 20 | _clientRequest = clientRequest; 21 | _rule = rule; 22 | } 23 | 24 | /// 25 | /// Adds the next in sequence. 26 | /// 27 | /// The next in sequence. 28 | public MockHttpClientResponse Respond() 29 | { 30 | var resp = new MockHttpClientResponse(_clientRequest, null); 31 | _rule.Responses!.Add(resp); 32 | return resp; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/UnitTestEx/Schema/mock.unittestex.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "JSON Schema for UnitTestEx HTTP request/response mocking (https://github.com/Avanade/unittestex).", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "type": "array", 5 | "definitions": { 6 | "MockRequest": { 7 | "type": "object", 8 | "title": "The mocked HTTP request configuration.", 9 | "properties": { 10 | "method": { 11 | "type": "string", 12 | "title": "The HTTP Request Method; defaults to 'GET'." 13 | }, 14 | "uri": { 15 | "type": "string", 16 | "title": "The HTTP Request URI (path and query)." 17 | }, 18 | "body": { 19 | "type": "string", 20 | "title": "The HTTP Request Body (content)." 21 | }, 22 | "media": { 23 | "type": "string", 24 | "title": "The HTTP Request Body (content) media type.", 25 | "description": "Defaults to 'application/json' where the Body contains JSON; otherwise, 'text/plain'." 26 | }, 27 | "ignore": { 28 | "type": "array", 29 | "title": "The HTTP Request Body JSON paths to ignore from the comparison.", 30 | "description": "This is only applied where the 'media' is 'application/json'.", 31 | "items": { 32 | "type": "string" 33 | } 34 | }, 35 | "response": { 36 | "title": "The mocked HTTP response (singular) configuration", 37 | "description": "The 'response' and 'sequence' are mutually exclusive.", 38 | "$ref": "#/definitions/MockResponse" 39 | }, 40 | "sequence": { 41 | "type": "array", 42 | "title": "The mocked HTTP responses array (sequence) configuration.", 43 | "description": "The 'response' and 'sequence' are mutually exclusive.", 44 | "items": { 45 | "$ref": "#/definitions/MockResponse" 46 | } 47 | } 48 | }, 49 | "not": { 50 | "anyOf": [ 51 | { "required": [ "response", "sequence" ] } 52 | ] 53 | } 54 | }, 55 | "MockResponse": { 56 | "title": "The mocked HTTP response configuration.", 57 | "type": "object", 58 | "properties": { 59 | "status": { 60 | "type": "integer", 61 | "title": "The HTTP Response status code.", 62 | "description": "Defaults to 200-OK where there is a corresponding Body; otherwise, 204-No content." 63 | }, 64 | "headers": { 65 | "$ref": "#/definitions/MockResponseHeaders", 66 | "title": "The HTTP Response headers configuration." 67 | }, 68 | "body": { 69 | "type": "string", 70 | "title": "The HTTP Response body (content)." 71 | }, 72 | "media": { 73 | "type": "string", 74 | "title": "The HTTP Request Body (content) media type.", 75 | "description": "Defaults to 'application/json' where the Body contains JSON; otherwise, 'text/plain'." 76 | } 77 | } 78 | }, 79 | "MockResponseHeaders": { 80 | "title": "The HTTP Response headers configuration.", 81 | "type": "object", 82 | "additionalProperties": { 83 | "type": "array", 84 | "items": { 85 | "type": "string" 86 | } 87 | } 88 | }, 89 | "items": { 90 | "$ref": "#/definitions/MockRequest" 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/UnitTestEx/SystemExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx 2 | 3 | namespace System 4 | { 5 | /// 6 | /// Adds and test-oriented extension methods. 7 | /// 8 | public static class SystemExtensionMethods 9 | { 10 | /// 11 | /// Converts an to a ; e.g. '1' will be '00000001-0000-0000-0000-000000000000'. 12 | /// 13 | /// The value. 14 | /// The corresponding . 15 | /// Sets the first argument with the and the remainder with zeroes. 16 | public static Guid ToGuid(this int value) => new(value, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); 17 | 18 | /// 19 | /// Creates a long string by repeating the character for the specified count (defaults to 250). 20 | /// 21 | /// The character value. 22 | /// The repeating count. Defaults to 250. 23 | /// The resulting string. 24 | public static string ToLongString(this char value, int count = 250) => new(value, count); 25 | } 26 | } -------------------------------------------------------------------------------- /src/UnitTestEx/UnitTestEx.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | UnitTestEx 6 | UnitTestEx 7 | UnitTestEx Test Extensions. 8 | UnitTestEx Test Extensions. 9 | unittestex api function unit test unittest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/UnitTestEx/strong-name-key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avanade/UnitTestEx/d80ff2987e1f9aab282aa1c80638ba6015fbff0a/src/UnitTestEx/strong-name-key.snk -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/Controllers/PersonController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.ModelBinding; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using UnitTestEx.Api.Models; 8 | 9 | namespace UnitTestEx.Api.Controllers 10 | { 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class PersonController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private readonly IConfiguration _config; 17 | 18 | public PersonController(ILogger logger, IConfiguration config) 19 | { 20 | _logger = logger; 21 | _config = config; 22 | } 23 | 24 | [HttpGet("{id}")] 25 | public IActionResult Get(int id) 26 | { 27 | _logger.LogInformation("{Content}", $"Get using identifier {id}."); 28 | 29 | if (_config["SpecialKey"] != "VerySpecialValue") 30 | throw new InvalidOperationException("The people do not feel very special!"); 31 | 32 | if (id == 1) 33 | return new JsonResult(new Person { Id = 1, FirstName = "Bob", LastName = "Smith" }); 34 | else if (id == 2) 35 | return new ObjectResult(new Person { Id = 2, FirstName = "Jane", LastName = "Jones" }); 36 | else if (id == 3) 37 | return new ObjectResult(new Person { Id = 3, FirstName = "Brad", LastName = "Davies" }); 38 | else if (id == 88) 39 | return BadRequest("No can do eighty-eight."); 40 | else 41 | return new NotFoundResult(); 42 | } 43 | 44 | [HttpGet("")] 45 | public IActionResult GetByArgs(string firstName, string lastName, [FromQuery] List id = default) 46 | { 47 | return new ObjectResult($"{firstName}-{lastName}-{(id is null ? "" : string.Join(",", id))}"); 48 | } 49 | 50 | [HttpPost("{id}")] 51 | public IActionResult Update(int id, [FromBody] Person person) 52 | { 53 | if (id == 88) 54 | return BadRequest("No can do eighty-eight."); 55 | 56 | if (id == 99) 57 | return new NotFoundResult(); 58 | 59 | var msd = new ModelStateDictionary(); 60 | 61 | if (string.IsNullOrEmpty(person.FirstName)) 62 | msd.AddModelError("firstName", "First name is required."); 63 | 64 | if (string.IsNullOrEmpty(person.LastName)) 65 | msd.AddModelError("lastName", "Last name is required."); 66 | 67 | if (!msd.IsValid) 68 | return new BadRequestObjectResult(msd); 69 | 70 | _logger.LogInformation("{message}", $"Person {id} is being updated."); 71 | person.Id = id; 72 | return new JsonResult(person); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/Controllers/ProductController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | 9 | namespace UnitTestEx.Api.Controllers 10 | { 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class ProductController : ControllerBase 14 | { 15 | private readonly HttpClient _httpClient; 16 | private readonly HttpClient _defaultHttpClient; 17 | 18 | public ProductController(IHttpClientFactory clientFactory) 19 | { 20 | _httpClient = clientFactory.CreateClient("XXX"); 21 | _defaultHttpClient = clientFactory.CreateClient(); 22 | } 23 | 24 | [HttpGet("{id}")] 25 | public async Task Get(string id) 26 | { 27 | var result = await _httpClient.GetAsync($"products/{id}").ConfigureAwait(false); 28 | if (result.StatusCode == HttpStatusCode.NotFound) 29 | return new NotFoundResult(); 30 | 31 | result.EnsureSuccessStatusCode(); 32 | 33 | var str = await result.Content.ReadAsStringAsync().ConfigureAwait(false); 34 | var val = JsonConvert.DeserializeObject(str); 35 | 36 | return new OkObjectResult(val); 37 | } 38 | 39 | [HttpGet("")] 40 | public async Task Get() 41 | { 42 | var result = await _defaultHttpClient.GetAsync($"products/default").ConfigureAwait(false); 43 | if (result.StatusCode == HttpStatusCode.NotFound) 44 | return new NotFoundResult(); 45 | 46 | result.EnsureSuccessStatusCode(); 47 | var str = await result.Content.ReadAsStringAsync().ConfigureAwait(false); 48 | var val = JsonConvert.DeserializeObject(str); 49 | return new OkObjectResult(val); 50 | } 51 | 52 | [HttpGet("test/created")] 53 | public Task GetCreated() 54 | { 55 | return Task.FromResult((IActionResult)new CreatedResult("bananas", "abc")); 56 | } 57 | 58 | [HttpGet("test/ok")] 59 | public Task GetOK() 60 | { 61 | return Task.FromResult((IActionResult)new OkResult()); 62 | } 63 | 64 | [HttpGet("test/problem")] 65 | public Task GetProblem() 66 | { 67 | return Task.FromResult((IActionResult)new JsonResult(new HttpValidationProblemDetails(new Dictionary { { "id", ["Not specified."] } } ))); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace UnitTestEx.Api.Controllers 4 | { 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class TestController : ControllerBase 8 | { 9 | private int _state; 10 | 11 | public TestController Add(int value) 12 | { 13 | _state += value; 14 | return this; 15 | } 16 | 17 | [HttpGet()] 18 | public int Get() 19 | { 20 | return _state; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace UnitTestEx.Api.Models 5 | { 6 | public class Person 7 | { 8 | [JsonProperty("id")] 9 | [JsonPropertyName("id")] 10 | public int Id { get; set; } 11 | 12 | [JsonProperty("firstName")] 13 | [JsonPropertyName("firstName")] 14 | public string FirstName { get; set; } 15 | 16 | [JsonProperty("lastName")] 17 | [JsonPropertyName("lastName")] 18 | public string LastName { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace UnitTestEx.Api 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => 17 | { 18 | webBuilder.UseStartup(); 19 | }) 20 | .ConfigureAppConfiguration((ctx, builder) => 21 | { 22 | if (builder.Build()["SpecialKey"] != "VerySpecialValue") 23 | throw new InvalidOperationException("The people do not feel very special!"); 24 | }); 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using System; 7 | using System.Net.Http; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace UnitTestEx.Api 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | // This method gets called by the runtime. Use this method to add services to the container. 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddControllers().AddNewtonsoftJson(); 26 | services.AddHttpClient("XXX", hc => hc.BaseAddress = new System.Uri("https://somesys")) 27 | .AddHttpMessageHandler(_ => new MessageProcessingHandler()) 28 | .ConfigureHttpClient(hc => hc.DefaultRequestVersion = new Version(1, 2)); 29 | } 30 | 31 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 32 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 33 | { 34 | if (env.IsDevelopment()) 35 | { 36 | app.UseDeveloperExceptionPage(); 37 | } 38 | 39 | app.UseHttpsRedirection(); 40 | 41 | app.UseRouting(); 42 | 43 | app.UseAuthorization(); 44 | 45 | app.UseEndpoints(endpoints => 46 | { 47 | endpoints.MapControllers(); 48 | }); 49 | } 50 | 51 | public class MessageProcessingHandler : DelegatingHandler 52 | { 53 | public static bool WasExecuted { get; set;} 54 | 55 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 56 | { 57 | WasExecuted = true; 58 | return base.SendAsync(request, cancellationToken); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/UnitTestEx.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0 5 | true 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "SpecialKey": "SpecialValue", 10 | "OtherKey": "OtherValue" 11 | } 12 | -------------------------------------------------------------------------------- /tests/UnitTestEx.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "SpecialKey": "NotVerySpecialValue", 11 | "OtherKey": "NotOtherValue" 12 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/PersonFunction.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.Http; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.AspNetCore.Mvc.ModelBinding; 10 | using System.Net.Mime; 11 | using System.Text.Json; 12 | using System.Text.Json.Serialization; 13 | using System.Web.Http; 14 | 15 | namespace UnitTestEx.Function 16 | { 17 | public class PersonFunction 18 | { 19 | [FunctionName("PersonFunction")] 20 | public async Task Run( 21 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log) 22 | { 23 | log.LogInformation("C# HTTP trigger function processed a request."); 24 | 25 | string name = req.Query["name"]; 26 | 27 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 28 | var data = string.IsNullOrEmpty(requestBody) ? null : JsonSerializer.Deserialize(requestBody); 29 | name ??= data?.Name; 30 | 31 | if (name == "Brian") 32 | { 33 | var msd = new ModelStateDictionary(); 34 | msd.AddModelError("name", "Name cannot be Brian."); 35 | return new BadRequestObjectResult(msd); 36 | } 37 | 38 | if (name == "Rachel") 39 | { 40 | var obj = new { FirstName = "Rachel", LastName = "Smith" }; 41 | return new JsonResult(obj); 42 | } 43 | 44 | if (name == "Damien") 45 | return new BadRequestErrorMessageResult("Name cannot be Damien."); 46 | 47 | if (name == "Bruce") 48 | return new ContentResult { Content = "Name cannot be Bruce.", StatusCode = 400 }; 49 | 50 | string responseMessage = string.IsNullOrEmpty(name) 51 | ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." 52 | : $"Hello, {name}. This HTTP triggered function executed successfully."; 53 | 54 | return new OkObjectResult(responseMessage); 55 | } 56 | 57 | [FunctionName("PersonFunctionObj")] 58 | public async Task RunWithValue([HttpTrigger(AuthorizationLevel.Function, "post", Route = "people/{name}")] Person person, ILogger log) 59 | { 60 | await Task.CompletedTask.ConfigureAwait(false); 61 | log.LogInformation("C# HTTP trigger function processed a request."); 62 | return new OkObjectResult(new { first = person.FirstName, last = person.LastName }); 63 | } 64 | 65 | [FunctionName("PersonFunctionContent")] 66 | public async Task RunWithContent([HttpTrigger(AuthorizationLevel.Function, "post", Route = "people/content/{name}")] Person person, ILogger log) 67 | { 68 | await Task.CompletedTask.ConfigureAwait(false); 69 | log.LogInformation("C# HTTP trigger function processed a request."); 70 | return new ContentResult { Content = JsonSerializer.Serialize(new { first = person.FirstName, last = person.LastName }), ContentType = MediaTypeNames.Application.Json, StatusCode = 200 }; 71 | } 72 | 73 | [FunctionName("PersonFunctionQuery")] 74 | public async Task RunWithQuery([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/people?name={name}")] HttpRequest request, string name, ILogger log) 75 | { 76 | await Task.CompletedTask.ConfigureAwait(false); 77 | log.LogInformation("C# HTTP trigger function processed a request."); 78 | return new OkObjectResult(new { name }); 79 | } 80 | } 81 | 82 | public class Namer 83 | { 84 | [JsonPropertyName("name")] 85 | public string Name { get; set; } 86 | } 87 | 88 | public class Person 89 | { 90 | [JsonPropertyName("firstName")] 91 | public string FirstName { get; set; } 92 | 93 | [JsonPropertyName("lastName")] 94 | public string LastName { get; set; } 95 | } 96 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/ProductFunction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.Extensions.Logging; 6 | using Newtonsoft.Json.Linq; 7 | using System; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | 12 | namespace UnitTestEx.Function 13 | { 14 | public class ProductFunction 15 | { 16 | private readonly HttpClient _httpClient; 17 | private readonly ILogger _logger; 18 | 19 | public ProductFunction(IHttpClientFactory clientFactory, ILogger logger) 20 | { 21 | _httpClient = clientFactory.CreateClient("XXX"); 22 | _logger = logger; 23 | } 24 | 25 | [FunctionName("ProductFunction")] 26 | public async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = "product/{id}")] HttpRequest _, string id, ILogger log) 27 | => await Logic(id, log).ConfigureAwait(false); 28 | 29 | private async Task Logic(string id, ILogger log) 30 | { 31 | log.LogInformation("C# HTTP trigger function processed a request."); 32 | 33 | if (id == "exception") 34 | throw new InvalidOperationException("An unexpected exception occured."); 35 | 36 | var result = await _httpClient.GetAsync($"products/{id}").ConfigureAwait(false); 37 | if (result.StatusCode == HttpStatusCode.NotFound) 38 | return new NotFoundResult(); 39 | 40 | result.EnsureSuccessStatusCode(); 41 | 42 | var str = await result.Content.ReadAsStringAsync().ConfigureAwait(false); 43 | JObject val = JObject.Parse(str); 44 | 45 | return new JsonResult(new { id = val["id"].Value(), description = val["description"].Value() }); 46 | } 47 | 48 | [FunctionName("TimerTriggered")] 49 | public Task DailyRun([TimerTrigger("0 0 0 */1 * *", RunOnStartup = true)] TimerInfo _, ILogger otherLogger) 50 | { 51 | _logger.LogInformation("C# Timer trigger function executed (DI)."); 52 | otherLogger.LogInformation("C# Timer trigger function executed (method)."); 53 | return Task.CompletedTask; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/ServiceBusFunction.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | using Microsoft.Azure.WebJobs; 3 | using Microsoft.Azure.WebJobs.ServiceBus; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Net.Http; 7 | using System.Net.Http.Formatting; 8 | using System.Net.Http.Json; 9 | using System.Text.Json; 10 | using System.Threading.Tasks; 11 | 12 | namespace UnitTestEx.Function 13 | { 14 | public class ServiceBusFunction 15 | { 16 | private readonly HttpClient _httpClient; 17 | 18 | public ServiceBusFunction(IHttpClientFactory clientFactory) 19 | { 20 | _httpClient = clientFactory.CreateClient("XXX"); 21 | } 22 | 23 | [FunctionName("ServiceBusFunction")] 24 | public async Task Run([ServiceBusTrigger("unittestex", Connection = "ServiceBusConnectionString")] Person p, ILogger log) 25 | { 26 | log.LogInformation($"C# ServiceBus queue trigger function processed message: {p.FirstName} {p.LastName}"); 27 | 28 | if (p.FirstName == null) 29 | throw new InvalidOperationException("First name is required."); 30 | 31 | var resp = await _httpClient.PostAsJsonAsync($"person", p, new JsonSerializerOptions(JsonSerializerDefaults.Web)).ConfigureAwait(false); 32 | 33 | resp.EnsureSuccessStatusCode(); 34 | } 35 | 36 | [FunctionName("ServiceBusFunction2")] 37 | public async Task Run2([ServiceBusTrigger("unittestex", Connection = "ServiceBusConnectionString")] ServiceBusReceivedMessage message, ILogger log) 38 | { 39 | var p = message.Body.ToObjectFromJson(); 40 | log.LogInformation($"C# ServiceBus queue trigger function processed message: {p.FirstName} {p.LastName}"); 41 | 42 | if (p.FirstName == null) 43 | throw new InvalidOperationException("First name is required."); 44 | 45 | var resp = await _httpClient.PostAsJsonAsync($"person", p, new JsonSerializerOptions(JsonSerializerDefaults.Web)).ConfigureAwait(false); 46 | 47 | resp.EnsureSuccessStatusCode(); 48 | } 49 | 50 | [FunctionName("ServiceBusFunction3")] 51 | public async Task Run3([ServiceBusTrigger("unittestex", Connection = "ServiceBusConnectionString", AutoCompleteMessages = false)] ServiceBusReceivedMessage message, ServiceBusMessageActions messageActions, ILogger log) 52 | { 53 | var p = message.Body.ToObjectFromJson(); 54 | log.LogInformation($"C# ServiceBus queue trigger function processed message: {p.FirstName} {p.LastName}"); 55 | 56 | if (p.FirstName == null) 57 | { 58 | await messageActions.DeadLetterMessageAsync(message, "Validation error.", "First name is required.").ConfigureAwait(false); 59 | log.LogError("First name is required."); 60 | return; 61 | } 62 | 63 | if (p.FirstName == "zerodivision") 64 | throw new DivideByZeroException("Divide by zero is not a thing."); 65 | 66 | var resp = await _httpClient.PostAsJsonAsync($"person", p, new JsonSerializerOptions(JsonSerializerDefaults.Web)).ConfigureAwait(false); 67 | 68 | resp.EnsureSuccessStatusCode(); 69 | 70 | await messageActions.CompleteMessageAsync(message).ConfigureAwait(false); 71 | } 72 | 73 | [FunctionName("ServiceBusFunction4")] 74 | public static Task Run4([ServiceBusTrigger("%Run4QueueName%", Connection = "ServiceBusConnectionString")] Person p, string subject, ILogger log) 75 | { 76 | log.LogInformation($"C# ServiceBus queue trigger function processed {subject} message: {p.FirstName} {p.LastName}"); 77 | 78 | if (p.FirstName == null) 79 | throw new InvalidOperationException("First name is required."); 80 | 81 | return Task.CompletedTask; 82 | } 83 | 84 | [FunctionName("ServiceBusSessionFunction5")] 85 | public static Task Run5([ServiceBusTrigger("unittestexsess", Connection = "ServiceBusConnectionString", IsSessionsEnabled = true)] ServiceBusReceivedMessage _) 86 | { 87 | return Task.CompletedTask; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | [assembly: FunctionsStartup(typeof(UnitTestEx.Function.Startup))] 5 | 6 | namespace UnitTestEx.Function 7 | { 8 | public class Startup : FunctionsStartup 9 | { 10 | public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder) 11 | { 12 | //if (builder.ConfigurationBuilder.Build()["SpecialKey"] != "VerySpecialValue") 13 | // throw new InvalidOperationException("The people do not feel very special!"); 14 | } 15 | 16 | public override void Configure(IFunctionsHostBuilder builder) 17 | { 18 | builder.Services.AddHttpClient("XXX", hc => hc.BaseAddress = new System.Uri("https://somesys")); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/UnitTestEx.Function.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | v4 5 | fff1f13f-b465-40c0-bf17-1f705f473c9e 6 | preview 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | Always 20 | Never 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/UnitTestEx.Function/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/HttpFunction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Azure.Functions.Worker; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace UnitTestEx.IsolatedFunction 7 | { 8 | public class HttpFunction 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public HttpFunction(ILoggerFactory loggerFactory) 13 | { 14 | _logger = loggerFactory.CreateLogger(); 15 | } 16 | 17 | [Function("HttpFunction")] 18 | public async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req) 19 | { 20 | _logger.LogInformation("C# HTTP trigger function processed a request."); 21 | await Task.CompletedTask; 22 | return new OkObjectResult("Welcome to Azure Functions!"); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using UnitTestEx.IsolatedFunction; 3 | 4 | var startup = new Startup(); 5 | var host = new HostBuilder() 6 | .ConfigureFunctionsWebApplication() 7 | .ConfigureServices((hbc, sc) => startup.ConfigureServices(sc)) 8 | .Build(); 9 | 10 | host.Run(); 11 | -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/ServiceBusFunction.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | using Microsoft.Azure.Functions.Worker; 3 | 4 | namespace UnitTestEx.IsolatedFunction 5 | { 6 | public class ServiceBusFunction 7 | { 8 | private readonly HttpClient _httpClient; 9 | 10 | public ServiceBusFunction(IHttpClientFactory clientFactory) 11 | { 12 | _httpClient = clientFactory.CreateClient("XXX"); 13 | } 14 | 15 | [Function("ServiceBusFunction")] 16 | public async Task Run([ServiceBusTrigger("myqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message) 17 | { 18 | var id = message.Body.ToObjectFromJson(); 19 | await DoStuff(id).ConfigureAwait(false); 20 | } 21 | 22 | private async Task DoStuff(int id) 23 | { 24 | var hr = await _httpClient.GetAsync($"products/{id}").ConfigureAwait(false); 25 | hr.EnsureSuccessStatusCode(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace UnitTestEx.IsolatedFunction 4 | { 5 | public class Startup 6 | { 7 | public void ConfigureServices(IServiceCollection services) 8 | { 9 | services.AddHttpClient("XXX", hc => hc.BaseAddress = new System.Uri("https://somesys")); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/TimerFunction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace UnitTestEx.IsolatedFunction 5 | { 6 | public class TimerFunction 7 | { 8 | private readonly HttpClient _httpClient; 9 | private readonly ILogger _logger; 10 | 11 | public TimerFunction(IHttpClientFactory clientFactory, ILogger logger) 12 | { 13 | _httpClient = clientFactory.CreateClient("XXX"); 14 | _logger = logger; 15 | } 16 | 17 | [Function("TimerTriggerFunction")] 18 | public async Task Run([TimerTrigger("0 */5 * * * *" /*, RunOnStartup = true */)] string _) 19 | { 20 | _logger.LogInformation("C# Timer trigger function executed."); 21 | var hr = await _httpClient.GetAsync($"products/123").ConfigureAwait(false); 22 | hr.EnsureSuccessStatusCode(); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/UnitTestEx.IsolatedFunction.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | Never 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/UnitTestEx.IsolatedFunction/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/ExpressionTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using UnitTestEx.Api; 4 | using UnitTestEx.Api.Controllers; 5 | 6 | namespace UnitTestEx.MSTest.Test 7 | { 8 | [TestClass] 9 | public class ExpressionTest 10 | { 11 | [TestMethod] 12 | public void Test_ExpressionRuns() 13 | { 14 | using var test = ApiTester.Create(); 15 | 16 | var ex = Assert.ThrowsException(() => test.Controller().Run(c => c.Add(2).Add(3).Get())); 17 | Assert.IsTrue(ex.Message.StartsWith("UnitTestEx methods that enable an expression must not include method-chaining 'c.Add(2).Add(3).Get()'")); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/Model/Person.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace UnitTestEx.MSTest.Test.Model 5 | { 6 | public class Person 7 | { 8 | [JsonProperty("firstName")] 9 | [JsonPropertyName("firstName")] 10 | public string FirstName { get; set; } 11 | 12 | [JsonProperty("lastName")] 13 | [JsonPropertyName("lastName")] 14 | public string LastName { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/Model/Person2.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace UnitTestEx.MSTest.Test.Model 5 | { 6 | class Person2 7 | { 8 | [JsonProperty("first")] 9 | [JsonPropertyName("first")] 10 | public string First { get; set; } 11 | 12 | [JsonProperty("last")] 13 | [JsonPropertyName("last")] 14 | public string Last { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/Other/GenericTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using UnitTestEx.Expectations; 4 | 5 | namespace UnitTestEx.MSTest.Test.Other 6 | { 7 | [TestClass] 8 | public class GenericTest 9 | { 10 | [TestMethod] 11 | public void Run_Success() 12 | { 13 | using var test = GenericTester.Create(); 14 | test.Run(() => 1) 15 | .AssertSuccess() 16 | .AssertValue(1); 17 | } 18 | 19 | [TestMethod] 20 | public void Run_Exception() 21 | { 22 | using var test = GenericTester.Create(); 23 | test.ExpectError("Badness.") 24 | .Run(() => throw new DivideByZeroException("Badness.")); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/Other/LoggerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | using System.Collections.Generic; 5 | using UnitTestEx.MSTest.Internal; 6 | 7 | namespace UnitTestEx.MSTest.Test.Other 8 | { 9 | [TestClass] 10 | public class LoggerTest 11 | { 12 | [TestMethod] 13 | public void Test() 14 | { 15 | var l = new MSTestImplementor().CreateLoggerProvider().CreateLogger("LoggerTest"); 16 | 17 | var scope = l.BeginScope(new Dictionary() { { "CorrelationId", "abc" }, { "AltCode", 1234 } }); 18 | l.LogInformation("A single line of {Text}.", "text"); 19 | var scope2 = l.BeginScope(new Dictionary() { { "Other", "bananas" } }); 20 | l.LogWarning($"First line of text.{Environment.NewLine}Second line of text.{Environment.NewLine}Third line of text."); 21 | l.LogInformation("A single line of text."); 22 | scope2.Dispose(); 23 | l.LogWarning($"First line of text.{Environment.NewLine}Second line of text.{Environment.NewLine}Third line of text."); 24 | scope.Dispose(); 25 | l.LogInformation("A single line of text."); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/Other/ObjectComparerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using UnitTestEx.MSTest.Test.Model; 4 | 5 | namespace UnitTestEx.MSTest.Test.Other 6 | { 7 | [TestClass] 8 | public class ObjectComparerTest 9 | { 10 | [TestMethod] 11 | public void Test() 12 | { 13 | var p1 = new Person { FirstName = "Wendy", LastName = "Brown" }; 14 | var p2 = new Person { FirstName = "Wendy", LastName = "Brown" }; 15 | ObjectComparer.Assert(p1, p2); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/ProductControllerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | using System.Net; 5 | using System.Net.Http; 6 | using UnitTestEx.Api; 7 | using UnitTestEx.Api.Controllers; 8 | 9 | namespace UnitTestEx.MSTest.Test 10 | { 11 | [TestClass] 12 | public class ProductControllerTest : WithApiTester 13 | { 14 | [TestMethod] 15 | public void Notfound_WithoutConfigurations() 16 | { 17 | var mcf = MockHttpClientFactory.Create(); 18 | mcf.CreateClient("XXX", new Uri("https://somesys")) 19 | .Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound); 20 | 21 | Startup.MessageProcessingHandler.WasExecuted = false; 22 | 23 | Test.ReplaceHttpClientFactory(mcf) 24 | .Controller() 25 | .Run(c => c.Get("xyz")) 26 | .AssertNotFound(); 27 | 28 | Assert.IsFalse(Startup.MessageProcessingHandler.WasExecuted); 29 | } 30 | 31 | [TestMethod] 32 | public void Notfound_WithConfigurations() 33 | { 34 | var mcf = MockHttpClientFactory.Create(); 35 | mcf.CreateClient("XXX").WithConfigurations() 36 | .Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound); 37 | 38 | Startup.MessageProcessingHandler.WasExecuted = false; 39 | 40 | Test.ReplaceHttpClientFactory(mcf) 41 | .Controller() 42 | .Run(c => c.Get("xyz")) 43 | .AssertNotFound(); 44 | 45 | Assert.IsTrue(Startup.MessageProcessingHandler.WasExecuted); 46 | } 47 | 48 | [TestMethod] 49 | public void Success() 50 | { 51 | var mcf = MockHttpClientFactory.Create(); 52 | mcf.CreateClient("XXX", new Uri("https://somesys")) 53 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 54 | 55 | Test.ReplaceHttpClientFactory(mcf) 56 | .Controller() 57 | .Run(c => c.Get("abc")) 58 | .AssertOK() 59 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 60 | } 61 | 62 | [TestMethod] 63 | public void ServiceProvider() 64 | { 65 | var mcf = MockHttpClientFactory.Create(); 66 | mcf.CreateClient("XXX", new Uri("https://somesys")).Request(HttpMethod.Get, "test").Respond.With("test output"); 67 | 68 | var hc = Test.ReplaceHttpClientFactory(mcf) 69 | .Services.GetService().CreateClient("XXX"); 70 | 71 | var r = hc.GetAsync("test").Result; 72 | Assert.IsNotNull(r); 73 | Assert.AreEqual("test output", r.Content.ReadAsStringAsync().Result); 74 | } 75 | 76 | [TestMethod] 77 | public void MockHttpClientFactory_NoMocking() 78 | { 79 | var mcf = MockHttpClientFactory.Create(); 80 | mcf.CreateClient("XXX").WithoutMocking(); 81 | 82 | Startup.MessageProcessingHandler.WasExecuted = false; 83 | 84 | var hc = Test.ReplaceHttpClientFactory(mcf) 85 | .Services.GetService().CreateClient("XXX"); 86 | 87 | var ex = Assert.ThrowsException(() => hc.GetAsync("test").Result); 88 | 89 | Assert.IsTrue(Startup.MessageProcessingHandler.WasExecuted); 90 | 91 | mcf.VerifyAll(); 92 | } 93 | 94 | [TestMethod] 95 | public void MockHttpClientFactory_NoMocking_Exclude() 96 | { 97 | using var mcf = MockHttpClientFactory.Create(); 98 | mcf.CreateClient("XXX").WithoutMocking(typeof(Startup.MessageProcessingHandler)); 99 | 100 | Startup.MessageProcessingHandler.WasExecuted = false; 101 | 102 | var hc = Test.ReplaceHttpClientFactory(mcf) 103 | .Services.GetService().CreateClient("XXX"); 104 | 105 | var ex = Assert.ThrowsException(() => hc.GetAsync("test").Result); 106 | 107 | Assert.IsFalse(Startup.MessageProcessingHandler.WasExecuted); 108 | 109 | mcf.VerifyAll(); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/ProductFunctionTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | using UnitTestEx.Function; 6 | 7 | namespace UnitTestEx.MSTest.Test 8 | { 9 | [TestClass] 10 | public class ProductFunctionTest 11 | { 12 | [TestMethod] 13 | public void Notfound() 14 | { 15 | var mcf = MockHttpClientFactory.Create(); 16 | mcf.CreateClient("XXX", new Uri("https://d365test")) 17 | .Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound); 18 | 19 | using var test = FunctionTester.Create(); 20 | test.ReplaceHttpClientFactory(mcf) 21 | .HttpTrigger() 22 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/xyz"), "xyz", test.Logger)) 23 | .AssertNotFound(); 24 | } 25 | 26 | [TestMethod] 27 | public void Success() 28 | { 29 | var mcf = MockHttpClientFactory.Create(); 30 | mcf.CreateClient("XXX", new Uri("https://d365test")) 31 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 32 | 33 | using var test = FunctionTester.Create(); 34 | test.ReplaceHttpClientFactory(mcf) 35 | .HttpTrigger() 36 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) 37 | .AssertOK() 38 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 39 | } 40 | 41 | [TestMethod] 42 | public void Success2() 43 | { 44 | var mcf = MockHttpClientFactory.Create(); 45 | mcf.CreateClient("XXX", new Uri("https://d365test")) 46 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 47 | 48 | using var test = FunctionTester.Create(); 49 | test.ReplaceHttpClientFactory(mcf) 50 | .Type() 51 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) 52 | .ToActionResultAssertor() 53 | .AssertOK() 54 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 55 | } 56 | 57 | [TestMethod] 58 | public void Exception() 59 | { 60 | var mcf = MockHttpClientFactory.Create(); 61 | 62 | using var test = FunctionTester.Create(); 63 | test.ReplaceHttpClientFactory(mcf) 64 | .HttpTrigger() 65 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/exception"), "exception", test.Logger)) 66 | .AssertException("An unexpected exception occured."); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/Resources/FunctionTest-ValidJsonResource.json: -------------------------------------------------------------------------------- 1 | { 2 | "FirstName": "Rachel", 3 | "LastName": "Smith" 4 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/Resources/MockHttpClientTest-UriAndBody_WithJsonResponse3.json: -------------------------------------------------------------------------------- 1 | {"first":"Bob","last":"Jane"} -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/UnitTestEx.MSTest.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | true 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/UnitTestEx.MSTest.Test/appsettings.unittest.json: -------------------------------------------------------------------------------- 1 | { 2 | "SpecialKey": "VerySpecialValue", 3 | "DefaultJsonSerializer": "UnitTestEx.MSTest.Test.NewtonsoftJsonSerializer, UnitTestEx.MSTest.Test" 4 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/IsolatedFunctionTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Net.Http; 3 | using UnitTestEx.Expectations; 4 | using UnitTestEx.IsolatedFunction; 5 | 6 | namespace UnitTestEx.NUnit.Test 7 | { 8 | [TestFixture] 9 | public class IsolatedFunctionTest 10 | { 11 | [Test] 12 | public void HttpTrigger() 13 | { 14 | using var test = FunctionTester.Create(); 15 | test.HttpTrigger() 16 | .ExpectLogContains("C# HTTP trigger function processed a request.") 17 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, null))) 18 | .AssertSuccess(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Model/Person.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace UnitTestEx.NUnit.Test.Model 5 | { 6 | public class Person 7 | { 8 | [JsonProperty("firstName")] 9 | [JsonPropertyName("firstName")] 10 | public string FirstName { get; set; } 11 | 12 | [JsonProperty("lastName")] 13 | [JsonPropertyName("lastName")] 14 | public string LastName { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Model/Person2.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace UnitTestEx.NUnit.Test.Model 5 | { 6 | class Person2 7 | { 8 | [JsonProperty("first")] 9 | [JsonPropertyName("first")] 10 | public string First { get; set; } 11 | 12 | [JsonProperty("last")] 13 | [JsonPropertyName("last")] 14 | public string Last { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using NUnit.Framework; 5 | using System; 6 | using System.Threading.Tasks; 7 | using UnitTestEx.Expectations; 8 | 9 | namespace UnitTestEx.NUnit.Test.Other 10 | { 11 | [TestFixture] 12 | public class GenericTest 13 | { 14 | [Test] 15 | public void Run_Success() 16 | { 17 | using var test = GenericTester.Create(); 18 | test.Run(() => 1) 19 | .AssertSuccess() 20 | .AssertValue(1); 21 | } 22 | 23 | [Test] 24 | public void Run_Success_AssertJSON() 25 | { 26 | using var test = GenericTester.Create(); 27 | test.Run(() => 1) 28 | .AssertSuccess() 29 | .AssertJson("1"); 30 | } 31 | 32 | [Test] 33 | public void Run_Exception() 34 | { 35 | using var test = GenericTester.Create(); 36 | test.ExpectException("Badness.") 37 | .Run(() => throw new DivideByZeroException("Badness.")); 38 | } 39 | 40 | [Test] 41 | public void Run_Service() 42 | { 43 | using var test = GenericTester.Create(); 44 | 45 | test.Run(gin => gin.Pour()) 46 | .AssertSuccess() 47 | .AssertValue(1); 48 | 49 | test.Run(gin => gin.PourAsync()) 50 | .AssertSuccess() 51 | .AssertValue(1); 52 | 53 | test.Run(gin => gin.Shake()) 54 | .AssertSuccess(); 55 | 56 | test.Run(gin => gin.ShakeAsync()) 57 | .AssertSuccess(); 58 | 59 | test.Run(gin => gin.Stir()) 60 | .AssertException("As required by Bond; shaken, not stirred."); 61 | 62 | test.Run(gin => gin.StirAsync()) 63 | .AssertException("As required by Bond; shaken, not stirred."); 64 | } 65 | 66 | [Test] 67 | public void Configuration_Overrride_Use() 68 | { 69 | // Demonstrates how to override the configuration settings for a test. 70 | using var test = GenericTester.Create(); 71 | test.UseAdditionalConfiguration([new("SpecialKey", "NotSoSpecial")]); 72 | var cv = test.Configuration.GetValue("SpecialKey"); 73 | Assert.That(cv, Is.EqualTo("NotSoSpecial")); 74 | } 75 | } 76 | 77 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Just for testing aye bro!")] 78 | public class Gin 79 | { 80 | public void Stir() => throw new DivideByZeroException("As required by Bond; shaken, not stirred."); 81 | public Task StirAsync() => throw new DivideByZeroException("As required by Bond; shaken, not stirred."); 82 | public void Shake() { } 83 | public Task ShakeAsync() => Task.CompletedTask; 84 | public int Pour() => 1; 85 | public Task PourAsync() => Task.FromResult(1); 86 | } 87 | 88 | public class EntryPoint 89 | { 90 | //public void ConfigureAppConfiguration(HostBuilderContext context, IConfigurationBuilder config) { } 91 | 92 | //public void ConfigureHostConfiguration(IConfigurationBuilder config) { } 93 | 94 | //public void ConfigureServices(IServiceCollection services) { } 95 | 96 | public void ConfigureApplication(IHostApplicationBuilder builder) => builder.Services.AddSingleton(); 97 | } 98 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Other/LoggerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Collections.Generic; 5 | using UnitTestEx.NUnit.Internal; 6 | 7 | namespace UnitTestEx.NUnit.Test.Other 8 | { 9 | [TestFixture] 10 | public class LoggerTest 11 | { 12 | [Test] 13 | public void Test() 14 | { 15 | var l = new NUnitTestImplementor().CreateLoggerProvider().CreateLogger("LoggerTest"); 16 | 17 | var scope = l.BeginScope(new Dictionary() { { "CorrelationId", "abc" }, { "AltCode", 1234 } }); 18 | l.LogInformation("A single line of {Text}.", "text"); 19 | var scope2 = l.BeginScope(new Dictionary() { { "Other", "bananas" } }); 20 | l.LogWarning($"First line of text.{Environment.NewLine}Second line of text.{Environment.NewLine}Third line of text."); 21 | l.LogInformation("A single line of text."); 22 | scope2.Dispose(); 23 | l.LogWarning($"First line of text.{Environment.NewLine}Second line of text.{Environment.NewLine}Third line of text."); 24 | scope.Dispose(); 25 | l.LogInformation("A single line of text."); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Other/ObjectComparerTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UnitTestEx.NUnit.Test.Model; 3 | 4 | namespace UnitTestEx.NUnit.Test.Other 5 | { 6 | [TestFixture] 7 | public class ObjectComparerTest 8 | { 9 | [Test] 10 | public void MatchyMatchy() 11 | { 12 | var p1 = new Person { FirstName = "Wendy", LastName = "Brown" }; 13 | var p2 = new Person { FirstName = "Wendy", LastName = "Brown" }; 14 | ObjectComparer.Assert(p1, p2); 15 | } 16 | 17 | [Test] 18 | public void PathsToIgnore_PropertyName() 19 | { 20 | var p1 = new Person { FirstName = "Wendy", LastName = "XXX" }; 21 | var p2 = new Person { FirstName = "Wendy", LastName = "YYY" }; 22 | ObjectComparer.Assert(p1, p2, "LastName"); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Other/OneOffTestSetUpTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using NUnit.Framework; 3 | using UnitTestEx.Abstractions; 4 | 5 | [assembly: UnitTestEx.Abstractions.OneOffTestSetUp(typeof(UnitTestEx.NUnit.Test.Other.OneOffTestSetUp))] 6 | 7 | namespace UnitTestEx.NUnit.Test.Other 8 | { 9 | [TestFixture] 10 | public class OneOffTestSetUpTest 11 | { 12 | [OneTimeSetUp] 13 | public void OneTimeSetUp() => OneOffTestSetUpAttribute.SetUp(GetType().Assembly); 14 | 15 | [Test] 16 | public void SetUp_SetDefaultUserName() 17 | { 18 | Assert.That(TestSetUp.Default.DefaultUserName, Is.EqualTo("Luke")); 19 | } 20 | 21 | [Test] 22 | public void TesterExtensions_ApiTester() 23 | { 24 | using var test = ApiTester.Create(); 25 | var bs = test.Services.GetService(); 26 | Assert.That(bs, Is.Not.Null); 27 | } 28 | 29 | [Test] 30 | public void TesterExtensions_FunctionTester() 31 | { 32 | using var test = FunctionTester.Create(); 33 | var bs = test.Services.GetService(); 34 | Assert.That(bs, Is.Not.Null); 35 | } 36 | 37 | [Test] 38 | public void TesterExtensions_GenericTester() 39 | { 40 | using var test = GenericTester.Create(); 41 | var bs = test.Services.GetService(); 42 | Assert.That(bs, Is.Not.Null); 43 | } 44 | } 45 | 46 | public class OneOffTestSetUp : OneOffTestSetUpBase 47 | { 48 | public override void SetUp() 49 | { 50 | TestSetUp.Default.DefaultUserName = "Luke"; 51 | TestSetUp.Extensions.Add(new TestExtension()); 52 | } 53 | } 54 | 55 | public class TestExtension : TesterExtensionsConfig 56 | { 57 | public override void ConfigureServices(TesterBase owner, IServiceCollection services) 58 | { 59 | services.AddSingleton(); 60 | } 61 | } 62 | 63 | public class BlahService { } 64 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/ProductFunctionTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.WebJobs; 2 | using Microsoft.Azure.WebJobs.Extensions.Timers; 3 | using Moq; 4 | using NUnit.Framework; 5 | using System; 6 | using System.Net; 7 | using System.Net.Http; 8 | using UnitTestEx.Expectations; 9 | using UnitTestEx.Function; 10 | 11 | namespace UnitTestEx.NUnit.Test 12 | { 13 | [TestFixture] 14 | public class ProductFunctionTest 15 | { 16 | [Test] 17 | public void Notfound() 18 | { 19 | var mcf = MockHttpClientFactory.Create(); 20 | mcf.CreateClient("XXX", new Uri("https://d365test")) 21 | .Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound); 22 | 23 | using var test = FunctionTester.Create(); 24 | test.ReplaceHttpClientFactory(mcf) 25 | .HttpTrigger() 26 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/xyz"), "xyz", test.Logger)) 27 | .AssertNotFound(); 28 | } 29 | 30 | [Test] 31 | public void Success() 32 | { 33 | var mcf = MockHttpClientFactory.Create(); 34 | mcf.CreateClient("XXX", new Uri("https://d365test")) 35 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 36 | 37 | using var test = FunctionTester.Create(); 38 | test.ReplaceHttpClientFactory(mcf) 39 | .HttpTrigger() 40 | .ExpectLogContains("C# HTTP trigger function processed a request.") 41 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) 42 | .AssertOK() 43 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 44 | } 45 | 46 | [Test] 47 | public void Success2() 48 | { 49 | var mcf = MockHttpClientFactory.Create(); 50 | mcf.CreateClient("XXX", new Uri("https://d365test")) 51 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 52 | 53 | using var test = FunctionTester.Create(); 54 | test.ReplaceHttpClientFactory(mcf) 55 | .Type() 56 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/abc"), "abc", test.Logger)) 57 | .ToActionResultAssertor() 58 | .AssertOK() 59 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 60 | } 61 | 62 | [Test] 63 | public void Exception() 64 | { 65 | var mcf = MockHttpClientFactory.Create(); 66 | 67 | using var test = FunctionTester.Create(); 68 | test.ReplaceHttpClientFactory(mcf) 69 | .HttpTrigger() 70 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/exception"), "exception", test.Logger)) 71 | .AssertException("An unexpected exception occured."); 72 | } 73 | 74 | [Test] 75 | public void TimerTriggered() 76 | { 77 | using var test = FunctionTester.Create(); 78 | test.Type() 79 | .ExpectLogContains("(DI)") 80 | .ExpectLogContains("(method)") 81 | .Run(f => f.DailyRun(new TimerInfo(new DailySchedule("2:00:00"), It.IsAny(), false), test.Logger)) 82 | .AssertSuccess(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Resources/FunctionTest-ValidJsonResource.json: -------------------------------------------------------------------------------- 1 | { 2 | "FirstName": "Rachel", 3 | "LastName": "Smith" 4 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Resources/MockHttpClientTest-UriAndBody_WithJsonResponse3.json: -------------------------------------------------------------------------------- 1 | {"first":"Bob","last":"Jane"} -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Resources/mock.unittestex.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Avanade/UnitTestEx/refs/heads/main/src/UnitTestEx/Schema/mock.unittestex.json 2 | - method: post 3 | uri: products/xyz 4 | body: ^ 5 | response: 6 | status: 202 7 | body: | 8 | {"product":"xyz","quantity":1} 9 | 10 | - method: get 11 | uri: people/123 12 | response: 13 | headers: 14 | Age: [ '55' ] 15 | x-blah: [ abc ] 16 | body: | 17 | { 18 | "first":"Bob", 19 | "last":"Jane" 20 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/Resources/sequence.unittestex.yaml: -------------------------------------------------------------------------------- 1 | - method: get 2 | uri: people/123 3 | sequence: 4 | - body: | 5 | { 6 | "first":"Bob", 7 | "last":"Jane" 8 | } 9 | - body: | 10 | { 11 | "first":"Sarah", 12 | "last":"Johns" 13 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/UnitTestEx.NUnit.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | false 6 | preview 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | all 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | 37 | 38 | 39 | all 40 | runtime; build; native; contentfiles; analyzers; buildtransitive 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/UnitTestEx.NUnit.Test/appsettings.unittest.json: -------------------------------------------------------------------------------- 1 | { 2 | "SpecialKey": "VerySpecialValue" 3 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/Model/Person.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace UnitTestEx.Xunit.Test.Model 5 | { 6 | public class Person 7 | { 8 | [JsonProperty("firstName")] 9 | [JsonPropertyName("firstName")] 10 | public string FirstName { get; set; } 11 | 12 | [JsonProperty("lastName")] 13 | [JsonPropertyName("lastName")] 14 | public string LastName { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/Model/Person2.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace UnitTestEx.Xunit.Test.Model 5 | { 6 | class Person2 7 | { 8 | [JsonProperty("first")] 9 | [JsonPropertyName("first")] 10 | public string First { get; set; } 11 | 12 | [JsonProperty("last")] 13 | [JsonPropertyName("last")] 14 | public string Last { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/Other/GenericTest.cs: -------------------------------------------------------------------------------- 1 | using UnitTestEx.Expectations; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace UnitTestEx.Xunit.Test.Other 6 | { 7 | public class GenericTest : UnitTestBase 8 | { 9 | public GenericTest(ITestOutputHelper output) : base(output) { } 10 | 11 | [Fact] 12 | public void Run_Success() 13 | { 14 | using var test = GenericTester.Create(); 15 | test.Run(() => 1) 16 | .AssertSuccess() 17 | .AssertValue(1); 18 | } 19 | 20 | [Fact] 21 | public void Run_Exception() 22 | { 23 | using var test = GenericTester.Create(); 24 | test.ExpectError("Badness.") 25 | .Run(() => throw new System.ArithmeticException("Badness.")); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/Other/LoggerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using UnitTestEx.Xunit.Internal; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace UnitTestEx.Xunit.Test.Other 9 | { 10 | public class LoggerTest : UnitTestBase 11 | { 12 | public LoggerTest(ITestOutputHelper output) : base(output) { } 13 | 14 | [Fact] 15 | public void Test() 16 | { 17 | var l = new XunitTestImplementor(Output).CreateLoggerProvider().CreateLogger("LoggerTest"); 18 | 19 | var scope = l.BeginScope(new Dictionary() { { "CorrelationId", "abc" }, { "AltCode", 1234 } }); 20 | l.LogInformation("A single line of {Text}.", "text"); 21 | var scope2 = l.BeginScope(new Dictionary() { { "Other", "bananas" } }); 22 | l.LogWarning($"First line of text.{Environment.NewLine}Second line of text.{Environment.NewLine}Third line of text."); 23 | l.LogInformation("A single line of text."); 24 | scope2.Dispose(); 25 | l.LogWarning($"First line of text.{Environment.NewLine}Second line of text.{Environment.NewLine}Third line of text."); 26 | scope.Dispose(); 27 | l.LogInformation("A single line of text."); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/Other/ObjectComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnitTestEx.Xunit.Test.Model; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace UnitTestEx.Xunit.Test.Other 7 | { 8 | public class ObjectComparerTest : UnitTestBase 9 | { 10 | public ObjectComparerTest(ITestOutputHelper output) : base(output) { } 11 | 12 | [Fact] 13 | public void Test() 14 | { 15 | var p1 = new Person { FirstName = "Wendy", LastName = "Brown" }; 16 | var p2 = new Person { FirstName = "Wendy", LastName = "Brown" }; 17 | ObjectComparer.Assert(p1, p2); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/PersonControllerTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using UnitTestEx.Api; 4 | using UnitTestEx.Api.Controllers; 5 | using UnitTestEx.Api.Models; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace UnitTestEx.Xunit.Test 10 | { 11 | public class PersonControllerTest : UnitTestBase 12 | { 13 | public PersonControllerTest(ITestOutputHelper output) : base(output) { } 14 | 15 | [Fact] 16 | public async Task Get_Test1() 17 | { 18 | using var test = ApiTester.Create(); 19 | (await test.Controller() 20 | .RunAsync(c => c.Get(1))) 21 | .AssertOK() 22 | .AssertValue(new Person { Id = 1, FirstName = "Bob", LastName = "Smith" }); 23 | } 24 | 25 | [Fact] 26 | public void Get_Test2() 27 | { 28 | int id = 2; 29 | using var test = ApiTester.Create(); 30 | test.Controller() 31 | .Run(c => c.Get(id)) 32 | .AssertOK() 33 | .AssertValue(new Person { Id = id, FirstName = "Jane", LastName = "Jones" }); 34 | } 35 | 36 | [Fact] 37 | public void Get_Test3() 38 | { 39 | var p = new Person { Id = 3, FirstName = "Brad", LastName = "Davies" }; 40 | 41 | using var test = ApiTester.Create(); 42 | test.Controller().Run(c => c.Get(p.Id)).AssertOK().AssertValue(p); 43 | } 44 | 45 | [Fact] 46 | public void Get_Test4() 47 | { 48 | using var test = ApiTester.Create(); 49 | test.Controller() 50 | .Run(c => c.Get(4)) 51 | .AssertNotFound(); 52 | } 53 | 54 | [Fact] 55 | public void GetByArgs_Test1() 56 | { 57 | using var test = ApiTester.Create(); 58 | test.Controller() 59 | .Run(c => c.GetByArgs("Mary", "Brown", new List { 88, 99 })) 60 | .AssertOK() 61 | .AssertValue("Mary-Brown-88,99"); 62 | } 63 | 64 | [Fact] 65 | public void GetByArgs_Test2() 66 | { 67 | using var test = ApiTester.Create(); 68 | test.Controller() 69 | .Run(c => c.GetByArgs(null, null, null)) 70 | .AssertOK() 71 | .AssertValue("--"); 72 | } 73 | 74 | [Fact] 75 | public void Update_Test1() 76 | { 77 | using var test = ApiTester.Create(); 78 | test.Controller() 79 | .Run(c => c.Update(1, new Person { FirstName = "Bob", LastName = "Smith" })) 80 | .AssertOK() 81 | .AssertValue(new Person { Id = 1, FirstName = "Bob", LastName = "Smith" }); 82 | } 83 | 84 | [Fact] 85 | public void Update_Test2() 86 | { 87 | using var test = ApiTester.Create(); 88 | test.Controller() 89 | .Run(c => c.Update(1, new Person { FirstName = null, LastName = null })) 90 | .AssertBadRequest() 91 | .AssertErrors( 92 | "First name is required.", 93 | "Last name is required."); 94 | } 95 | 96 | [Fact] 97 | public void Update_Test3() 98 | { 99 | using var test = ApiTester.Create(); 100 | test.Controller() 101 | .Run(c => c.Update(1, new Person { FirstName = null, LastName = null })) 102 | .AssertBadRequest() 103 | .AssertErrors( 104 | new ApiError("firstName", "First name is required."), 105 | new ApiError("lastName", "Last name is required.")); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/ProductApiTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnitTestEx.Xunit.Test 4 | { 5 | public class ProductApiTestFixture : ApiTestFixture where TStartup : class 6 | { 7 | private static int _counter = 0; 8 | 9 | protected override void OnConfiguration() 10 | { 11 | Test.ReplaceSingleton(); 12 | MockHttpClientFactory.Create(); 13 | 14 | _counter++; 15 | if (_counter > 1) 16 | { 17 | throw new InvalidOperationException("ProductApiTestFixture should only be instantiated once per test run."); 18 | } 19 | } 20 | 21 | public class TestSomeSome { } 22 | } 23 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/ProductControllerTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using UnitTestEx.Api; 7 | using UnitTestEx.Api.Controllers; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace UnitTestEx.Xunit.Test 12 | { 13 | public class ProductControllerTest : WithApiTester, IClassFixture> 14 | { 15 | public ProductControllerTest(ProductApiTestFixture fixture, ITestOutputHelper output) : base(fixture, output) { } 16 | 17 | [Fact] 18 | public void Notfound() 19 | { 20 | var mcf = MockHttpClientFactory.Create(); 21 | mcf.CreateClient("XXX", new Uri("https://somesys/")) 22 | .Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound); 23 | 24 | Test.ReplaceHttpClientFactory(mcf) 25 | .Controller() 26 | .Run(c => c.Get("xyz")) 27 | .AssertNotFound(); 28 | } 29 | 30 | [Fact] 31 | public void Success() 32 | { 33 | var mcf = MockHttpClientFactory.Create(); 34 | mcf.CreateClient("XXX", new Uri("https://somesys")) 35 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 36 | 37 | Test.ReplaceHttpClientFactory(mcf) 38 | .Controller() 39 | .Run(c => c.Get("abc")) 40 | .AssertOK() 41 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 42 | } 43 | 44 | [Fact] 45 | public async Task ServiceProvider() 46 | { 47 | var mcf = MockHttpClientFactory.Create(); 48 | mcf.CreateClient("XXX", new Uri("https://somesys")).Request(HttpMethod.Get, "test").Respond.With("test output"); 49 | 50 | var hc = Test.ReplaceHttpClientFactory(mcf) 51 | .Services.GetService().CreateClient("XXX"); 52 | 53 | var r = await hc.GetAsync("test"); 54 | Assert.NotNull(r); 55 | Assert.Equal("test output", await r.Content.ReadAsStringAsync()); 56 | } 57 | 58 | [Fact] 59 | public void To_HttpResponseMessage_Created() 60 | { 61 | Test.Type() 62 | .Run(c => c.GetCreated()) 63 | .ToHttpResponseMessageAssertor() 64 | .AssertCreated() 65 | .AssertLocationHeaderContains("bananas") 66 | .AssertContent("abc"); 67 | } 68 | 69 | [Fact] 70 | public void To_HttpResponseMessage_OK() 71 | { 72 | Test.Type() 73 | .Run(c => c.GetOK()) 74 | .ToHttpResponseMessageAssertor() 75 | .AssertOK(); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/ProductFunctionTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using UnitTestEx.Function; 5 | using UnitTestEx.Xunit; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace UnitTestEx.Xunit.Test 10 | { 11 | public class ProductFunctionTest : UnitTestBase 12 | { 13 | public ProductFunctionTest(ITestOutputHelper output) : base(output) { } 14 | 15 | [Fact] 16 | public void Notfound() 17 | { 18 | var mcf = MockHttpClientFactory.Create(); 19 | mcf.CreateClient("XXX", new Uri("https://d365test")) 20 | .Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound); 21 | 22 | using var test = FunctionTester.Create(); 23 | test.ReplaceHttpClientFactory(mcf) 24 | .HttpTrigger() 25 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/xyz"), "xyz", test.Logger)) 26 | .AssertNotFound(); 27 | } 28 | 29 | [Fact] 30 | public void Success() 31 | { 32 | var mcf = MockHttpClientFactory.Create(); 33 | mcf.CreateClient("XXX", new Uri("https://d365test")) 34 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 35 | 36 | using var test = FunctionTester.Create(); 37 | test.ReplaceHttpClientFactory(mcf) 38 | .HttpTrigger() 39 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) 40 | .AssertOK() 41 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 42 | } 43 | 44 | [Fact] 45 | public void Success2() 46 | { 47 | var mcf = MockHttpClientFactory.Create(); 48 | mcf.CreateClient("XXX", new Uri("https://d365test")) 49 | .Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" }); 50 | 51 | using var test = FunctionTester.Create(); 52 | test.ReplaceHttpClientFactory(mcf) 53 | .Type() 54 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) 55 | .ToActionResultAssertor() 56 | .AssertOK() 57 | .AssertValue(new { id = "Abc", description = "A blue carrot" }); 58 | } 59 | 60 | [Fact] 61 | public void Exception() 62 | { 63 | var mcf = MockHttpClientFactory.Create(); 64 | 65 | using var test = FunctionTester.Create(); 66 | test.ReplaceHttpClientFactory(mcf) 67 | .HttpTrigger() 68 | .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/exception"), "exception", test.Logger)) 69 | .AssertException("An unexpected exception occured."); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/Resources/FunctionTest-ValidJsonResource.json: -------------------------------------------------------------------------------- 1 | { 2 | "FirstName": "Rachel", 3 | "LastName": "Smith" 4 | } -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/Resources/MockHttpClientTest-UriAndBody_WithJsonResponse3.json: -------------------------------------------------------------------------------- 1 | {"first":"Bob","last":"Jane"} -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/UnitTestEx.Xunit.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | all 32 | 33 | 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | all 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/UnitTestEx.Xunit.Test/appsettings.unittest.json: -------------------------------------------------------------------------------- 1 | { 2 | "SpecialKey": "VerySpecialValue" 3 | } --------------------------------------------------------------------------------