Skip to content

Commit c4c0b1e

Browse files
authored
New features (#8)
* Implement issue #7. Rename `GenericTrigger` to `Type`. * Update version number.
1 parent 068bd2d commit c4c0b1e

File tree

18 files changed

+306
-79
lines changed

18 files changed

+306
-79
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Represents the **NuGet** versions.
44

5+
## v1.0.5
6+
- *[Issue 7](https://github.com/Avanade/UnitTestEx/issues/7)*: Added delay (sleep) option so response is not always immediate.
7+
- *Enhancement:* **Breaking change.** `Functions.GenericTriggerTester` replaced with `Hosting.TypeTester` as agnostic to any function trigger. `Functions.TriggerTesterBase` replaced with `Hosting.HostTesterBase` for same agnostic reasoning. `FunctionTestBase.GenericTrigger` method renamed to `FunctionTestBase.Type` so as to not imply a _trigger_ requirement (i.e. can be any _Type+Method_ that needs testing).
8+
59
## v1.0.4
610
- *[Issue 3](https://github.com/Avanade/UnitTestEx/issues/3)*: Added support for MOQ `Times` struct to verify the number of times a request is made.
711
- *[Issue 4](https://github.com/Avanade/UnitTestEx/issues/4)*: Added support for MOQ sequences; i.e. multiple different responses.

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The scenarios that _UnitTestEx_ looks to address is the end-to-end unit-style te
1212

1313
- [API Controller](#API-Controller)
1414
- [HTTP-triggered Azure Function](#HTTP-triggered-Azure-Function)
15-
- [Generic Azure Function](#Generic-Azure-Function)
15+
- [Generic Azure Function Type](#Generic-Azure-Function-Type)
1616
- [HTTP Client mocking](#HTTP-Client-mocking)
1717

1818
<br/>
@@ -47,7 +47,7 @@ test.ConfigureServices(sc => mcf.Replace(sc))
4747

4848
## HTTP-triggered Azure Function
4949

50-
Unfortunately, at time of writing, there is no `WebApplicationFactory` equivalent for Azure functions. _UnitTestEx_ looks to simulate by self-hosting the function, managing Dependency Injection (DI) configuration, and invocation of the underlying method.
50+
Unfortunately, at time of writing, there is no `WebApplicationFactory` equivalent for Azure functions. _UnitTestEx_ looks to simulate by self-hosting the function, managing Dependency Injection (DI) configuration, and invocation of the specified method. _UnitTestEx_ when invoking verifies usage of `HttpTriggerAttribute`(https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=csharp) and ensures a `Task<IActionResult>` result.
5151

5252
The following is an [example](./tests/UnitTestEx.NUnit.Test/ProductControllerTest.cs).
5353

@@ -61,16 +61,16 @@ test.ConfigureServices(sc => mcf.Replace(sc))
6161

6262
<br/>
6363

64-
## Generic-triggered Azure Function
64+
## Generic Azure Function Type
6565

66-
To support other non [HTTP-triggered Azure Functions](#HTTP-triggered-Azure-Function), _UnitTestEx_ supports the execution of any generic-triggered Azure Function; i.e. any trigger.
66+
To support testing of any generic `Type` within an Azure Fuction, _UnitTestEx_ looks to simulate by self-hosting the function, managing Dependency Injection (DI) configuration, and invocation of the specified method.
6767

6868
The following is an [example](./tests/UnitTestEx.NUnit.Test/ServiceBusFunctionTest.cs).
6969

7070
``` csharp
7171
using var test = FunctionTester.Create<Startup>();
7272
test.ConfigureServices(sc => mcf.Replace(sc))
73-
.GenericTrigger<ServiceBusFunction>()
73+
.Type<ServiceBusFunction>()
7474
.Run(f => f.Run2(test.CreateServiceBusMessage(new Person { FirstName = "Bob", LastName = "Smith" }), test.Logger))
7575
.AssertSuccess();
7676
```
@@ -126,6 +126,23 @@ mc.Request(HttpMethod.Get, "products/xyz").Respond.WithSequence(s =>
126126

127127
<br>
128128

129+
### Delay
130+
131+
A delay (sleep) can be simulated so a response is not always immediated. This can be specified as a fixed value, or randomly generated using a from and to.
132+
133+
``` csharp
134+
var mcf = MockHttpClientFactory.Create();
135+
var mc = mcf.CreateClient("XXX", new Uri("https://d365test"));
136+
mc.Request(HttpMethod.Get, "products/xyz").Respond.Delay(500).With(HttpStatusCode.NotFound);
137+
mc.Request(HttpMethod.Get, "products/kjl").Respond.WithSequence(s =>
138+
{
139+
s.Respond().Delay(250).With(HttpStatusCode.NotModified);
140+
s.Respond().Delay(100, 200).With(HttpStatusCode.NotFound);
141+
});
142+
```
143+
144+
<br>
145+
129146
## Examples
130147

131148
As _UnitTestEx_ is intended for testing, look at the tests for further details on how to leverage:

src/UnitTestEx.MSTest/UnitTestEx.MSTest.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.1</TargetFramework>
55
<RootNamespace>UnitTestEx.MSTest</RootNamespace>
6-
<Version>1.0.4</Version>
6+
<Version>1.0.5</Version>
77
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
88
<Authors>UnitTestEx Developers</Authors>
99
<Company>Avanade</Company>

src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.1</TargetFramework>
55
<RootNamespace>UnitTestEx.NUnit</RootNamespace>
6-
<Version>1.0.4</Version>
6+
<Version>1.0.5</Version>
77
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
88
<Authors>UnitTestEx Developers</Authors>
99
<Company>Avanade</Company>

src/UnitTestEx.XUnit/UnitTestEx.Xunit.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.1</TargetFramework>
55
<RootNamespace>UnitTestEx.Xunit</RootNamespace>
6-
<Version>1.0.4</Version>
6+
<Version>1.0.5</Version>
77
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
88
<Authors>UnitTestEx Developers</Authors>
99
<Company>Avanade</Company>

src/UnitTestEx/Functions/FunctionTesterBase.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Reflection;
2020
using System.Text;
2121
using UnitTestEx.Abstractions;
22+
using UnitTestEx.Hosting;
2223

2324
namespace UnitTestEx.Functions
2425
{
@@ -198,18 +199,18 @@ public TSelf ConfigureServices(Action<IServiceCollection> configureServices)
198199
public TSelf MockScopedService<T>(Mock<T> mock) where T : class => ConfigureServices(sc => sc.ReplaceScoped(mock.Object));
199200

200201
/// <summary>
201-
/// Specify the Function that has an <see cref="HttpTrigger"/> to test.
202+
/// Specifies the <i>Function</i> <see cref="Type"/> that utilizes the <see cref="Microsoft.Azure.WebJobs.HttpTriggerAttribute"/> that is to be tested.
202203
/// </summary>
203-
/// <typeparam name="TFunction">The Function <see cref="Type"/>.</typeparam>
204+
/// <typeparam name="TFunction">The Function <see cref="Type"/> that utilizes the <see cref="Microsoft.Azure.WebJobs.HttpTriggerAttribute"/> to be tested.</typeparam>
204205
/// <returns>The <see cref="HttpTriggerTester{TFunction}"/>.</returns>
205206
public HttpTriggerTester<TFunction> HttpTrigger<TFunction>() where TFunction : class => new(GetHost().Services.CreateScope(), Implementor);
206207

207208
/// <summary>
208-
/// Specify the Function that has any generic trigger to test.
209+
/// Specifies the <see cref="Type"/> of <typeparamref name="T"/> that is to be tested.
209210
/// </summary>
210-
/// <typeparam name="TFunction">The Function <see cref="Type"/>.</typeparam>
211-
/// <returns>The <see cref="GenericTriggerTester{TFunction}"/>.</returns>
212-
public GenericTriggerTester<TFunction> GenericTrigger<TFunction>() where TFunction : class => new(GetHost().Services.CreateScope(), Implementor);
211+
/// <typeparam name="T">The <see cref="Type"/> to be tested.</typeparam>
212+
/// <returns>The <see cref="TypeTester{TFunction}"/>.</returns>
213+
public TypeTester<T> Type<T>() where T : class => new(GetHost().Services.CreateScope(), Implementor);
213214

214215
/// <summary>
215216
/// Creates a new <see cref="HttpRequest"/> with no body.

src/UnitTestEx/Functions/HttpTriggerTester.cs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
using System.Threading.Tasks;
2020
using UnitTestEx.Abstractions;
2121
using UnitTestEx.Assertors;
22+
using UnitTestEx.Hosting;
2223

2324
namespace UnitTestEx.Functions
2425
{
2526
/// <summary>
2627
/// Provides the Azure Function <see cref="HttpTriggerTester{TFunction}"/> unit-testing capabilities.
2728
/// </summary>
2829
/// <typeparam name="TFunction">The Azure Function <see cref="Type"/>.</typeparam>
29-
public class HttpTriggerTester<TFunction> : TriggerTesterBase<TFunction> where TFunction : class
30+
public class HttpTriggerTester<TFunction> : HostTesterBase<TFunction> where TFunction : class
3031
{
3132
/// <summary>
3233
/// Initializes a new <see cref="HttpTriggerTester{TFunction}"/> class.
@@ -44,7 +45,7 @@ public ActionResultAssertor Run(Expression<Func<TFunction, Task<IActionResult>>>
4445
{
4546
object? requestVal = null;
4647
HttpRequest? httpRequest = null;
47-
(IActionResult result, Exception? ex, long ms) = RunFunction(expression, typeof(HttpTriggerAttribute), (p, a, v) =>
48+
(IActionResult result, Exception? ex, long ms) = Run(expression, typeof(HttpTriggerAttribute), (p, a, v) =>
4849
{
4950
if (a == null)
5051
throw new InvalidOperationException($"The function method must have a parameter using the {nameof(HttpTriggerAttribute)}.");
@@ -96,23 +97,28 @@ private void LogOutput(HttpRequest? req, object? reqVal, IActionResult res, Exce
9697
JToken? json = null;
9798
if (req.Body != null)
9899
{
99-
req.Body.Position = 0;
100-
using var sr = new StreamReader(req.Body);
101-
var body = sr.ReadToEnd();
102-
103-
// Parse out the content.
104-
if (body.Length > 0)
100+
if (req.Body.CanRead)
105101
{
106-
try
102+
req.Body.Position = 0;
103+
using var sr = new StreamReader(req.Body);
104+
var body = sr.ReadToEnd();
105+
106+
// Parse out the content.
107+
if (body.Length > 0)
107108
{
108-
json = JToken.Parse(body);
109+
try
110+
{
111+
json = JToken.Parse(body);
112+
}
113+
catch (Exception) { /* This is being swallowed by design. */ }
109114
}
110-
catch (Exception) { /* This is being swallowed by design. */ }
111-
}
112115

113-
Implementor.WriteLine($"Content: [{req.ContentType ?? "None"}]");
114-
if (json != null || !string.IsNullOrEmpty(body))
115-
Implementor.WriteLine(json == null ? body : json.ToString());
116+
Implementor.WriteLine($"Content: [{req.ContentType ?? "None"}]");
117+
if (json != null || !string.IsNullOrEmpty(body))
118+
Implementor.WriteLine(json == null ? body : json.ToString());
119+
}
120+
else
121+
Implementor.WriteLine($"Content: [{req.ContentType ?? "None"}] => Response.Body is not in a read state.");
116122
}
117123
}
118124

src/UnitTestEx/Functions/TriggerTesterBase.cs renamed to src/UnitTestEx/Hosting/HostTesterBase.cs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx
22

33
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
45
using System;
56
using System.Diagnostics;
67
using System.Linq;
78
using System.Linq.Expressions;
89
using System.Threading.Tasks;
910
using UnitTestEx.Abstractions;
1011

11-
namespace UnitTestEx.Functions
12+
namespace UnitTestEx.Hosting
1213
{
1314
/// <summary>
14-
/// Provides the base Azure Function unit-testing capabilities.
15+
/// Provides the base <see cref="IHost"/> unit-testing capabilities.
1516
/// </summary>
16-
/// <typeparam name="TFunction">The Azure Function <see cref="Type"/>.</typeparam>
17-
public class TriggerTesterBase<TFunction> where TFunction : class
17+
/// <typeparam name="THost">The host <see cref="Type"/>.</typeparam>
18+
public class HostTesterBase<THost> where THost : class
1819
{
1920
/// <summary>
20-
/// Initializes a new <see cref="GenericTriggerTester{TFunction}"/> class.
21+
/// Initializes a new <see cref="HostTesterBase{TFunction}"/> class.
2122
/// </summary>
2223
/// <param name="serviceScope">The <see cref="IServiceScope"/>.</param>
2324
/// <param name="implementor">The <see cref="TestFrameworkImplementor"/>.</param>
24-
protected TriggerTesterBase(IServiceScope serviceScope, TestFrameworkImplementor implementor)
25+
protected HostTesterBase(IServiceScope serviceScope, TestFrameworkImplementor implementor)
2526
{
2627
ServiceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
2728
Implementor = implementor ?? throw new ArgumentNullException(nameof(implementor));
@@ -38,18 +39,18 @@ protected TriggerTesterBase(IServiceScope serviceScope, TestFrameworkImplementor
3839
protected TestFrameworkImplementor Implementor { get; }
3940

4041
/// <summary>
41-
/// Create (instantiate) the <typeparamref name="TFunction"/> using the <see cref="ServiceScope"/> to provide the constructor based dependency injection (DI) values.
42+
/// Create (instantiate) the <typeparamref name="THost"/> using the <see cref="ServiceScope"/> to provide the constructor based dependency injection (DI) values.
4243
/// </summary>
43-
private TFunction CreateFunction() => ServiceScope.ServiceProvider.CreateInstance<TFunction>();
44+
private THost CreateFunction() => ServiceScope.ServiceProvider.CreateInstance<THost>();
4445

4546
/// <summary>
46-
/// Orchestrates the execution of the function method as described by the <paramref name="expression"/> returning no result.
47+
/// Orchestrates the execution of a method as described by the <paramref name="expression"/> returning no result.
4748
/// </summary>
48-
/// <param name="expression">The funtion execution expression.</param>
49+
/// <param name="expression">The method execution expression.</param>
4950
/// <param name="paramAttributeType">The optional parameter <see cref="Attribute"/> <see cref="Type"/> to find.</param>
5051
/// <param name="onBeforeRun">Action to verify the method parameters prior to method invocation.</param>
5152
/// <returns>The resulting exception if any and elapsed milliseconds.</returns>
52-
protected (Exception? Exception, long ElapsedMilliseconds) RunFunction(Expression<Func<TFunction, Task>> expression, Type? paramAttributeType, Action<object?[], Attribute?, object?>? onBeforeRun)
53+
protected (Exception? Exception, long ElapsedMilliseconds) Run(Expression<Func<THost, Task>> expression, Type? paramAttributeType, Action<object?[], Attribute?, object?>? onBeforeRun)
5354
{
5455
if (expression == null)
5556
throw new ArgumentNullException(nameof(expression));
@@ -85,7 +86,7 @@ protected TriggerTesterBase(IServiceScope serviceScope, TestFrameworkImplementor
8586
var mr = mce.Method.Invoke(f, @params)!;
8687

8788
if (!(mr is Task tr))
88-
throw new InvalidOperationException($"The function method must return a result of Type {nameof(Task)}.");
89+
throw new InvalidOperationException($"The method must return a result of Type {nameof(Task)}.");
8990

9091
try
9192
{
@@ -101,13 +102,13 @@ protected TriggerTesterBase(IServiceScope serviceScope, TestFrameworkImplementor
101102
}
102103

103104
/// <summary>
104-
/// Orchestrates the execution of the function method as described by the <paramref name="expression"/> returning a result of <see cref="Type"/> <typeparamref name="TResult"/>.
105+
/// Orchestrates the execution of a method as described by the <paramref name="expression"/> returning a result of <see cref="Type"/> <typeparamref name="TResult"/>.
105106
/// </summary>
106-
/// <param name="expression">The funtion execution expression.</param>
107+
/// <param name="expression">The method execution expression.</param>
107108
/// <param name="paramAttributeType">The optional parameter <see cref="Attribute"/> <see cref="Type"/> to find.</param>
108109
/// <param name="onBeforeRun">Action to verify the method parameters prior to method invocation.</param>
109110
/// <returns>The resulting value, resulting exception if any, and elapsed milliseconds.</returns>
110-
protected (TResult Result, Exception? Exception, long ElapsedMilliseconds) RunFunction<TResult>(Expression<Func<TFunction, Task<TResult>>> expression, Type? paramAttributeType, Action<object?[], Attribute?, object?>? onBeforeRun)
111+
protected (TResult Result, Exception? Exception, long ElapsedMilliseconds) Run<TResult>(Expression<Func<THost, Task<TResult>>> expression, Type? paramAttributeType, Action<object?[], Attribute?, object?>? onBeforeRun)
111112
{
112113
if (expression == null)
113114
throw new ArgumentNullException(nameof(expression));
@@ -143,7 +144,7 @@ protected TriggerTesterBase(IServiceScope serviceScope, TestFrameworkImplementor
143144
var mr = mce.Method.Invoke(f, @params)!;
144145

145146
if (!(mr is Task<TResult> tr))
146-
throw new InvalidOperationException($"The function method must return a result of Type {nameof(Task<TResult>)}.");
147+
throw new InvalidOperationException($"The method must return a result of Type {nameof(Task<TResult>)}.");
147148

148149
try
149150
{

src/UnitTestEx/Functions/GenericTriggerTester.cs renamed to src/UnitTestEx/Hosting/TypeTester.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,42 @@
99
using UnitTestEx.Abstractions;
1010
using UnitTestEx.Assertors;
1111

12-
namespace UnitTestEx.Functions
12+
namespace UnitTestEx.Hosting
1313
{
1414
/// <summary>
15-
/// Provides the Azure Function <see cref="HttpTriggerTester{TFunction}"/> unit-testing capabilities.
15+
/// Provides the generic <see cref="Type"/> unit-testing capabilities.
1616
/// </summary>
17-
/// <typeparam name="TFunction">The Azure Function <see cref="Type"/>.</typeparam>
18-
public class GenericTriggerTester<TFunction> : TriggerTesterBase<TFunction> where TFunction : class
17+
/// <typeparam name="T">The <see cref="Type"/> (must be a <c>class</c>).</typeparam>
18+
public class TypeTester<T> : HostTesterBase<T> where T : class
1919
{
2020
/// <summary>
21-
/// Initializes a new <see cref="GenericTriggerTester{TFunction}"/> class.
21+
/// Initializes a new <see cref="TypeTester{TFunction}"/> class.
2222
/// </summary>
2323
/// <param name="serviceScope">The <see cref="IServiceScope"/>.</param>
2424
/// <param name="implementor">The <see cref="TestFrameworkImplementor"/>.</param>
25-
internal GenericTriggerTester(IServiceScope serviceScope, TestFrameworkImplementor implementor) : base(serviceScope, implementor) { }
25+
internal TypeTester(IServiceScope serviceScope, TestFrameworkImplementor implementor) : base(serviceScope, implementor) { }
2626

2727
/// <summary>
28-
/// Runs the asynchronous function method with no result.
28+
/// Runs the asynchronous method with no result.
2929
/// </summary>
3030
/// <param name="expression">The function execution expression.</param>
3131
/// <returns>A <see cref="VoidAssertor"/>.</returns>
32-
public VoidAssertor Run(Expression<Func<TFunction, Task>> expression)
32+
public VoidAssertor Run(Expression<Func<T, Task>> expression)
3333
{
34-
(Exception? ex, long ms) = RunFunction(expression, null, null);
34+
(Exception? ex, long ms) = Run(expression, null, null);
3535
LogElapsed(ex, ms);
3636
LogTrailer();
3737
return new VoidAssertor(ex, Implementor);
3838
}
3939

4040
/// <summary>
41-
/// Runs the asynchronous function method with a result.
41+
/// Runs the asynchronous method with a result.
4242
/// </summary>
4343
/// <param name="expression">The function execution expression.</param>
4444
/// <returns>A <see cref="ResultAssertor{TResult}"/>.</returns>
45-
public ResultAssertor<TResult> Run<TResult>(Expression<Func<TFunction, Task<TResult>>> expression)
45+
public ResultAssertor<TResult> Run<TResult>(Expression<Func<T, Task<TResult>>> expression)
4646
{
47-
(TResult result, Exception? ex, long ms) = RunFunction(expression, null, null);
47+
(TResult result, Exception? ex, long ms) = Run(expression, null, null);
4848
LogElapsed(ex, ms);
4949

5050
if (ex == null)
@@ -71,7 +71,7 @@ public ResultAssertor<TResult> Run<TResult>(Expression<Func<TFunction, Task<TRes
7171
private void LogElapsed(Exception? ex, long ms)
7272
{
7373
Implementor.WriteLine("");
74-
Implementor.WriteLine("FUNCTION GENERIC-TRIGGER TESTER...");
74+
Implementor.WriteLine("GENERIC TYPE TESTER...");
7575
Implementor.WriteLine($"Elapsed (ms): {ms}");
7676
if (ex != null)
7777
{

0 commit comments

Comments
 (0)