Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,54 @@
## 2.0.4

Major new version to support a major new feature: Enable http interception after the request is sent, so you can return healthy stubs instead of a bad response, log, record the request/response or perform any action you wish.

In the spirit of 'everything as code', you provide a lambda to process HttpRequestMessage and HttpResponseMessage; and with the help of a lot of our new helper methods, you can return stubs, log detailed messages or record the request/response to the file system.

- New feature set
- IServiceCollection.InterceptHttpCallsAfterSending() will receive a lambda with the input parameter InterceptedHttpCall, which contains fields HttpRequestMessage, HttpResponseMessage or Exception, Duration
- Optional parameter InterceptionConfiguration, with the parameters:
- RootStubsFolder (default = /App_Data/SystemTestingTools) where stubs / recording will be read / written from
- ExposeStubsAs (default = Stubs), the URL where your stubs will be browsable (if you call ExposeStubsForDirectoryBrowsing() )
- ForwardHeadersPrefix (default = SystemTestingTools), will forward any header that starts with this prefix to downstream calls; useful in conjunction with InterceptHttpCallsBeforeSending(), so you could drive stubs in downstream systems
- IApplicationBuilder.ExposeStubsForDirectoryBrowsing() will expose the stubs folder for navigation
- InterceptedHttpCall (besides the properties mentioned above) also contains these methods and :
- SaveAsRecording(): will save the request and response (if no exception ocurrred) to a text file in the RootStubsFolder. Optional params:
- relativeFolder: sub folder inside RootStubsFolder where to save, if not provided it will save to root
- fileName: the name of the file (without extension), if not provided the HttpStatus of the response will be used (Ok, Accepted, NotFound, ...)
- howManyFilesToKeep: if zero (the default value), it will keep 'infinite' files, if one obviously only one will be kept, and any other number will be the maximum number. If it's configured to keep more than one, it will add a number at the end of the file name, example: Ok_00001.txt, NotFound_00001.txt, NotFound_00002.txt
- KeepResultUnchanged(): return the same result, without changing anything, same response, or if an exceptino occurred, it will be re-thrown
- Summarize(): Returns the most important metadata of the interception: the full endpoint and the response http status OR exception message; useful if you want to log it or quickly see what happened. Examples:
- POST http://www.dneonline.com/calculator.asmx received httpStatus [OK]
- GET http://www.omdbapia.com?type=movie&t=matrix received exception [No such host is known.]
- ReturnRecording(): Return the recording response, you can obtain the recording by searching for it in RecordingCollection
- ReturnStub(): Return a stub HttpResponse, it can obtained via one of the ResponseFactory methods
- ReturnHandCraftedResponse(): Return a hand crafted HttpResponse created by you
- Note: All the 'Return' methods require a string reason field, for you to explain why you are not returning the original result. This will be put in a header in the response (SystemTestingToolsStub), so consumers will know they are not receiving a live/real response. You can use the method Summarize() to help you create a reason if you wish. Example: "Recording [omdb/new/happy/matrix] reason GET http://www.omdbapia.com?type=movie&t=matrix received httpStatus [BadGateway]"
- RootStubsFolder: exposes the root folder where all the stubs are found, can be useful to create a full path for a stub
- HttpContextAccessor: can be useful to check details about the current request, like headers and parameters
- RecordingCollection contains a list of recordings in base folder. You can use this to find a recording of interest (like a succesfull response for the same endpoint you are hitting now) and return that response instead of the one you currently have

- New features in existing capabilities
- More extension methods for HttpRequestMessage, to make it's usage easier: GetHeaderValue(),
GetSoapAction(), ReadBody() and ReadBody\<T>(), GetQueryValue()
- ServiceEndpoint.EnableHttpInterception() will allow WCF (SOAP) calls to be intercepted by both IServiceCollection.InterceptHttpCallsBeforeSending() and IWebHostBuilder.InterceptHttpCallsAfterSending()
- IWebHostBuilder.InterceptHttpCallsBeforeSending() (formerly known as 'ConfigureInterceptionOfHttpClientCalls') has a new parameter keepListOfOutgoingRequests (optional, default=true); turn it off if you are doing performance tests, as the keeping track of calls might throw stats off or look like a memory leak.
- HttpClient.AppendHttpCallStub() has a new parameter 'counter' (optional, default=1); to represent how many times that response stub will be returned, if more than the limit, an exception will be thrown. Set 0 for infinite, which is very useful for performance testing
- UnsessionedData.AppendGlobalHttpCallStub will append a response (or an exception) to be returned by a when a matching request is intercepted. This is similar to HttpClient.AppendHttpCallStub, which adds stubs to a session; but the global method will add a stub to all sessions; the interceptor will look for a match in the session, and if not found it will look in the global configuration. This is useful as a 'fall back', so you don't have to configure the same response in many methods; can also be useful when doing performance testing, as the 'counter' of this global response is infinite, meaning no matter how many requests are intercepted, the same response will be returned

- Breaking changes
- IWebHostBuilder.ConfigureInterceptionOfHttpClientCalls() has been renamed to IWebHostBuilder.InterceptHttpCallsBeforeSending(), to better show it's intent.
- ContextRepo has been renamed to UnsessionedData, to better show it's intent.
- Class WcfHttpInterceptor and it's methods have been decommissioned:
- CreateRequestResponseRecorder() is no longer necessary, ServiceEndpoint.EnableHttpInterception() enables IServiceCollection.InterceptHttpCallsBeforeSending() to detect http WCF calls
- CreateInterceptor() is no longer necessary, ServiceEndpoint.EnableHttpInterception() enables IWebHostBuilder.InterceptHttpCallsAfterSending() to detect http WCF calls
- Class HttpRequestMessageWrapper has been decommissioned, it was only useful to contain the date the request was sent, this can now be achieved with the extension method GetDatesSent(). Everywhere that used HttpRequestMessageWrapper now uses HttpRequestMessage
- When using extension method GetHeaderValue() or recording requests and responses; the divider between many values in a header has been changed from comma to || (pipe + pipe), as this is a less likely divider to match an existing valid value

- Notes
- Stub vs Recording: a stub (as per industry standard) is a fake response you will return instead of a real response from a downstream system. A recording is a subtype of stub, because it contains a response and also the request that generated it; it's a new concept created by SystemTestingTools, to enable more scenarios: matching the current request with a recording so you can return a healthy response when your downstream system is momentarily unhealthy, documenting how the response was obtained and how to reproduce it, ...


## 1.3.10
- New features (backwards compatible)
- Recorder now generates files with duration of request and a identifier header (SystemTestingTools_Recording.V2), which will enable future features. It can only be read by the new method ResponseFactory.FromRecordedFile()
Expand Down
2 changes: 0 additions & 2 deletions Examples/MovieProject/MovieProject.Logic/MathService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using MovieProject.Logic.Proxy;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MovieProject.Logic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MovieProject.Logic.Option;
using MovieProject.Logic.Proxy;
using NSubstitute;
using Shouldly;

using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -37,12 +39,15 @@ public async Task CanRetrieveMovie_TheMatrix()

var result = await proxy.GetMovieOrTvSeries("movie", "the matrix");

result.ShouldNotBeNull();
result.Id.ShouldBe("tt0133093");
result.Name.ShouldBe("The Matrix");
result.Year.ShouldBe("1999");
using (new AssertionScope())
{
result.Should().NotBeNull();
result.Id.Should().Be("tt0133093");
result.Name.Should().Be("The Matrix");
result.Year.Should().Be("1999");

logger.DidNotReceiveWithAnyArgs().Log(LogLevel.Critical, "", null);
logger.DidNotReceiveWithAnyArgs().Log(LogLevel.Critical, "", null);
}
}

[Fact]
Expand All @@ -52,12 +57,15 @@ public async Task CanRetrieveTvSeries_TheBigBangTheory()

var result = await proxy.GetMovieOrTvSeries("series", "the big bang theory");

result.ShouldNotBeNull();
result.Id.ShouldBe("tt0898266");
result.Name.ShouldBe("The Big Bang Theory");
result.Year.ShouldBe("2007�2019");
using (new AssertionScope())
{
result.Should().NotBeNull();
result.Id.Should().Be("tt0898266");
result.Name.Should().Be("The Big Bang Theory");
result.Year.Should().Be("2007�2019");

logger.DidNotReceiveWithAnyArgs().Log(LogLevel.Critical, "", null);
logger.DidNotReceiveWithAnyArgs().Log(LogLevel.Critical, "", null);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="microsoft.extensions.configuration.json" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
<PackageReference Include="microsoft.extensions.options.configurationextensions" Version="3.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NSubstitute" Version="4.2.1" />
<PackageReference Include="Shouldly" Version="3.0.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using FluentAssertions;
using FluentAssertions.Execution;
using System.Net;
using System.Threading.Tasks;
using SystemTestingTools;
using Xunit;

namespace MovieProject.InterceptionTests
{
[Collection("SharedServer collection")]
[Trait("Project", "MovieProject.InterceptionHappyTests")]
public class GetMovieTests
{
private readonly TestServerFixture Fixture;

public GetMovieTests(TestServerFixture fixture)
{
Fixture = fixture;
}

/// <summary>
/// This test relies on downstream system returning a good response; this was the only way to test fully the recording function
/// </summary>
/// <returns></returns>
[Fact]
public async Task When_UserAsksForMovie_WithNoStubs_Then_RetrieveFromRealServer()
{
// arrange
var client = Fixture.Server.CreateClient();
client.CreateSession();

// act
var httpResponse = await client.GetAsync("/api/movie/inception");

using (new AssertionScope())
{
// assert logs
client.GetSessionLogs().Should().BeEmpty();

// assert return
httpResponse.StatusCode.Should().Be(HttpStatusCode.OK);
httpResponse.GetHeaderValue("SystemTestingToolsStub").Should().BeNull();

var movie = await httpResponse.ReadJsonBody<Logic.DTO.Media>();
movie.Id.Should().Be("tt1375666");
movie.Name.Should().Be("Inception");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<None Remove="appsettings.tests.json" />
</ItemGroup>


<ItemGroup>
<Content Include="..\..\MovieProject.Web\appsettings.json" Link="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\MovieProject.Web\NLog.config" Link="NLog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="appsettings.tests.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NSubstitute" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\Tool\SystemTestingTools\SystemTestingTools.csproj" />
<ProjectReference Include="..\..\MovieProject.Logic\MovieProject.Logic.csproj" />
<ProjectReference Include="..\..\MovieProject.Web\MovieProject.Web.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
This project is just here to test if the MovieWebsite can indeed intercept responses; no preemptive stubbing (for testing purposes will be needed).

This is not a typical test you would find in a project; this is just necessary to test SystemTestingTools.

A typical project can be found in MovieProject.IsolatedTests
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using MovieProject.Web;
using System;
using System.Linq;
using System.Net.Http;
using SystemTestingTools;
using Xunit;

namespace MovieProject.InterceptionTests
{
public class TestServerFixture : IDisposable
{
public TestServer Server { get; private set; }

public TestServerFixture()
{
StartServer();
SanityCheckServer();
}

private void StartServer()
{
var builder = Program.CreateWebHostBuilder(new string[0]) // use the exact same builder as the website, to test the wiring
//.InterceptHttpCallsBeforeSending() we don't intercept Before sending the request, but After, which is configured in website startup
.ConfigureAppConfiguration((hostingContext, config) =>
{
// make small changes to configuration, such as disabling caching
config.AddJsonFile("appsettings.tests.json", optional: false, reloadOnChange: true);
})
.IntercepLogs(minimumLevelToIntercept: LogLevel.Information,
namespaceToIncludeStart: new[] { "MovieProject" },
namespaceToExcludeStart: new[] { "Microsoft" }) // redundand exclusion, just here to show the possible configuration
.UseEnvironment("Development");

Server = new TestServer(builder);
}

/// <summary>
/// Run a quick sanity check, before running any tests
/// </summary>
/// <returns></returns>
private void SanityCheckServer()
{
HttpResponseMessage response = null;
using (var client = Server.CreateClient())
response = client.GetAsync("/healthcheck").Result; // we run the async method synchronously because it's called from a contructor, that can't be async

if (response.StatusCode != System.Net.HttpStatusCode.OK)
throw new ApplicationException("TestServer doesn't respond to basic request to /healthcheck");

if (UnsessionedData.UnsessionedLogs.Count != 1)
throw new ApplicationException($"Expected to find 1 log during startup, found {UnsessionedData.UnsessionedLogs.Count}");

var firstMessage = UnsessionedData.UnsessionedLogs.First()?.ToString();
if (firstMessage != "Information: Application is starting")
throw new ApplicationException($"First log was not the expected one: {firstMessage}");
}

public void Dispose()
{
Server.Dispose();
}
}

[CollectionDefinition("SharedServer collection")]
public class SharedServerCollection : ICollectionFixture<TestServerFixture>
{
// as suggested in https://xunit.github.io/docs/shared-context
// done so this collection is shared between many tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"readme": "this file allows for small changes to the app.settings (added as a LINK), to make easier to test, like disabling state (such as cache)",
"caching": {
"movieApiInSeconds": -1
}
}
Loading