From 6cf51ae9c7763198c27a713b1c9739901c987a4c Mon Sep 17 00:00:00 2001 From: David Naylor Date: Fri, 6 Jan 2023 18:21:32 +0000 Subject: [PATCH] Quickstart --- README.md | 14 +- docs/About.md | 17 ++- docs/dependency_injection/index.md | 27 ---- docs/dependency_injection/proxy_lifetime.md | 23 --- .../redirect_configuration.md | 43 ------ .../redirect_registration.md | 35 ----- docs/dependency_injection/via_redirect.md | 39 ----- .../dependency_injection.md} | 24 +++- docs/quickstart/index.md | 134 ++++++++++++++++-- docs/quickstart/proxy_factory.md | 14 ++ docs/{Redirect.md => quickstart/redirects.md} | 49 ++++--- docs/{Verify.md => quickstart/verify.md} | 70 +++++---- docs/redirect/index.md | 15 -- docs/redirect/method_parameters.md | 67 --------- docs/redirect/method_variations.md | 85 ----------- docs/redirect/proxy.md | 55 ------- docs/redirect/relay.md | 101 ------------- docs/redirect/reset.md | 25 ---- docs/redirect/retarget.md | 49 ------- docs/redirect/strict_mode.md | 25 ---- docs/redirect/via.md | 32 ----- docs/test_doubles/index.md | 17 --- docs/test_doubles/redirects.md | 105 -------------- docs/verify/index.md | 10 -- docs/verify/named_arguments.md | 57 -------- docs/verify/record.md | 33 ----- docs/verify/record_chaining.md | 25 ---- docs/verify/record_ordering.md | 29 ---- docs/verify/recording_exceptions.md | 32 ----- docs/verify/redirect_record.md | 26 ---- docs/verify/verify_snapshot.md | 38 ----- docs/verify/verify_visitor.md | 29 ---- test/DivertR.UnitTests/Model/Bar.cs | 11 ++ test/DivertR.UnitTests/Model/BarFactory.cs | 15 ++ test/DivertR.UnitTests/Model/IBar.cs | 6 + test/DivertR.UnitTests/Model/IBarFactory.cs | 6 + ...edirectExample.cs => QuickstartExample.cs} | 83 +++++++++-- .../{ => TestHarness}/IFooClient.cs | 2 +- .../{ => TestHarness}/WebAppFixture.cs | 3 +- .../{ => Tests}/MockSampleTests.cs | 3 +- .../{ => Tests}/SpySampleTests.cs | 3 +- 41 files changed, 349 insertions(+), 1127 deletions(-) delete mode 100644 docs/dependency_injection/index.md delete mode 100644 docs/dependency_injection/proxy_lifetime.md delete mode 100644 docs/dependency_injection/redirect_configuration.md delete mode 100644 docs/dependency_injection/redirect_registration.md delete mode 100644 docs/dependency_injection/via_redirect.md rename docs/{DI.md => quickstart/dependency_injection.md} (89%) create mode 100644 docs/quickstart/proxy_factory.md rename docs/{Redirect.md => quickstart/redirects.md} (94%) rename docs/{Verify.md => quickstart/verify.md} (91%) delete mode 100644 docs/redirect/index.md delete mode 100644 docs/redirect/method_parameters.md delete mode 100644 docs/redirect/method_variations.md delete mode 100644 docs/redirect/proxy.md delete mode 100644 docs/redirect/relay.md delete mode 100644 docs/redirect/reset.md delete mode 100644 docs/redirect/retarget.md delete mode 100644 docs/redirect/strict_mode.md delete mode 100644 docs/redirect/via.md delete mode 100644 docs/test_doubles/index.md delete mode 100644 docs/test_doubles/redirects.md delete mode 100644 docs/verify/index.md delete mode 100644 docs/verify/named_arguments.md delete mode 100644 docs/verify/record.md delete mode 100644 docs/verify/record_chaining.md delete mode 100644 docs/verify/record_ordering.md delete mode 100644 docs/verify/recording_exceptions.md delete mode 100644 docs/verify/redirect_record.md delete mode 100644 docs/verify/verify_snapshot.md delete mode 100644 docs/verify/verify_visitor.md create mode 100644 test/DivertR.UnitTests/Model/Bar.cs create mode 100644 test/DivertR.UnitTests/Model/BarFactory.cs create mode 100644 test/DivertR.UnitTests/Model/IBar.cs create mode 100644 test/DivertR.UnitTests/Model/IBarFactory.cs rename test/DivertR.UnitTests/{RedirectExample.cs => QuickstartExample.cs} (52%) rename test/DivertR.WebAppTests/{ => TestHarness}/IFooClient.cs (89%) rename test/DivertR.WebAppTests/{ => TestHarness}/WebAppFixture.cs (97%) rename test/DivertR.WebAppTests/{ => Tests}/MockSampleTests.cs (98%) rename test/DivertR.WebAppTests/{ => Tests}/SpySampleTests.cs (99%) diff --git a/README.md b/README.md index 08c2910d..4bec2566 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ dotnet add package DivertR # Feature Summary -1. Test double proxy framework for mocking, faking, stubbing, spying, etc. [[more]](./docs/Redirect.md) +1. Test double proxy framework for mocking, faking, stubbing, spying, etc. [[more]](https://devodo.github.io/DivertR/redirects/) 2. Proxies that wrap and forward to root (original) instances so tests run against the integrated system whilst modifying and spying on specific parts as needed. [[more]](./docs/Redirect.md#proxy) 3. A lightweight, fluent interface for configuring proxies to redirect calls to delegates or substitute instances. [[more]](./docs/Redirect.md#via) 4. Dynamic update and reset of proxies in a running application enabling changes between tests without requiring restart and initialisation overhead. [[more]](./docs/Redirect.md#reset) @@ -78,12 +78,6 @@ public async Task GivenFooRepoException_WhenGetFoo_ThenReturns500InternalServerE } ``` -# Quickstart - -For more examples and a demonstration of setting up a test harness for a WebApp see this [WebApp Testing Sample](./test/DivertR.WebAppTests/WebAppTests.cs) -or follow below for a quickstart: - -* [Redirects](./docs/Redirect.md) for creating and configuring proxies. -* [Dependency Injection](./docs/DI.md) integration. -* [Recording and Verifying](./docs/Verify.md) calls. -* [About](./docs/About.md) DivertR. +# Documentation +* [Documentation](https://devodo.github.io/DivertR/) +* [Quickstart](https://devodo.github.io/DivertR/quickstart/) diff --git a/docs/About.md b/docs/About.md index 946735bc..2932e0a8 100644 --- a/docs/About.md +++ b/docs/About.md @@ -1,4 +1,10 @@ -# About +--- +layout: default +title: About +nav_order: 3 +--- + +# About DivertR is similar to well known mocking frameworks like Moq or FakeItEasy but provides additional features for use with integration testing such as dynamically manipulating the dependency injection (DI) layer at runtime. You can redirect calls to test doubles, such as substitute instances, mocks or delegates, and then optionally relay them back to the original services. @@ -8,11 +14,4 @@ which also lets you customise the DI configuration, e.g. to substitute test doub DivertR was born out of the need to efficiently modify DI configurations between tests running against the same TestServer instance. It has grown into a framework that facilitates testing of wired up systems, bringing a familiar unit/mocking testing style into the realm of component and integration testing, -by providing features to conveniently substitute dependency behaviour (including error conditions) and verify inputs and outputs from recorded call information. - -# DispatchProxy - -The default DivertR proxy factory is implemented using [System.Reflection.DispatchProxy](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.dispatchproxy). As this is part of the .NET Standard 2.1, DivertR therefore does not have any dependencies on external libraries however it is limited to proxying interface types only. - -If class proxies are required, [DivertR.DynamicProxy](../src/DivertR.DynamicProxy/README.md) is an alternative proxy factory implemented using [Castle DynamicProxy](http://www.castleproject.org/projects/dynamicproxy/) that does support classes. -However, when proxying classes, care should be taken as only calls to non-virtual members cannot be intercepted, this can cause inconsistent behaviour e.g. when wrapping root instances. +by providing features to conveniently substitute dependency behaviour (including error conditions) and verify inputs and outputs from recorded call information. \ No newline at end of file diff --git a/docs/dependency_injection/index.md b/docs/dependency_injection/index.md deleted file mode 100644 index c8ffb5b6..00000000 --- a/docs/dependency_injection/index.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -layout: default -title: Dependency Injection -nav_order: 3 -has_children: true ---- - -# Dependency Injection - -DivertR is designed to be embedded easily and transparently into the dependency injection (DI) container to facilitate testing an integrated, wired-up system. -It does this by decorating existing DI service registrations with [Redirects](./Redirect.md) that replace the originals. -These Redirects create proxies that wrap the instances resolved from the originals as their default targets or *roots*. - -By default Redirect proxies transparently forward calls to their roots and therefore, in this initial state, the behaviour of the DI system is unchanged. -Then specific parts of the system can be modified as required by dynamically updating and resetting proxies between tests without requiring restart. - -## .NET ServiceCollection - -Out the box DivertR has support for the .NET `Microsoft.Extensions.DependencyInjection.IServiceCollection`. The examples that follow use this `ServiceCollection` and its registered dependencies: - -```csharp -IServiceCollection services = new ServiceCollection(); - -services.AddTransient(); -services.AddSingleton(); -services.AddSingleton(); -``` \ No newline at end of file diff --git a/docs/dependency_injection/proxy_lifetime.md b/docs/dependency_injection/proxy_lifetime.md deleted file mode 100644 index 76aaa34e..00000000 --- a/docs/dependency_injection/proxy_lifetime.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -layout: default -title: Proxy Lifetime -nav_order: 4 -parent: Dependency Injection ---- - -# Proxy Lifetime - -DivertR aims to leave the original system behaviour unchanged and therefore -when existing DI registrations are replaced by Redirect decorators the lifetime of the registration is preserved. - -For multiple instance registrations such as transients, a separate proxy instance is created for each but all from the same Redirect instance. -In other words all proxies resolved from a Redirect decorated registration are managed from this single Redirect. - -# Dispose - -If a DI created root instance implements the `IDisposable` interface then the DI container manages its disposal, as usual, according to its registration lifetime. - -If a DI Redirect proxy is an `IDisposable` then **only** the proxy instance is disposed by the DI container and not the root. -In this case the responsibilty is left to the proxy for forwarding the dispose call to its root (and it does this by default). - -The above also applies to `IAsyncDisposable`. diff --git a/docs/dependency_injection/redirect_configuration.md b/docs/dependency_injection/redirect_configuration.md deleted file mode 100644 index 84f005b6..00000000 --- a/docs/dependency_injection/redirect_configuration.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -layout: default -title: Redirect Configuration -nav_order: 2 -parent: Dependency Injection ---- - -# Redirect Configuration - -The resolved `IFoo` instance above is a Redirect proxy generated by the underlying `IRedirect` decorator that uses the original DI registration to initialise the proxy root. -In its initial state the `IFoo` proxy forwards all calls directly to its root. However, this behaviour can be modified by obtaining the underlying `Redirect` -from the `Diverter` instance and adding a *Via*: - -```csharp -// Obtain the underlying Redirect from the diverter instance -IRedirect fooRedirect = diverter.Redirect(); - -fooRedirect - .To(x => x.Name) - .Via(call => $"{call.Root.Name} diverted"); - -var foo = provider.GetService(); -Console.WriteLine(foo.Name); // "Foo diverted" -``` - -Any Vias added to the `Redirect` are applied to all its existing proxies and any resolved afterwards: - -```csharp -var foo2 = provider.GetService(); -foo2.Name = "Foo2"; -Console.WriteLine(foo2.Name); // "Foo2 diverted" -``` - -# Reset - -All Redirects registered in the `Diverter` instance and resolved proxies can be *reset* to their initial state with a single call: - -```csharp -diverter.ResetAll(); - -Console.WriteLine(foo.Name); // "Foo" -Console.WriteLine(foo2.Name); // "Foo2" -``` \ No newline at end of file diff --git a/docs/dependency_injection/redirect_registration.md b/docs/dependency_injection/redirect_registration.md deleted file mode 100644 index 85650260..00000000 --- a/docs/dependency_injection/redirect_registration.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -layout: default -title: Redirect Registration -nav_order: 1 -parent: Dependency Injection ---- - -# Redirect Registration - -First instantiate an instance of the `Diverter` class and *register* one or more DI service types of interest that you would like be wrapped as Redirect proxies: - -```csharp -var diverter = new Diverter() - .Register() - .Register(); -``` - -Then call `Divert`, a provided `IServiceCollection` extension method, to install the registered types as `Redirect` decorators: - -```csharp -services.Divert(diverter); -``` - -The `IServiceCollection` can now be used as usual to build the service provider and resolve dependency instances: - -```csharp -IServiceProvider provider = services.BuildServiceProvider(); - -var foo = provider.GetService(); -Console.WriteLine(foo.Name); // "Foo" - -// The behaviour of the resolved foo is the same as its root e.g.: -IFoo demo = new Foo(); -Console.WriteLine(demo.Name); // "Foo"; -``` diff --git a/docs/dependency_injection/via_redirect.md b/docs/dependency_injection/via_redirect.md deleted file mode 100644 index cf07c5e1..00000000 --- a/docs/dependency_injection/via_redirect.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -layout: default -title: Via Redirect -nav_order: 3 -parent: Dependency Injection ---- - -# Via Redirect - -Sometimes a test needs to manipulate instances that are not directly created by the DI container. -E.g. if we assume the `IBarFactory` service registration given above is a factory that creates `IBar` instances. -These instances can be wrapped and managed as Redirect proxies by calling `ViaRedirect` as follows: - -```csharp - -// Wrap created IBar instances as Redirect proxies and get a reference their Redirect -IRedirect barRedirect = diverter - .Redirect() - .To(x => x.Create(Is.Any)) - .ViaRedirect(); - -var barFactory = provider.GetService(); -IBar bar = barFactory.Create("MrBar"); // The Create call now returns IBar proxies -Console.WriteLine(bar.Name); // "MrBar" - -// Add a Via to alter behaviour -barRedirect - .To(x => x.Name) - .Via(call => call.Root.Name + " diverted"); - -Console.WriteLine(bar.Name); // "MrBar diverted" - -// ResetAll also resets ViaRedirects -diverter.ResetAll(); -Console.WriteLine(bar.Name); // "MrBar" -``` - -`RedirectVia` intercepts the method return values and wraps them as proxies created from a Via. -It returns this Via that can then be used to control the behaviour of the proxy wrappers. diff --git a/docs/DI.md b/docs/quickstart/dependency_injection.md similarity index 89% rename from docs/DI.md rename to docs/quickstart/dependency_injection.md index ef5c8da2..3a36b02c 100644 --- a/docs/DI.md +++ b/docs/quickstart/dependency_injection.md @@ -1,4 +1,11 @@ -# Dependency Injection +--- +layout: default +title: Dependency Injection +nav_order: 2 +parent: Quickstart +--- + +# Dependency Injection DivertR is designed to be embedded easily and transparently into the dependency injection (DI) container to facilitate testing an integrated, wired-up system. It does this by decorating existing DI service registrations with [Redirects](./Redirect.md) that replace the originals. @@ -7,9 +14,9 @@ These Redirects create proxies that wrap the instances resolved from the origina By default Redirect proxies transparently forward calls to their roots and therefore, in this initial state, the behaviour of the DI system is unchanged. Then specific parts of the system can be modified as required by dynamically updating and resetting proxies between tests without requiring restart. -# .NET ServiceCollection +## .NET ServiceCollection -Out the box DivertR has support for the .NET `Microsoft.Extensions.DependencyInjection.IServiceCollection`. The examples below use the following `ServiceCollection` and its registrations: +Out the box DivertR has support for the .NET `Microsoft.Extensions.DependencyInjection.IServiceCollection`. The examples that follow use this `ServiceCollection` and its registered dependencies: ```csharp IServiceCollection services = new ServiceCollection(); @@ -50,7 +57,7 @@ Console.WriteLine(demo.Name); // "Foo"; # Redirect Configuration -The resolved `IFoo` instance above is a Redirect proxy generated by the underlying `IRedirect` decorator that uses the original DI registration to initialise the proxy root. +The resolved `IFoo` instance above is a Redirect proxy generated by the underlying `IRedirect` decorator that uses the original DI registration to initialise the proxy root. In its initial state the `IFoo` proxy forwards all calls directly to its root. However, this behaviour can be modified by obtaining the underlying `Redirect` from the `Diverter` instance and adding a *Via*: @@ -85,7 +92,7 @@ Console.WriteLine(foo.Name); // "Foo" Console.WriteLine(foo2.Name); // "Foo2" ``` -# ViaRedirect +# Via Redirect Sometimes a test needs to manipulate instances that are not directly created by the DI container. E.g. if we assume the `IBarFactory` service registration given above is a factory that creates `IBar` instances. @@ -115,9 +122,12 @@ diverter.ResetAll(); Console.WriteLine(bar.Name); // "MrBar" ``` +`RedirectVia` intercepts the method return values and wraps them as proxies created from a Via. +It returns this Via that can then be used to control the behaviour of the proxy wrappers. + # Proxy Lifetime -DivertR aims to leave the original system behaviour unchanged and therefore +DivertR aims to leave the original system behaviour unchanged and therefore when existing DI registrations are replaced by Redirect decorators the lifetime of the registration is preserved. For multiple instance registrations such as transients, a separate proxy instance is created for each but all from the same Redirect instance. @@ -130,4 +140,4 @@ If a DI created root instance implements the `IDisposable` interface then the DI If a DI Redirect proxy is an `IDisposable` then **only** the proxy instance is disposed by the DI container and not the root. In this case the responsibilty is left to the proxy for forwarding the dispose call to its root (and it does this by default). -The above also applies to `IAsyncDisposable`. +The above also applies to `IAsyncDisposable`. \ No newline at end of file diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md index c045da98..a0466680 100644 --- a/docs/quickstart/index.md +++ b/docs/quickstart/index.md @@ -21,39 +21,40 @@ Or via the .NET command line interface: dotnet add package DivertR ``` -## Basic Usage +## Creating Proxies + +The `Redirect` class is used to create and manage DivertR proxies. Its basic usage is similar to other common mocking frameworks: -On the surface DivertR provides familiar proxy ```csharp using DivertR; -public class RedirectExample +public class QuickstartExample { [Fact] - public void Test() + public void RedirectTestSample() { // Create a Foo instance named "MrFoo" IFoo foo = new Foo("MrFoo"); Assert.Equal("MrFoo", foo.Name); - // Create a DivertR IFoo Redirect + // Create an IFoo Redirect var fooRedirect = new Redirect(); - // Use the Redirect to create an IFoo proxy that wraps the Foo instance above as its root target. + // Use the Redirect to create an IFoo proxy that wraps the Foo instance above as its root target IFoo fooProxy = fooRedirect.Proxy(foo); - // By default proxies are transparent and behave exactly the same as the root target. + // By default proxies are transparent and behave exactly the same as the root target Assert.Equal("MrFoo", fooProxy.Name); - // Intercept proxy calls and change behaviour by configuring one or more 'Vias' on the Redirect. + // Intercept proxy calls and change behaviour by configuring one or more 'Vias' on the Redirect fooRedirect .To(x => x.Name) .Via(() => "redirected"); Assert.Equal("redirected", fooProxy.Name); - // Reset the Redirect and revert the proxy to its default transparent behaviour. + // Reset the Redirect and revert the proxy to its default transparent behaviour fooRedirect.Reset(); Assert.Equal("MrFoo", fooProxy.Name); @@ -64,11 +65,11 @@ public class RedirectExample Assert.Equal("MrFoo redirected", fooProxy.Name); - // A Redirect can create any number of proxies. + // A Redirect can create any number of proxies var fooTwo = new Foo("FooTwo"); IFoo fooTwoProxy = fooRedirect.Proxy(fooTwo); - // Its configured Vias are applied to all existing and new proxies. + // Its configured Vias are applied to all existing and new proxies Assert.Equal("FooTwo redirected", fooTwoProxy.Name); // Reset is applied to all proxies. @@ -76,7 +77,7 @@ public class RedirectExample Assert.Equal("MrFoo", fooProxy.Name); Assert.Equal("FooTwo", fooTwoProxy.Name); - // A proxy with no root target returns default values. + // A proxy with no root target returns default values var fooMock = fooRedirect.Proxy(); Assert.Null(fooMock.Name); @@ -87,7 +88,7 @@ public class RedirectExample Assert.Equal("FooTwo", fooTwoProxy.Name); Assert.Null(fooMock.Name); - // Take an immutable snapshot of the currently recorded calls to verify against. + // Take an immutable snapshot of the currently recorded calls to verify against var snapshotCalls = fooCalls.To(x => x.Name).Verify(); Assert.Equal(3, snapshotCalls.Count); @@ -103,3 +104,110 @@ public class RedirectExample } ``` +## Dependency Injection Integration + +DivertR is designed to be embedded easily and transparently into `Microsoft.Extensions.DependencyInjection.IServiceCollection` dependency injection containers. + +```csharp + +[Fact] +public void ServiceCollectionDemoTest() +{ + // Instantiate a Microsoft.Extensions.DependencyInjection.IServiceCollection + IServiceCollection services = new ServiceCollection(); + + // Register some services + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + + // Instantiate a Diverter instance + var diverter = new Diverter(); + + // Register the services you want to be able to redirect + diverter + .Register() + .Register(); + + // Install DivertR into the ServiceCollection + services.Divert(diverter); + + // Build an IServiceProvider as usual + IServiceProvider provider = services.BuildServiceProvider(); + + // Resolve services from the ServiceProvider as usual + var foo = provider.GetRequiredService(); + var fooTwo = provider.GetRequiredService(); + + // At this stage DivertR is transparent and the behaviour of resolved services is unchanged + fooTwo.Name = "FooTwo"; + Assert.Equal("original", foo.Name); + Assert.Equal("FooTwo", fooTwo.Name); + + // Get a Redirect from the Diverter instance and configure a Via + diverter + .Redirect() + .To(x => x.Name) + .Via(call => $"{call.Next.Name} redirected"); + + // The behaviour of resolved service instances is now changed + Assert.Equal("original redirected", foo.Name); + Assert.Equal("FooTwo redirected", fooTwo.Name); + + // Reset the Diverter instance + diverter.ResetAll(); + + // The original service behaviour is restored + Assert.Equal("original", foo.Name); + Assert.Equal("FooTwo", fooTwo.Name); +} +``` + +## WebApplicationFactory Integration + +DivertR is also designed to integrate with Microsoft's [WebApplicationFactory (TestServer)](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests) and facilitates writing tests on a wired-up system like this: + +```csharp +[Fact] +public async Task GivenFooExistsInRepo_WhenGetFoo_ThenReturnsFoo_WithOk200() +{ + // ARRANGE + var foo = new Foo + { + Id = Guid.NewGuid(), + Name = "Foo123" + }; + + _diverter + .Redirect() // Redirect IFooRepository calls + .To(x => x.GetFooAsync(foo.Id)) // matching this method and argument + .Via(() => Task.FromResult(foo)); // via this delegate + + // ACT + var response = await _fooClient.GetFooAsync(foo.Id); + + // ASSERT + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Content.Id.ShouldBe(foo.Id); + response.Content.Name.ShouldBe(foo.Name); +} + +[Fact] +public async Task GivenFooRepoException_WhenGetFoo_ThenReturns500InternalServerError() +{ + // ARRANGE + _diverter + .Redirect() + .To(x => x.GetFooAsync(Is.Any)) + .Via(() => throw new Exception()); + + // ACT + var response = await _fooClient.GetFooAsync(Guid.NewGuid()); + + // ASSERT + response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); +} +``` + +For more examples and a demonstration of setting up a test harness for a WebApp like this see a [WebApp Testing Sample here](https://github.com/devodo/DivertR/tree/main/test/DivertR.WebAppTests). + diff --git a/docs/quickstart/proxy_factory.md b/docs/quickstart/proxy_factory.md new file mode 100644 index 00000000..dceeb8a5 --- /dev/null +++ b/docs/quickstart/proxy_factory.md @@ -0,0 +1,14 @@ +--- +layout: default +title: Proxy Factory +nav_order: 4 +parent: Quickstart +--- + +# Proxy Factory + +The default DivertR proxy factory is implemented using [System.Reflection.DispatchProxy](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.dispatchproxy). +As this is part of the .NET Standard 2.1, DivertR therefore does not have any dependencies on external libraries however it is limited to proxying **interface types** only. + +If class proxies are required, [DivertR.DynamicProxy](../src/DivertR.DynamicProxy/README.md) is an alternative proxy factory implemented using [Castle DynamicProxy](http://www.castleproject.org/projects/dynamicproxy/) that does support classes. +However, when proxying classes, care should be taken as only calls to non-virtual members cannot be intercepted, this can cause inconsistent behaviour e.g. when wrapping root instances. diff --git a/docs/Redirect.md b/docs/quickstart/redirects.md similarity index 94% rename from docs/Redirect.md rename to docs/quickstart/redirects.md index 7dc07c92..54fb2b59 100644 --- a/docs/Redirect.md +++ b/docs/quickstart/redirects.md @@ -1,13 +1,20 @@ -# Redirect +--- +layout: default +title: Redirects +nav_order: 1 +parent: Quickstart +--- -Redirects are the main DivertR entities used to create and configure proxies. +# Redirects + +The `Redirect` is the main DivertR entity used to create and configure proxies. `Redirect` instances are instantiated from the generic `Redirect` class: ```csharp IRedirect fooRedirect = new Redirect(); ``` -# Proxy +# Creating Proxies A `Redirect` creates proxy objects of its generic `TTarget` type. E.g. an `IRedirect` like the one instantiated above creates `IFoo` proxies: @@ -19,7 +26,7 @@ IFoo fooProxy = fooRedirect.Proxy(); // Create a proxy IFoo fooTwo = fooRedirect.Proxy(); // Create another proxy ``` -> A single Redirect can create any number of proxies. +> A single Redirect can create any number of proxies. ## Proxy Root @@ -52,7 +59,7 @@ In general dummy roots return the .NET `default` of the call's return type, e.g. There are some special cases such as `Task` types are returned as `null` valued completed Tasks. > Proxies with dummy roots can be used as mock objects. -# Via +# Via Intercepts `Via` instances are added to a `Redirect` to control the way its proxies behave. Proxy calls are diverted and passed to the Vias for handling. @@ -97,11 +104,11 @@ By adding Vias and resetting, proxy behaviour can be modified at runtime allowin After a Redirect is reset its proxies are in their default, transparent state of forwarding all calls to their root instances. This enables a pattern of testing where proxy behaviour is modified with Vias and then the system is reset to its original state between tests. -# Method parameters +# Method Parameters Via intercept match rules can be configured on method parameters using call argument values and these can also be passed to Via delegates. -## Parameter matching +## Parameter Matching If the Via `To` expression specifies a method with parameters, these are matched to call arguments as follows: @@ -126,7 +133,7 @@ Console.WriteLine(fooProxy.Echo("two")); // "match" Console.WriteLine(fooProxy.Echo("three")); // "equal" ``` -## Call arguments +## Call Arguments Proxy call arguments can be passed to the Via delegate as follows: @@ -137,9 +144,9 @@ fooRedirect Console.WriteLine(fooProxy.Echo("me")); // "me viaed" ``` -> The `Args` property is an `IReadOnlyList` collection. +> The `Args` property is an `IReadOnlyList` collection. -## Named arguments +## Named Arguments Strongly typed and named arguments can be specified by defining a `ValueTuple` generic type on the `Via` method as follows: @@ -160,9 +167,9 @@ then the discard type `__` must be used to provide a second dummy parameter. # Relay -A special feature of Redirects is their ability to control how calls are forwarded or *relayed* back to proxy root instances. +A special feature of Redirects is their ability to control how calls are forwarded or *relayed* back to proxy root instances. -## Relay root +## Relay Root The Via delegate can *relay* calls back to the proxy root by calling the `Relay.Root` property: @@ -181,11 +188,11 @@ Console.WriteLine(fooProxy.Name); // "MrFoo relayed" > The `Relay.Root` property is a proxy that relays calls to the root instance. -## Relay next +## Relay Next Any number of Vias can be added to a Redirect. When Vias are added they are pushed onto a stack (with the last added at the top). -![Via Stack](./assets/images/Via_Stack.svg) +![Via Stack]({{ site.url }}/assets/images/Via_Stack.svg) Proxy calls are traversed through the stack from top to bottom. If a call matches the `To` constraint it is passed to the Via delegate for handling. If no Vias match, the call falls through the stack to the root instance. @@ -208,7 +215,7 @@ Console.WriteLine(fooProxy.Name); // "MrFoo 1 2 3" > If no Vias match it will relay to the root. > The Root and Next properties can also be accessed directly from the call argument for convenience. -## Call forwarding +## Call Forwarding A Via can call `CallRoot()` to forward the call to the target method of the root instance: @@ -253,9 +260,9 @@ fooRedirect Console.WriteLine(fooProxy.Echo("me")); // "you" ``` -# Method variations +# Additional Usages -## Async methods +## Async Methods Async is fully supported by DivertR and Via delegates can be added to `Task` or `ValueTask` methods using the standard C# `async` syntax: @@ -297,7 +304,7 @@ fooProxy.Name = "Me"; Console.WriteLine(fooProxy.Name); // "Me changed" ``` -## Void methods +## Void Methods For methods that return `void`, the same `Redirect` fluent interface syntax is used, only the `Via` delegate provided is an `Action` rather than a `Func`: @@ -310,7 +317,7 @@ fooRedirect }); ``` -## Generic methods +## Generic Methods Generic method Vias are declared using the same fluent syntax ands are matched on the specified generic type arguments. @@ -320,7 +327,7 @@ fooRedirect .Via(call => call.CallNext() * 2); ``` -## Throwing exceptions +## Throwing Exceptions Via delegates can throw exceptions using standard C# syntax and any exceptions thrown will bubble up to callers as usual: @@ -392,4 +399,4 @@ fooProxy.Echo("ok"); // "ok" When strict mode is enabled a `StrictNotSatisfiedException` is thrown if a call is made to a proxy and does not match any Vias. -Strict mode is disabled when a Redirect is created or [reset](#reset). +Strict mode is disabled when a Redirect is created or [reset](#reset). \ No newline at end of file diff --git a/docs/Verify.md b/docs/quickstart/verify.md similarity index 91% rename from docs/Verify.md rename to docs/quickstart/verify.md index 75c8e208..6b0d3a9f 100644 --- a/docs/Verify.md +++ b/docs/quickstart/verify.md @@ -1,8 +1,15 @@ -# Recording and Verifying Calls +--- +layout: default +title: Verifying Calls +nav_order: 3 +parent: Quickstart +--- + +# Verifying Calls DivertR can record the details of calls to its proxies and this can be used for test spying and verification. -## Record +# Record The Redirect fluent interface is used to start a recording of proxy calls that match a `To` expression: @@ -29,32 +36,12 @@ for(var call in echoCalls) The `Record` method returns an `ICallStream` that recorded called are appended to. This variable is an `IReadOnlyCollection` and `IEnumerable` that can be enumerated or queried e.g. with standard Linq expressions. -## Verify +# Verify snapshot The `ICallStream` interface provides `Verify` helper methods to facilitate iteration and verification over the recorded calls collection. -```csharp -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(new Foo()); - -var nameCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Record(); - -var result = fooProxy.Echo("record"); - -var verifySnapshot = nameCalls.Verify(call => -{ - call.Args[0].ShouldBe("record"); - call.Returned.Value.ShouldBe("record"); -}); +Recorded calls are appended to the `ICallStream` whenever a matching proxy call is made. This means the `ICallStream` will hold different record data at different points in time as calls are happening. -verifySnapshot.Count.ShouldBe(1); // The verify snapshot records calls at a point in time and is immutable -``` - -### Verify snapshot - -Recorded calls are appended to the `ICallStream` whenever a matching proxy call is made. This means the `ICallStream` may hold different record data at different points in time. Therefore the `Verify` method iterates over and returns an immutable snapshot of the collection of recorded calls at the point of time it is called. This allows performing multiple, consistent operations on a stable set of data. E.g. in the example above iterating over the collection and then verifying the call count. @@ -80,7 +67,30 @@ Console.WriteLine(verifyCalls.Count); // 1 Console.WriteLine(verifyCalls[0].Args[0]); // one ``` -## Record chaining +# Verify Visitor + +The `ICallStream` interface provides `Verify` helper methods to facilitate iteration and verification over the recorded calls collection. + +```csharp +var fooRedirect = new Redirect(); +var fooProxy = fooRedirect.Proxy(new Foo()); + +var nameCalls = fooRedirect + .To(x => x.Echo(Is.Any)) + .Record(); + +var result = fooProxy.Echo("record"); + +var verifySnapshot = nameCalls.Verify(call => +{ + call.Args[0].ShouldBe("record"); + call.Returned.Value.ShouldBe("record"); +}); + +verifySnapshot.Count.ShouldBe(1); // The verify snapshot records calls at a point in time and is immutable +``` + +# Record chaining The Redirect fluent interface allows chaining the `Record` method after a `Via` call: @@ -99,7 +109,7 @@ nameCalls.Verify(call => }).Count.ShouldBe(1); ``` -## Verify named arguments +# Named Arguments The `Verify` methods allows specifying call argument types and names using the same Via [`ValueTuple` syntax](#named-arguments): @@ -150,7 +160,7 @@ nameCalls.Verify(call => }).Count.ShouldBe(1); ``` -## Recording exceptions +# Recording exceptions ```csharp var nameCalls = fooRedirect @@ -176,7 +186,7 @@ nameCalls.Verify(call => }).Count.ShouldBe(1); ``` -## Redirect Record +# Redirect Record ```csharp var fooRedirect = new Redirect(); @@ -196,7 +206,7 @@ fooCalls }).Count.ShouldBe(1); ``` -## Record ordering +# Record ordering ```csharp var fooRedirect = new Redirect(); @@ -217,4 +227,4 @@ fooCalls call.Args.input.ShouldBe("record"); call.Returned.Value.ShouldBe("record viaed"); }).Count.ShouldBe(1); -``` +``` \ No newline at end of file diff --git a/docs/redirect/index.md b/docs/redirect/index.md deleted file mode 100644 index 72f55616..00000000 --- a/docs/redirect/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -layout: default -title: Redirects -nav_order: 2 -has_children: true ---- - -# Redirects - -The `Redirect` is the main DivertR type used to create and configure test proxies. -`Redirect` instances are instantiated from the generic `Redirect` class: - -```csharp -IRedirect fooRedirect = new Redirect(); -``` diff --git a/docs/redirect/method_parameters.md b/docs/redirect/method_parameters.md deleted file mode 100644 index 4a84e8cf..00000000 --- a/docs/redirect/method_parameters.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -layout: default -title: Method Parameters -nav_order: 4 -parent: Redirects ---- - -# Method parameters - -Via intercept match rules can be configured on method parameters using call argument values and these can also be passed to Via delegates. - -## Parameter matching - -If the Via `To` expression specifies a method with parameters, these are matched to call arguments as follows: - -```csharp -// Match calls to the Echo method with any argument value -fooRedirect - .To(x => x.Echo(Is.Any)) - .Via(() => "any"); - -// Match calls with arguments that satisfy a Match expression -fooRedirect - .To(x => x.Echo(Is.Match(a => a == "two"))) - .Via(() => "match"); - -// Match calls with arguments equal to a specified value -fooRedirect - .To(x => x.Echo("three")) - .Via(() => "equal"); - -Console.WriteLine(fooProxy.Echo("one")); // "any" -Console.WriteLine(fooProxy.Echo("two")); // "match" -Console.WriteLine(fooProxy.Echo("three")); // "equal" -``` - -## Call arguments - -Proxy call arguments can be passed to the Via delegate as follows: - -```csharp -fooRedirect - .To(x => x.Echo(Is.Any)) - .Via(call => $"{call.Args[0]} viaed"); - -Console.WriteLine(fooProxy.Echo("me")); // "me viaed" -``` -> The `Args` property is an `IReadOnlyList` collection. - -## Named arguments - -Strongly typed and named arguments can be specified by defining a `ValueTuple` generic type on the `Via` method as follows: - -```csharp -fooRedirect - .To(x => x.Echo(Is.Any)) - .Via<(string input, __)>(call => $"{call.Args.input} viaed"); - -Console.WriteLine(fooProxy.Echo("me")); // "me viaed" -``` - -Call arguments are mapped in parameter order onto the `ValueTuple` items and it replaces the `Args` property from the previous example. - -The special Diverter type `__` (double underscore) is used to specify a discard mapping that is ignored so that only named types of parameters that will be used need to be defined. - -C# requires named ValueTuples to have at least two parameters. If the call only has a single parameter, as in the example above, -then the discard type `__` must be used to provide a second dummy parameter. diff --git a/docs/redirect/method_variations.md b/docs/redirect/method_variations.md deleted file mode 100644 index 5ed2d259..00000000 --- a/docs/redirect/method_variations.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -layout: default -title: Method Variations -nav_order: 6 -parent: Redirects ---- - -# Method variations - -## Async methods - -Async is fully supported by DivertR and Via delegates can be added to `Task` or `ValueTask` methods using the standard C# `async` syntax: - -```csharp -fooRedirect - .To(x => x.SaveAsync(Is.Any, Is.Any)) - .Via(async call => - { - var result = await call.CallNext(); - - return result; - }); -``` - -## Property Getters and Setters - -Vias for property getters, [demonstrated earlier](#via), are added using the same `To` syntax as for standard methods. However, to indicate a Via is for a property setter, the `ToSet` method is used instead: - -```csharp -fooRedirect - .ToSet(x => x.Name) - .Via<(string name, __)>(call => - { - call.Next.Name = call.Args.name + " changed"; - }); -``` - -By default the Via above will match any setter value input but the `ToSet` method accepts a second parameter as a value match expression using the usual [parameter matching](#parameter-matching) syntax: - -```csharp -fooRedirect - .ToSet(x => x.Name, () => Is.Match(p => p.StartsWith("M"))) - .Via<(string name, __)>(call => - { - call.Next.Name = name + " changed"; - }); - -fooProxy.Name = "Me"; -Console.WriteLine(fooProxy.Name); // "Me changed" -``` - -## Void methods - -For methods that return `void`, the same `Redirect` fluent interface syntax is used, only the `Via` delegate provided is an `Action` rather than a `Func`: - -```csharp -fooRedirect - .To(x => x.SetAge(Is.Any)) - .Via<(int age, __)>(call => - { - call.Next.SetAge(call.Args.ags + 10); - }); -``` - -## Generic methods - -Generic method Vias are declared using the same fluent syntax ands are matched on the specified generic type arguments. - -```csharp -fooRedirect - .To(x => x.Echo(Is.Any)) - .Via(call => call.CallNext() * 2); -``` - -## Throwing exceptions - -Via delegates can throw exceptions using standard C# syntax and any exceptions thrown will bubble up to callers as usual: - -```csharp -fooRedirect - .To(x => x.Echo("exception")) - .Via(() => throw new MyException()) - -fooProxy.Echo("exception"); // throws MyException -``` diff --git a/docs/redirect/proxy.md b/docs/redirect/proxy.md deleted file mode 100644 index 5d1184c0..00000000 --- a/docs/redirect/proxy.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -layout: default -title: Creating Proxies -nav_order: 1 -parent: Redirects ---- - -# Creating Proxies - -`Redirec` instances are used to create and manage proxy objects. A `Redirect` creates proxies of its generic `TTarget` type. -E.g. an `IRedirect` like the one instantiated in previously creates `IFoo` proxies: - -```csharp -var fooRedirect = new Redirect(); - -// Create a proxy -IFoo fooProxy = fooRedirect.Proxy(); -// Create another proxy -IFoo fooTwo = fooRedirect.Proxy(); -``` -{: .note-title } -> Note -> -> A single Redirect can create any number of proxies of its target type. - -## Proxy Root - -When a proxy object is created it can be given a *root* instance of its target type. -The default behaviour of a proxy is to relay all calls to its root: - -```csharp -IFoo fooRoot = new Foo("MrFoo"); -Console.WriteLine(fooRoot.Name); // "MrFoo" - -var fooRedirect = new Redirect(); -IFoo fooProxy = fooRedirect.Proxy(fooRoot); -Console.WriteLine(fooProxy.Name); // "MrFoo" -``` - -When a proxy is in its initial state it is transparent, i.e. it behaves identically to its root instance. Therefore if a system has its instances replaced with transparent proxies its original behaviour is left unchanged. - -## Dummy Root - -If no root instance is provided then the proxy is created with a *dummy* root that returns default values: - -```csharp -var fooRedirect = new Redirect(); - -var fooMock = fooRedirect.Proxy(); // Proxy created with dummy root -Console.WriteLine(fooMock.Name); // null -``` - -In general dummy roots return the .NET `default` of the call's return type, e.g. `null` for reference types and `0` for `int`. -There are some special cases such as `Task` types are returned as `null` valued completed Tasks. -> Proxies with dummy roots can be used as mock objects. diff --git a/docs/redirect/relay.md b/docs/redirect/relay.md deleted file mode 100644 index 422be096..00000000 --- a/docs/redirect/relay.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -layout: default -title: Relay -nav_order: 5 -parent: Redirects ---- - -# Relay - -A special feature of Redirects is their ability to control how calls are forwarded or *relayed* back to proxy root instances. - -## Relay root - -The Via delegate can *relay* calls back to the proxy root by calling the `Relay.Root` property: - -```csharp -fooRedirect - .To(x => x.Name) - .Via(call => - { - IFoo root = call.Relay.Root; - return $"{root.Name} relayed"; - }); - -Console.WriteLine(fooRoot.Name); // "MrFoo" -Console.WriteLine(fooProxy.Name); // "MrFoo relayed" -``` - -> The `Relay.Root` property is a proxy that relays calls to the root instance. - -## Relay next - -Any number of Vias can be added to a Redirect. When Vias are added they are pushed onto a stack (with the last added at the top). - -![Via Stack]({{ site.url }}/assets/images/Via_Stack.svg) - -Proxy calls are traversed through the stack from top to bottom. If a call matches the `To` constraint it is passed to the Via delegate for handling. -If no Vias match, the call falls through the stack to the root instance. - -Via delegates can relay the call directly to the root as in the previous example -but they can also continue the call down the Via stack by calling the `Relay.Next` property as follows: - -```csharp -fooRedirect - .To(x => x.Name) - .Via(call => $"{call.Relay.Next.Name} 1") - .Via(call => $"{call.Relay.Next.Name} 2") // Via calls can be chained - .Via(call => $"{call.Next.Name} 3"); // The Root and Next properties can be accessed directly from the call argument - -Console.WriteLine(fooRoot.Name); // "MrFoo" -Console.WriteLine(fooProxy.Name); // "MrFoo 1 2 3" -``` - -> The `Relay.Next` property is a proxy that relays calls to the next Via that matches. -> If no Vias match it will relay to the root. -> The Root and Next properties can also be accessed directly from the call argument for convenience. - -## Call forwarding - -A Via can call `CallRoot()` to forward the call to the target method of the root instance: - -```csharp -fooRedirect - .To(x => x.Name) - .Via(call => call.CallRoot() + " 1"); - -Console.WriteLine(fooRoot.Name); // "MrFoo" -Console.WriteLine(fooProxy.Name); // "MrFoo 1" -``` - -Or the call can be forwarded down the Via stack using `CallNext()`: - -```csharp -fooRedirect - .To(x => x.Name) - .Via(call => call.CallNext() + " 1") - .Via(call => call.CallNext() + " 2"); - -Console.WriteLine(fooRoot.Name); // "MrFoo" -Console.WriteLine(fooProxy.Name); // "MrFoo 1 2" -``` - -If the target method has parameters then `CallRoot()` and `CallNext()` forward the arguments from the original call: - -```csharp -fooRedirect - .To(x => x.Echo(Is.Any)) - .Via(call => call.CallNext() + " and you"); - -Console.WriteLine(fooProxy.Echo("me")); // "me and you" -``` - -Custom arguments can be forwarded by passing an `object[]` to `CallRoot()` or `CallNext()`: - -```csharp -fooRedirect - .To(x => x.Echo(Is.Any)) - .Via(call => call.CallNext(new[] { "you" })); - -Console.WriteLine(fooProxy.Echo("me")); // "you" -``` diff --git a/docs/redirect/reset.md b/docs/redirect/reset.md deleted file mode 100644 index f5209377..00000000 --- a/docs/redirect/reset.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: default -title: Reset -nav_order: 3 -parent: Redirects ---- - -# Reset - -A Redirect can be *reset* which removes all its Vias, reverting its proxies to their original behaviour: - -```csharp -fooRedirect.To(x => x.Name).Via("diverted"); -Console.WriteLine(fooProxy.Name); // "diverted" - -fooRedirect.Reset(); - -Console.WriteLine(fooProxy.Name); // "MrFoo" -``` - -Reset can be called at any time and is applied immediately to all of the Redirect's proxies. -By adding Vias and resetting, proxy behaviour can be modified at runtime allowing a running process to be altered between tests, e.g. to avoid restart and initialisation overhead. - -After a Redirect is reset its proxies are in their default, transparent state of forwarding all calls to their root instances. -This enables a pattern of testing where proxy behaviour is modified with Vias and then the system is reset to its original state between tests. diff --git a/docs/redirect/retarget.md b/docs/redirect/retarget.md deleted file mode 100644 index b303c0cf..00000000 --- a/docs/redirect/retarget.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -layout: default -title: Retarget -nav_order: 7 -parent: Redirects ---- - -# Retarget - -A Redirect can be configured to *retarget* its proxy calls to a substitute instance of its target type: - -```csharp -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(new Foo("MrFoo")); -Console.WriteLine(fooProxy.Name); // "MrFoo" - -var fooTwo = new Foo("two"); -Console.WriteLine(fooTwo.Name); // "two" - -fooRedirect.Retarget(fooTwo); - -Console.WriteLine(fooProxy.Name); // "two" -``` - -The retarget substitute can be any instance of the Redirect's target type including e.g. Mock objects: - -```csharp -var mock = new Mock(); -mock - .Setup(x => x.Echo(It.IsAny())) - .Returns((string input) => $"{input} mock"); - -fooRedirect.Retarget(mock.Object); - -Console.WriteLine(fooProxy.Echo("hello")); // "hello mock" -``` - -When a `Retarget` is added it is also pushed onto the Redirect Via stack. -Retarget substitutes are also able to relay calls to the proxy root or next Via from the Redirect's `Relay` property: - -```csharp -IFoo next = fooRedirect.Relay.Next; -IFoo root = fooRedirect.Relay.Root; -mock - .Setup(x => x.Name) - .Returns(() => $"{root.Name} {next.Name} mock"); - -Console.WriteLine(fooProxy.Name); // "MrFoo two mock" -``` diff --git a/docs/redirect/strict_mode.md b/docs/redirect/strict_mode.md deleted file mode 100644 index 4022c9d3..00000000 --- a/docs/redirect/strict_mode.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: default -title: Strict Mode -nav_order: 7 -parent: Redirects ---- - -# Strict mode - -Enable *strict mode* on a Redirect to ensure only methods with matching Vias are allowed to be called: - -```csharp -fooRedirect.Strict(); // enables strict mode - -fooRedirect - .To(x => x.Echo("ok")) - .Via(call => call.Args[0]); - -fooProxy.Echo("me"); // throws StrictNotSatisfiedException -fooProxy.Echo("ok"); // "ok" -``` - -When strict mode is enabled a `StrictNotSatisfiedException` is thrown if a call is made to a proxy and does not match any Vias. - -Strict mode is disabled when a Redirect is created or [reset](#reset). diff --git a/docs/redirect/via.md b/docs/redirect/via.md deleted file mode 100644 index 0d96e29b..00000000 --- a/docs/redirect/via.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -layout: default -title: Via -nav_order: 2 -parent: Redirects ---- - -# Via - -`Via` instances are added to a `Redirect` to control the way its proxies behave. -Proxy calls are diverted and passed to the Vias for handling. -A fluent interface is provided on the Redirect for building and adding Vias to itself: - -```csharp -var foo = new Foo("MrFoo"); -Console.WriteLine(foo.Name); // "MrFoo" - -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(foo); -Console.WriteLine(fooProxy.Name); // "MrFoo" - -// Add a Via to the Redirect -fooRedirect - .To(x => x.Name) // 1. Match expression - .Via(() => "Hello Via"); // 2. Via delegate - -Console.WriteLine(fooProxy.Name); // "Hello Via" -``` - -The Via intercepts any proxy calls matching the `To` expression 1. and diverts them to the `Via` delegate 2. - -Vias can be added to a Redirect at any time and apply immediately to all its existing proxies as well as any created afterwards. diff --git a/docs/test_doubles/index.md b/docs/test_doubles/index.md deleted file mode 100644 index f10921b5..00000000 --- a/docs/test_doubles/index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -layout: default -title: DivertR Proxies -nav_order: 2 -has_children: true ---- - -# DivertR Proxies - -DivertR is a proxy framework for creating and configuring test doubles like mocks. - -1. The `Redirect` class is used to create proxies. -2. Configure proxy behaviour by adding `Vias` to the `Redirect`. -3. Delete `Vias` to `Reset` proxy behaviour between tests. - - - diff --git a/docs/test_doubles/redirects.md b/docs/test_doubles/redirects.md deleted file mode 100644 index a7570f0f..00000000 --- a/docs/test_doubles/redirects.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -layout: default -title: Redirecting -nav_order: 1 -parent: DivertR Proxies ---- - -# Redirects - -Redirects are the main DivertR entities used to create and configure proxies. -`Redirect` instances are instantiated from the generic `Redirect` class: - -```csharp -IRedirect fooRedirect = new Redirect(); -``` - -# Creating Proxies - -A `Redirect` creates proxy objects of its generic `TTarget` type. -E.g. an `IRedirect` like the one instantiated above creates `IFoo` proxies: - -```csharp -var fooRedirect = new Redirect(); - -IFoo fooProxy = fooRedirect.Proxy(); // Create a proxy -IFoo fooTwo = fooRedirect.Proxy(); // Create another proxy -``` - -> A single Redirect can create any number of proxies. - -## Proxy Root - -When a proxy object is created it can be given a *root* instance of its target type. -The default behaviour of a proxy is to relay all calls to its root: - -```csharp -IFoo fooRoot = new Foo("MrFoo"); -Console.WriteLine(fooRoot.Name); // "MrFoo" - -var fooRedirect = new Redirect(); -IFoo fooProxy = fooRedirect.Proxy(fooRoot); -Console.WriteLine(fooProxy.Name); // "MrFoo" -``` - -When a proxy is in its initial state it is transparent, i.e. it behaves identically to its root instance. Therefore if a system has its instances replaced with transparent proxies its original behaviour is left unchanged. - -## Dummy Root - -If no root instance is provided then the proxy is created with a *dummy* root that returns default values: - -```csharp -var fooRedirect = new Redirect(); - -var fooMock = fooRedirect.Proxy(); // Proxy created with dummy root -Console.WriteLine(fooMock.Name); // null -``` - -In general dummy roots return the .NET `default` of the call's return type, e.g. `null` for reference types and `0` for `int`. -There are some special cases such as `Task` types are returned as `null` valued completed Tasks. -> Proxies with dummy roots can be used as mock objects. - -# Via - -`Via` instances are added to a `Redirect` to control the way its proxies behave. -Proxy calls are diverted and passed to the Vias for handling. -A fluent interface is provided on the Redirect for building and adding Vias to itself: - -```csharp -var foo = new Foo("MrFoo"); -Console.WriteLine(foo.Name); // "MrFoo" - -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(foo); -Console.WriteLine(fooProxy.Name); // "MrFoo" - -// Add a Via to the Redirect -fooRedirect - .To(x => x.Name) // 1. Match expression - .Via(() => "Hello Via"); // 2. Via delegate - -Console.WriteLine(fooProxy.Name); // "Hello Via" -``` - -The Via intercepts any proxy calls matching the `To` expression 1. and diverts them to the `Via` delegate 2. - -Vias can be added to a Redirect at any time and apply immediately to all its existing proxies as well as any created afterwards. - -# Reset - -A Redirect can be *reset* which removes all its Vias, reverting its proxies to their original behaviour: - -```csharp -fooRedirect.To(x => x.Name).Via("diverted"); -Console.WriteLine(fooProxy.Name); // "diverted" - -fooRedirect.Reset(); - -Console.WriteLine(fooProxy.Name); // "MrFoo" -``` - -Reset can be called at any time and is applied immediately to all of the Redirect's proxies. -By adding Vias and resetting, proxy behaviour can be modified at runtime allowing a running process to be altered between tests, e.g. to avoid restart and initialisation overhead. - -After a Redirect is reset its proxies are in their default, transparent state of forwarding all calls to their root instances. -This enables a pattern of testing where proxy behaviour is modified with Vias and then the system is reset to its original state between tests. \ No newline at end of file diff --git a/docs/verify/index.md b/docs/verify/index.md deleted file mode 100644 index 8d787370..00000000 --- a/docs/verify/index.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: default -title: Verifying Calls -nav_order: 4 -has_children: true ---- - -# Verifying Calls - -DivertR can record the details of calls to its proxies and this can be used for test spying and verification. diff --git a/docs/verify/named_arguments.md b/docs/verify/named_arguments.md deleted file mode 100644 index ff7ebfed..00000000 --- a/docs/verify/named_arguments.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -layout: default -title: Named Arguments -nav_order: 4 -parent: Verifying Calls ---- - -# Named Arguments - -The `Verify` methods allows specifying call argument types and names using the same Via [`ValueTuple` syntax](#named-arguments): - -```csharp -var nameCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Record(); - -var result = fooProxy.Echo("record example"); - -nameCalls.Verify<(string input, __)>(call => -{ - call.Args.input.ShouldBe("record example"); - call.Returned.Value.ShouldBe("record example echo"); -}).Count.ShouldBe(1); -``` - -The argument `ValueTuple` can also be defined on the `Record` method and gets passed through to `Verify` calls: - -```csharp -var nameCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Record<(string input, __)>(); - -var result = fooProxy.Echo("record example"); - -nameCalls.Verify(call => -{ - call.Args.input.ShouldBe("record example"); - call.Returned.Value.ShouldBe("record example echo"); -}).Count.ShouldBe(1); -``` - -Finally if the argument `ValueTuple` is defined on a chained `Via` the strongly typed argument information is passed through to the `Record` and can be used in the `Verify` calls: - -```csharp -var nameCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Via<(string input, __)>(call => $"{call.Args.input} viaed") - .Record(); - -var result = fooProxy.Echo("record example"); - -nameCalls.Verify(call => -{ - call.Args.input.ShouldBe("record example"); - call.Returned.Value.ShouldBe("record example viaed"); -}).Count.ShouldBe(1); -``` diff --git a/docs/verify/record.md b/docs/verify/record.md deleted file mode 100644 index 0db78378..00000000 --- a/docs/verify/record.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -layout: default -title: Record -nav_order: 1 -parent: Verifying Calls ---- - -# Record - -The Redirect fluent interface is used to start a recording of proxy calls that match a `To` expression: - -```csharp -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(new Foo()); - -var echoCalls = fooRedirect - .To(x => x.Echo(Is.Any)) // Call match expression - .Record(); // Returns an ICallStream collection of all recorded calls - -var result = fooProxy.Echo("record test"); -Console.WriteLine(result); // "record test" - -// ICallStream is an `IReadOnlyCollection` -Console.WriteLine(echoCalls.Count); // 1 - -for(var call in echoCalls) -{ - Console.WriteLine(call.Args[0]); // "record test" - Console.WriteLine(call.Returned.Value); // "record test" -} -``` - -The `Record` method returns an `ICallStream` that recorded called are appended to. This variable is an `IReadOnlyCollection` and `IEnumerable` that can be enumerated or queried e.g. with standard Linq expressions. diff --git a/docs/verify/record_chaining.md b/docs/verify/record_chaining.md deleted file mode 100644 index 3a65b9fc..00000000 --- a/docs/verify/record_chaining.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: default -title: Record Chaining -nav_order: 3 -parent: Verifying Calls ---- - -# Record chaining - -The Redirect fluent interface allows chaining the `Record` method after a `Via` call: - -```csharp -var nameCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Via(call => call.CallNext() + " viaed") - .Record(); - -var result = fooProxy.Echo("record"); - -nameCalls.Verify(call => -{ - call.Args[0].ShouldBe("record"); - call.Returned.Value.ShouldBe("record viaed"); -}).Count.ShouldBe(1); -``` diff --git a/docs/verify/record_ordering.md b/docs/verify/record_ordering.md deleted file mode 100644 index f994b690..00000000 --- a/docs/verify/record_ordering.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -layout: default -title: Record Ordering -nav_order: 7 -parent: Verifying Calls ---- - -# Record ordering - -```csharp -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(new Foo()); - -var fooCalls = fooRedirect.Record(opt => opt.OrderFirst()); - -fooRedirect - .To(x => x.Echo(Is.Any)) - .Via<(string input, __)>(call => call.CallNext() + " viaed") - -fooProxy.Echo("record"); - -fooCalls - .To(x => x.Echo(Is.Any)) - .Verify<(string input, __)>(call => - { - call.Args.input.ShouldBe("record"); - call.Returned.Value.ShouldBe("record viaed"); - }).Count.ShouldBe(1); -``` diff --git a/docs/verify/recording_exceptions.md b/docs/verify/recording_exceptions.md deleted file mode 100644 index 07aa1f90..00000000 --- a/docs/verify/recording_exceptions.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -layout: default -title: Recording Exceptions -nav_order: 5 -parent: Verifying Calls ---- - -# Recording exceptions - -```csharp -var nameCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Via<(string input, __)>(() => throw new Exception()) - .Record(); - -Exception caughtException = null; -try -{ - fooProxy.Echo("record example"); -} -catch (Exception ex) -{ - caughtException = ex; -} - -nameCalls.Verify(call => -{ - call.Args.input.ShouldBe("record example"); - call.Returned.Exception.ShouldBe(caughtException) - call.Returned.Value.ShouldBeNull(); -}).Count.ShouldBe(1); -``` diff --git a/docs/verify/redirect_record.md b/docs/verify/redirect_record.md deleted file mode 100644 index 58276ff1..00000000 --- a/docs/verify/redirect_record.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -layout: default -title: Redirect Record -nav_order: 6 -parent: Verifying Calls ---- - -# Redirect Record - -```csharp -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(new Foo()); - -// Record all Redirect proxy calls -var fooCalls = fooRedirect.Record(); - -fooProxy.Echo("record"); - -fooCalls - .To(x => x.Echo(Is.Any)) // Use the 'To' expression to filter Redirect recorded calls - .Verify<(string input, __)>(call => - { - call.Args.input.ShouldBe("record"); - call.Returned.Value.ShouldBe("record"); - }).Count.ShouldBe(1); -``` diff --git a/docs/verify/verify_snapshot.md b/docs/verify/verify_snapshot.md deleted file mode 100644 index 1826a63f..00000000 --- a/docs/verify/verify_snapshot.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -layout: default -title: Verify Snapshot -nav_order: 2 -parent: Verifying Calls ---- - -# Verify snapshot - -The `ICallStream` interface provides `Verify` helper methods to facilitate iteration and verification over the recorded calls collection. - -Recorded calls are appended to the `ICallStream` whenever a matching proxy call is made. This means the `ICallStream` will hold different record data at different points in time as calls are happening. - -Therefore the `Verify` method iterates over and returns an immutable snapshot of the collection of recorded calls at the point of time it is called. -This allows performing multiple, consistent operations on a stable set of data. E.g. in the example above iterating over the collection and then verifying the call count. - -```csharp -var echoCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Record(); - -fooProxy.Echo("one"); -Console.WriteLine(echoCalls.Count); // 1 - -// Create verify snapshot -var verifyCalls = echoCalls.Verify(); - -// Call recorded method again -fooProxy.Echo("two"); - -// Record call stream has both calls -Console.WriteLine(echoCalls.Count); // 2 - -// Snapshot is immutable -Console.WriteLine(verifyCalls.Count); // 1 -Console.WriteLine(verifyCalls[0].Args[0]); // one -``` -~~~~ \ No newline at end of file diff --git a/docs/verify/verify_visitor.md b/docs/verify/verify_visitor.md deleted file mode 100644 index d1596c12..00000000 --- a/docs/verify/verify_visitor.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -layout: default -title: Verify Visitor -nav_order: 3 -parent: Verifying Calls ---- - -# Verify Visitor - -The `ICallStream` interface provides `Verify` helper methods to facilitate iteration and verification over the recorded calls collection. - -```csharp -var fooRedirect = new Redirect(); -var fooProxy = fooRedirect.Proxy(new Foo()); - -var nameCalls = fooRedirect - .To(x => x.Echo(Is.Any)) - .Record(); - -var result = fooProxy.Echo("record"); - -var verifySnapshot = nameCalls.Verify(call => -{ - call.Args[0].ShouldBe("record"); - call.Returned.Value.ShouldBe("record"); -}); - -verifySnapshot.Count.ShouldBe(1); // The verify snapshot records calls at a point in time and is immutable -``` diff --git a/test/DivertR.UnitTests/Model/Bar.cs b/test/DivertR.UnitTests/Model/Bar.cs new file mode 100644 index 00000000..5e07da62 --- /dev/null +++ b/test/DivertR.UnitTests/Model/Bar.cs @@ -0,0 +1,11 @@ +namespace DivertR.UnitTests.Model; + +public class Bar : IBar +{ + public Bar(string name) + { + Name = name; + } + + public string Name { get; } +} \ No newline at end of file diff --git a/test/DivertR.UnitTests/Model/BarFactory.cs b/test/DivertR.UnitTests/Model/BarFactory.cs new file mode 100644 index 00000000..defad71b --- /dev/null +++ b/test/DivertR.UnitTests/Model/BarFactory.cs @@ -0,0 +1,15 @@ +using System.Threading; + +namespace DivertR.UnitTests.Model; + +public class BarFactory : IBarFactory +{ + private int _barCount; + + public IBar Create(string name) + { + var barNumber = Interlocked.Increment(ref _barCount); + + return new Bar($"Bar {barNumber}"); + } +} \ No newline at end of file diff --git a/test/DivertR.UnitTests/Model/IBar.cs b/test/DivertR.UnitTests/Model/IBar.cs new file mode 100644 index 00000000..2cfe8423 --- /dev/null +++ b/test/DivertR.UnitTests/Model/IBar.cs @@ -0,0 +1,6 @@ +namespace DivertR.UnitTests.Model; + +public interface IBar +{ + string Name { get; } +} \ No newline at end of file diff --git a/test/DivertR.UnitTests/Model/IBarFactory.cs b/test/DivertR.UnitTests/Model/IBarFactory.cs new file mode 100644 index 00000000..07d454f1 --- /dev/null +++ b/test/DivertR.UnitTests/Model/IBarFactory.cs @@ -0,0 +1,6 @@ +namespace DivertR.UnitTests.Model; + +public interface IBarFactory +{ + IBar Create(string name); +} \ No newline at end of file diff --git a/test/DivertR.UnitTests/RedirectExample.cs b/test/DivertR.UnitTests/QuickstartExample.cs similarity index 52% rename from test/DivertR.UnitTests/RedirectExample.cs rename to test/DivertR.UnitTests/QuickstartExample.cs index 651237d3..caaee239 100644 --- a/test/DivertR.UnitTests/RedirectExample.cs +++ b/test/DivertR.UnitTests/QuickstartExample.cs @@ -1,35 +1,37 @@ -using System.Linq; +using System; +using DivertR.DependencyInjection; using DivertR.UnitTests.Model; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace DivertR.UnitTests; -public class RedirectExample +public class QuickstartExample { [Fact] - public void Test() + public void RedirectDemoTest() { // Create a Foo instance named "MrFoo" IFoo foo = new Foo("MrFoo"); Assert.Equal("MrFoo", foo.Name); - // Create a DivertR IFoo Redirect + // Create an IFoo Redirect var fooRedirect = new Redirect(); - // Use the Redirect to create an IFoo proxy that wraps the Foo instance above as its root target. + // Use the Redirect to create an IFoo proxy that wraps the Foo instance above as its root target IFoo fooProxy = fooRedirect.Proxy(foo); - // By default proxies are transparent and behave exactly the same as the root target. + // By default proxies are transparent and behave exactly the same as the root target Assert.Equal("MrFoo", fooProxy.Name); - // Intercept proxy calls and change behaviour by configuring one or more 'Vias' on the Redirect. + // Intercept proxy calls and change behaviour by configuring one or more 'Vias' on the Redirect fooRedirect .To(x => x.Name) .Via(() => "redirected"); Assert.Equal("redirected", fooProxy.Name); - // Reset the Redirect and revert the proxy to its default transparent behaviour. + // Reset the Redirect and revert the proxy to its default transparent behaviour fooRedirect.Reset(); Assert.Equal("MrFoo", fooProxy.Name); @@ -44,7 +46,7 @@ public void Test() var fooTwo = new Foo("FooTwo"); IFoo fooTwoProxy = fooRedirect.Proxy(fooTwo); - // Its configured Vias are applied to all existing and new proxies. + // Its configured Vias are applied to all existing and new proxies Assert.Equal("FooTwo redirected", fooTwoProxy.Name); // Reset is applied to all proxies. @@ -63,7 +65,7 @@ public void Test() Assert.Equal("FooTwo", fooTwoProxy.Name); Assert.Null(fooMock.Name); - // Take an immutable snapshot of the currently recorded calls to verify against. + // Take an immutable snapshot of the currently recorded calls to verify against var snapshotCalls = fooCalls.To(x => x.Name).Verify(); Assert.Equal(3, snapshotCalls.Count); @@ -76,4 +78,65 @@ public void Test() Assert.Same(fooTwoProxy, snapshotCalls[1].CallInfo.Proxy); Assert.Same(fooMock, snapshotCalls[2].CallInfo.Proxy); } + + [Fact] + public void ServiceCollectionDemoTest() + { + // Instantiate a Microsoft.Extensions.DependencyInjection.IServiceCollection + IServiceCollection services = new ServiceCollection(); + + // Register some services + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + + // Instantiate a Diverter instance + var diverter = new Diverter(); + + // Register the services you want to be able to redirect + diverter + .Register() + .Register(); + + // Install DivertR into the ServiceCollection + services.Divert(diverter); + + // Build an IServiceProvider as usual + IServiceProvider provider = services.BuildServiceProvider(); + + // Resolve services from the ServiceProvider as usual + var foo = provider.GetRequiredService(); + var fooTwo = provider.GetRequiredService(); + + // At this stage DivertR is transparent and the behaviour of resolved services is unchanged + fooTwo.Name = "FooTwo"; + Assert.Equal("original", foo.Name); + Assert.Equal("FooTwo", fooTwo.Name); + + // Get a Redirect from the Diverter instance and configure a Via + diverter + .Redirect() + .To(x => x.Name) + .Via(call => $"{call.Next.Name} redirected"); + + // The behaviour of resolved service instances is now changed + Assert.Equal("original redirected", foo.Name); + Assert.Equal("FooTwo redirected", fooTwo.Name); + + // Reset the Diverter instance + diverter.ResetAll(); + + // The original service behaviour is restored + Assert.Equal("original", foo.Name); + Assert.Equal("FooTwo", fooTwo.Name); + } + + + private interface IEtc + { + } + + private class Etc : IEtc + { + } } \ No newline at end of file diff --git a/test/DivertR.WebAppTests/IFooClient.cs b/test/DivertR.WebAppTests/TestHarness/IFooClient.cs similarity index 89% rename from test/DivertR.WebAppTests/IFooClient.cs rename to test/DivertR.WebAppTests/TestHarness/IFooClient.cs index bd455ddf..276f701a 100644 --- a/test/DivertR.WebAppTests/IFooClient.cs +++ b/test/DivertR.WebAppTests/TestHarness/IFooClient.cs @@ -4,7 +4,7 @@ using DivertR.SampleWebApp.Rest; using Refit; -namespace DivertR.WebAppTests +namespace DivertR.WebAppTests.TestHarness { public interface IFooClient { diff --git a/test/DivertR.WebAppTests/WebAppFixture.cs b/test/DivertR.WebAppTests/TestHarness/WebAppFixture.cs similarity index 97% rename from test/DivertR.WebAppTests/WebAppFixture.cs rename to test/DivertR.WebAppTests/TestHarness/WebAppFixture.cs index 37cd0b23..a9019485 100644 --- a/test/DivertR.WebAppTests/WebAppFixture.cs +++ b/test/DivertR.WebAppTests/TestHarness/WebAppFixture.cs @@ -1,7 +1,6 @@ using System; using System.Net.Http; using System.Net.Http.Headers; -using Divergic.Logging.Xunit; using DivertR.DependencyInjection; using DivertR.SampleWebApp.Services; using Microsoft.AspNetCore.Hosting; @@ -11,7 +10,7 @@ using Refit; using Xunit.Abstractions; -namespace DivertR.WebAppTests +namespace DivertR.WebAppTests.TestHarness { public class WebAppFixture { diff --git a/test/DivertR.WebAppTests/MockSampleTests.cs b/test/DivertR.WebAppTests/Tests/MockSampleTests.cs similarity index 98% rename from test/DivertR.WebAppTests/MockSampleTests.cs rename to test/DivertR.WebAppTests/Tests/MockSampleTests.cs index e8983c83..115af232 100644 --- a/test/DivertR.WebAppTests/MockSampleTests.cs +++ b/test/DivertR.WebAppTests/Tests/MockSampleTests.cs @@ -5,11 +5,12 @@ using DivertR.SampleWebApp.Model; using DivertR.SampleWebApp.Rest; using DivertR.SampleWebApp.Services; +using DivertR.WebAppTests.TestHarness; using Shouldly; using Xunit; using Xunit.Abstractions; -namespace DivertR.WebAppTests +namespace DivertR.WebAppTests.Tests { public class MockSampleTests : IClassFixture { diff --git a/test/DivertR.WebAppTests/SpySampleTests.cs b/test/DivertR.WebAppTests/Tests/SpySampleTests.cs similarity index 99% rename from test/DivertR.WebAppTests/SpySampleTests.cs rename to test/DivertR.WebAppTests/Tests/SpySampleTests.cs index a18284eb..e88e7cb3 100644 --- a/test/DivertR.WebAppTests/SpySampleTests.cs +++ b/test/DivertR.WebAppTests/Tests/SpySampleTests.cs @@ -4,12 +4,13 @@ using DivertR.SampleWebApp.Model; using DivertR.SampleWebApp.Rest; using DivertR.SampleWebApp.Services; +using DivertR.WebAppTests.TestHarness; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Xunit; using Xunit.Abstractions; -namespace DivertR.WebAppTests +namespace DivertR.WebAppTests.Tests { public class SpySampleTests : IClassFixture {