Skip to content

Commit

Permalink
Add docs for Spy functionality (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
devodo authored Aug 11, 2023
1 parent ee7d628 commit 2c16d45
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 40 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<IFoo>();

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/)
Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/advanced.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Advanced
nav_order: 5
nav_order: 6
parent: Documentation
---

Expand Down
2 changes: 1 addition & 1 deletion docs/documentation/proxy_factory.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Proxy Factory
nav_order: 4
nav_order: 5
parent: Documentation
---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
layout: default
title: Redirects
title: Redirect
nav_order: 1
parent: Documentation
---

# Redirects
# Redirect

{: .no_toc }

Expand Down
127 changes: 127 additions & 0 deletions docs/documentation/spy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
layout: default
title: Spy
nav_order: 4
parent: Documentation
---

# Spy

{: .no_toc }

<details open markdown="block">
<summary>
Table of contents
</summary>
{: .text-delta }
- TOC
{:toc}
</details>

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<TTarget>` 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<IFoo> fooSpy = new Spy<IFoo>();
// 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<string>.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<IFoo>(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<IFoo>();
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<IFoo>();

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<IFoo>(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);
```


87 changes: 63 additions & 24 deletions docs/quickstart/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IFoo>();

// 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<string>.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);
}
}
```
Expand Down Expand Up @@ -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<IFoo>();
// 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<string>.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.
Loading

0 comments on commit 2c16d45

Please sign in to comment.