From 2c16d458d8d45905ecc4b370c2118337e97b387c Mon Sep 17 00:00:00 2001 From: David Naylor Date: Fri, 11 Aug 2023 19:15:55 +0100 Subject: [PATCH] Add docs for Spy functionality (#86) --- README.md | 19 ++- docs/documentation/advanced.md | 2 +- docs/documentation/proxy_factory.md | 2 +- .../{redirects.md => redirect.md} | 4 +- docs/documentation/spy.md | 127 ++++++++++++++ docs/quickstart/index.md | 87 +++++++--- test/DivertR.UnitTests/QuickstartExamples.cs | 155 ++++++++++++++++-- 7 files changed, 356 insertions(+), 40 deletions(-) rename docs/documentation/{redirects.md => redirect.md} (99%) create mode 100644 docs/documentation/spy.md diff --git a/README.md b/README.md index f45a2f0d..5642ce8f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ WebApplicationFactory does let you customise dependency injection services but t To have different customisations between tests requires reinitialising a new test server instance each time. This can be very slow when running many tests or larger applications with heavier startup. +# WebApplicationFactory Example + DivertR turns dependency injection services into configurable proxies that can be reconfigured between tests running against the same test server instance like this: ```csharp @@ -78,10 +80,23 @@ public async Task GivenBookServiceError_WhenGetBookById_ThenReturns500InternalSe > **Note** > The source code for the example above is available [here](https://github.com/devodo/DivertR/tree/main/examples/DivertR.Examples.WebAppTests). +# Mocking Example + DivertR is a general purpose framework that can be used in many different scenarios including for standard unit test mocking purposes. -Please follow the [Resources](#resources) section below for more examples, quickstart, documentation, etc. -# Resources +```csharp +IFoo fooMock = Spy.On(); + +Spy.Of(fooMock) + .To(x => x.Name) + .Via(() => "redirected"); + +Assert.Equal("redirected", fooMock.Name); +``` + +# Documentation and Resources + +Please follow the links below for more examples, quickstart, documentation, etc. * [Quickstart guide](https://devodo.github.io/DivertR/quickstart/) * [Documentation](https://devodo.github.io/DivertR/) diff --git a/docs/documentation/advanced.md b/docs/documentation/advanced.md index 88681923..b01cd347 100644 --- a/docs/documentation/advanced.md +++ b/docs/documentation/advanced.md @@ -1,7 +1,7 @@ --- layout: default title: Advanced -nav_order: 5 +nav_order: 6 parent: Documentation --- diff --git a/docs/documentation/proxy_factory.md b/docs/documentation/proxy_factory.md index 8037943c..ef241741 100644 --- a/docs/documentation/proxy_factory.md +++ b/docs/documentation/proxy_factory.md @@ -1,7 +1,7 @@ --- layout: default title: Proxy Factory -nav_order: 4 +nav_order: 5 parent: Documentation --- diff --git a/docs/documentation/redirects.md b/docs/documentation/redirect.md similarity index 99% rename from docs/documentation/redirects.md rename to docs/documentation/redirect.md index 5adb01b2..784696b5 100644 --- a/docs/documentation/redirects.md +++ b/docs/documentation/redirect.md @@ -1,11 +1,11 @@ --- layout: default -title: Redirects +title: Redirect nav_order: 1 parent: Documentation --- -# Redirects +# Redirect {: .no_toc } diff --git a/docs/documentation/spy.md b/docs/documentation/spy.md new file mode 100644 index 00000000..bee4ffa4 --- /dev/null +++ b/docs/documentation/spy.md @@ -0,0 +1,127 @@ +--- +layout: default +title: Spy +nav_order: 4 +parent: Documentation +--- + +# Spy + +{: .no_toc } + +
+ + Table of contents + + {: .text-delta } +- TOC +{:toc} +
+ +The `Spy` class extends the core [Redirect](/redirect) to provide a convenient, familiar interface for standard mocking usage. + +The main difference between `Spy` and `Redirect` is the spy interface has two additional read-only properties, `Mock` and `Calls`. +For a `Spy` instance, the `Mock` property is a proxy object of type `TTarget` and any calls to this object are recorded in the `Calls` properties. + +Instantiate and use a Spy instance like this: + +```csharp +// Instantiate an IFoo spy +ISpy fooSpy = new Spy(); +// The Mock property is the spy's proxy object +IFoo fooMock = fooSpy.Mock; +// Out the box spy proxies return dummy values (C# defaults) +Assert.Null(fooMock.Name); +// For async methods a Task is returned wrapping a dummy result +Assert.Null(await fooMock.EchoAsync("test")); + +// Proxy behaviour can be configured using the usual redirect fluent syntax +fooSpy.To(x => x.Name).Via(() => "Hello spy"); +// Now matching calls are redirected to the Via delegate +Assert.Equal("Hello spy", fooMock.Name); + +// Proxy calls are recorded to the Calls property +Assert.Equal(3, fooSpy.Calls.Count); +// Recorded calls can be filtered and verified +Assert.Equal(1, fooSpy.Calls.To(x => x.EchoAsync(Is.Any)).Count); +``` + +## Proxy Root + +When a `Spy` is created it can be given a *root* instance of its target type. +The default behaviour of the spy proxy is to relay all calls to its root: + +```csharp +IFoo fooRoot = new Foo("MrFoo"); +Assert.Equal("MrFoo", fooRoot.Name); + +// Specify the proxy root at creation +var fooSpy = new Spy(fooRoot); +// By default proxy calls are relayed to the root +Assert.Equal("MrFoo", fooSpy.Mock.Name); +``` + +## Retarget + +The proxy root can also be set or changed after creation by retargeting: + +```csharp +IFoo fooRoot = new Foo("MrFoo"); +Assert.Equal("MrFoo", fooRoot.Name); + +// Create spy without proxy root +var fooSpy = new Spy(); +Assert.Null(fooSpy.Mock.Name); + +// Retarget to a new proxy root +fooSpy.Retarget(fooRoot); + +// Proxy calls are now relayed to the set target +Assert.Equal("MrFoo", fooSpy.Mock.Name); +``` + +## Reset + +Spies can be reset at any time. This clears recorded calls and removes all configured Vias. + +```csharp +var fooSpy = new Spy(); + +fooSpy.To(x => x.Name).Via(() => "redirected"); +Assert.Equal("redirected", fooSpy.Mock.Name); +Assert.Equal(1, fooSpy.Calls.Count); + +// Reset spy +fooSpy.Reset(); + +// Call counts are reset +Assert.Equal(0, fooSpy.Calls.Count); +// And configured Vias removed +Assert.Null(fooSpy.Mock.Name); +``` + + +## Static Spy Syntax + +The `Spy.On` static method is provided as an alternative shorthand way to create spy proxies directly. +The `Spy.Of` static method can then be used to access the underlying `Spy` from the proxy. + +```csharp +var fooRoot = new Foo("MrFoo"); + +// Create a spy proxy with optional root using the Spy.On static method +IFoo fooProxy = Spy.On(fooRoot); + +// The proxy instance relays calls to the root +Assert.Equal("MrFoo", fooProxy.Name); + +// Use the Spy.Of static method to access and update spy configuration +Spy.Of(fooProxy) + .To(x => x.Name) + .Via(call => call.CallNext() + " spied"); + +Assert.Equal("MrFoo spied", fooProxy.Name); +Assert.Equal(2, Spy.Of(fooProxy).Calls.Count); +``` + + diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md index f633d062..eaa68e00 100644 --- a/docs/quickstart/index.md +++ b/docs/quickstart/index.md @@ -37,70 +37,70 @@ public class QuickstartExample // Create a Foo instance named "MrFoo" IFoo foo = new Foo("MrFoo"); Assert.Equal("MrFoo", foo.Name); - + // 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 IFoo fooProxy = fooRedirect.Proxy(foo); - + // By default proxies transparently forward calls to their root targets Assert.Equal("MrFoo", fooProxy.Name); - + // Intercept proxy calls and change behaviour by adding one or more 'Via' delegates to the Redirect fooRedirect .To(x => x.Name) .Via(() => "redirected"); - + // The Redirect diverts proxy calls to its Via delegates Assert.Equal("redirected", fooProxy.Name); - + // Reset the Redirect and revert the proxy to its default transparent behaviour fooRedirect.Reset(); Assert.Equal("MrFoo", fooProxy.Name); - + // Via delegates can access call context and e.g. relay the call to the root target fooRedirect .To(x => x.Name) .Via(call => call.CallRoot() + " redirected"); - + Assert.Equal("MrFoo redirected", fooProxy.Name); - + // A Redirect can create any number of proxies var fooTwo = new Foo("FooTwo"); IFoo fooTwoProxy = fooRedirect.Proxy(fooTwo); - + // Vias added to the Redirect are applied to all its proxies Assert.Equal("FooTwo redirected", fooTwoProxy.Name); - + // Reset is applied to all proxies. fooRedirect.Reset(); Assert.Equal("MrFoo", fooProxy.Name); Assert.Equal("FooTwo", fooTwoProxy.Name); - + // A proxy with no root target returns default values var fooMock = fooRedirect.Proxy(); Assert.Null(fooMock.Name); - - // Record and verify proxy calls - var fooCalls = fooRedirect.Record(); + // Proxy calls and be recorded for verifying + var fooCalls = fooRedirect.Record(); + Assert.Equal("MrFoo", fooProxy.Name); Assert.Equal("FooTwo", fooTwoProxy.Name); - Assert.Null(fooMock.Name); + Assert.Null(fooMock.Echo("test")); + + // The recording is a collection containing details of the calls + Assert.Equal(3, fooCalls.Count); + // This can be filtered with expressions for verifying + Assert.Equal(1, fooCalls.To(x => x.Echo(Is.Any)).Count); + // 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); - + Assert.Equal(2, snapshotCalls.Count); + Assert.Equal("MrFoo", snapshotCalls[0].Return); Assert.Equal("FooTwo", snapshotCalls[1].Return); - Assert.Null(snapshotCalls[2].Return); - - // Calls are recorded across all of the Redirect's proxies - Assert.Same(fooProxy, snapshotCalls[0].CallInfo.Proxy); - Assert.Same(fooTwoProxy, snapshotCalls[1].CallInfo.Proxy); - Assert.Same(fooMock, snapshotCalls[2].CallInfo.Proxy); } } ``` @@ -210,4 +210,43 @@ public async Task GivenFooRepoException_WhenGetFoo_ThenReturns500InternalServerE 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). +## Standard Mocking + +The `Spy` class is provided and extends `Redirect` to add familiar, standard mocking capability: + +```csharp +[Fact] +public async Task SpyTestSample() +{ + // Create an IFoo mock + var fooMock = Spy.On(); + // By default mocks return dummy values (C# defaults) + Assert.Null(fooMock.Name); + // For async methods a Task is returned wrapping a dummy result + Assert.Null(await fooMock.EchoAsync("test")); + + // Mock behaviour can be configured by adding Vias using the usual redirect fluent syntax + Spy.Of(fooMock) + .To(x => x.Name) + .Via(() => "redirected"); + + // Mock calls are redirected to the Via delegates + Assert.Equal("redirected", fooMock.Name); + + // The spy records all mock calls + Assert.Equal(3, Spy.Of(fooMock).Calls.Count); + // These can be filtered with expressions for verifying + Assert.Equal(1, Spy.Of(fooMock).Calls.To(x => x.EchoAsync(Is.Any)).Count); + + // Spies can be reset + Spy.Of(fooMock).Reset(); + // This resets recorded calls + Assert.Equal(0, Spy.Of(fooMock).Calls.Count); + // And removes all Via configurations + Assert.Null(fooMock.Name); +} +``` + +## Learn More + Continue with [Documentation](../documentation/) for more details. \ No newline at end of file diff --git a/test/DivertR.UnitTests/QuickstartExamples.cs b/test/DivertR.UnitTests/QuickstartExamples.cs index 54495177..394d83d8 100644 --- a/test/DivertR.UnitTests/QuickstartExamples.cs +++ b/test/DivertR.UnitTests/QuickstartExamples.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Threading.Tasks; using DivertR.DependencyInjection; using DivertR.UnitTests.Model; using Microsoft.Extensions.DependencyInjection; @@ -59,25 +61,25 @@ public void RedirectTestSample() var fooMock = fooRedirect.Proxy(); Assert.Null(fooMock.Name); - // Record and verify proxy calls + // Proxy calls and be recorded for verifying var fooCalls = fooRedirect.Record(); - + Assert.Equal("MrFoo", fooProxy.Name); Assert.Equal("FooTwo", fooTwoProxy.Name); - Assert.Null(fooMock.Name); + Assert.Null(fooMock.Echo("test")); + + // The recording is a collection containing details of the calls + Assert.Equal(3, fooCalls.Count); + + // This can be filtered with expressions for verifying + Assert.Equal(1, fooCalls.To(x => x.Echo(Is.Any)).Count); // 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); + Assert.Equal(2, snapshotCalls.Count); Assert.Equal("MrFoo", snapshotCalls[0].Return); Assert.Equal("FooTwo", snapshotCalls[1].Return); - Assert.Null(snapshotCalls[2].Return); - - // Calls are recorded across all of the Redirect's proxies - Assert.Same(fooProxy, snapshotCalls[0].CallInfo.Proxy); - Assert.Same(fooTwoProxy, snapshotCalls[1].CallInfo.Proxy); - Assert.Same(fooMock, snapshotCalls[2].CallInfo.Proxy); } [Fact] @@ -171,7 +173,140 @@ public void RedirectSetExamples2() // Or across a subset by name redirectSet.Reset("GroupX"); } + + [Fact] + public async Task SpyTestSample() + { + // Create an IFoo mock + var fooMock = Spy.On(); + // By default mocks return dummy values (C# defaults) + Assert.Null(fooMock.Name); + // For async methods a Task is returned wrapping a dummy result + Assert.Null(await fooMock.EchoAsync("test")); + + // Mock behaviour can be configured by adding Vias using the usual redirect fluent syntax + Spy.Of(fooMock) + .To(x => x.Name) + .Via(() => "redirected"); + + // Mock calls are redirected to the Via delegates + Assert.Equal("redirected", fooMock.Name); + + // The spy records all mock calls + Assert.Equal(3, Spy.Of(fooMock).Calls.Count); + // These can be filtered with expressions for verifying + Assert.Equal(1, Spy.Of(fooMock).Calls.To(x => x.EchoAsync(Is.Any)).Count); + + // Spies can be reset + Spy.Of(fooMock).Reset(); + // This resets recorded calls + Assert.Equal(0, Spy.Of(fooMock).Calls.Count); + // And removes all Via configurations + Assert.Null(fooMock.Name); + } + + [Fact] + public void SpyReadmeExample() + { + IFoo fooMock = Spy.On(); + + Spy.Of(fooMock) + .To(x => x.Name) + .Via(() => "redirected"); + + Assert.Equal("redirected", fooMock.Name); + } + + [Fact] + public async Task SpyInstantiationAndUsage() + { + // Instantiate an IFoo spy + ISpy fooSpy = new Spy(); + // The Mock property is the spy's proxy object + IFoo fooMock = fooSpy.Mock; + // Out the box spy proxies return dummy values (C# defaults) + Assert.Null(fooMock.Name); + // For async methods a Task is returned wrapping a dummy result + Assert.Null(await fooMock.EchoAsync("test")); + + // Proxy behaviour can be configured using the usual redirect fluent syntax + fooSpy.To(x => x.Name).Via(() => "Hello spy"); + // Now matching calls are redirected to the Via delegate + Assert.Equal("Hello spy", fooMock.Name); + + // Proxy calls are recorded to the Calls property + Assert.Equal(3, fooSpy.Calls.Count); + // Recorded calls can be filtered and verified + Assert.Equal(1, fooSpy.Calls.To(x => x.EchoAsync(Is.Any)).Count); + } + + [Fact] + public void SpyProxyRoot() + { + IFoo fooRoot = new Foo("MrFoo"); + Assert.Equal("MrFoo", fooRoot.Name); + + // Specify the proxy root at creation + var fooSpy = new Spy(fooRoot); + // By default proxy calls are relayed to the root + Assert.Equal("MrFoo", fooSpy.Mock.Name); + } + + [Fact] + public void SpyRetarget() + { + IFoo fooRoot = new Foo("MrFoo"); + Assert.Equal("MrFoo", fooRoot.Name); + // Create spy without proxy root + var fooSpy = new Spy(); + Assert.Null(fooSpy.Mock.Name); + + // Retarget to a new proxy root + fooSpy.Retarget(fooRoot); + + // Proxy calls are now relayed to the set target + Assert.Equal("MrFoo", fooSpy.Mock.Name); + } + + [Fact] + public void SpyReset() + { + var fooSpy = new Spy(); + + fooSpy.To(x => x.Name).Via(() => "redirected"); + Assert.Equal("redirected", fooSpy.Mock.Name); + Assert.Equal(1, fooSpy.Calls.Count); + + // Reset spy + fooSpy.Reset(); + + // Call counts are reset + Assert.Equal(0, fooSpy.Calls.Count); + // And configured Vias removed + Assert.Null(fooSpy.Mock.Name); + } + + [Fact] + public void SpyStaticShorthand() + { + var fooRoot = new Foo("MrFoo"); + + // Create a spy proxy with optional root using the Spy.On static method + IFoo fooProxy = Spy.On(fooRoot); + + // The proxy instance relays calls to the root + Assert.Equal("MrFoo", fooProxy.Name); + + // Use the Spy.Of static method to access and update spy configuration + Spy.Of(fooProxy) + .To(x => x.Name) + .Via(call => call.CallNext() + " spied"); + + Assert.Equal("MrFoo spied", fooProxy.Name); + Assert.Equal(2, Spy.Of(fooProxy).Calls.Count); + } + private interface IEtc { }