Skip to content

Commit

Permalink
Feature/async await (#5)
Browse files Browse the repository at this point in the history
* CallResponse refactored to async/await

* Fix for too early dispose.

* Fixed tests.

* Testfix.

* Fixed few warnings.

* Readme fixes.
  • Loading branch information
savpek authored May 7, 2019
1 parent b7da6df commit 4b80ab5
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.4.0" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="NSubstitute" Version="3.1.0" />
<PackageReference Include="xunit" Version="2.3.1" />
Expand Down
4 changes: 1 addition & 3 deletions Protacon.NetCore.WebApi.TestUtil.Tests/TestStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
using Protacon.NetCore.WebApi.TestUtil.Tests.Dummy;
using Xunit;

[assembly: CollectionBehavior(DisableTestParallelization = true)]

namespace Protacon.NetCore.WebApi.TestUtil.Tests
{
public class TestStartup
Expand All @@ -22,7 +20,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton(Substitute.For<IExternalDepency>());
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddDebug();
app.UseMvc();
Expand Down
38 changes: 22 additions & 16 deletions Protacon.NetCore.WebApi.TestUtil.Tests/Tests/BasicFlowTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.Threading.Tasks;
using FluentAssertions;
using Protacon.NetCore.WebApi.TestUtil.Tests.Dummy;
using Xunit;
Expand All @@ -8,60 +9,65 @@ namespace Protacon.NetCore.WebApi.TestUtil.Tests.Tests
public class BasicFlowTests
{
[Fact]
public void WhenGetIsCalled_ThenAssertingItWorks()
public async Task WhenGetIsCalled_ThenAssertingItWorks()
{
TestHost.Run<TestStartup>().Get("/returnthree/")
var foo = await TestHost.Run<TestStartup>().Get("/returnthree/")
.ExpectStatusCode(HttpStatusCode.OK)
.WithContentOf<int>();

await TestHost.Run<TestStartup>().Get("/returnthree/")
.ExpectStatusCode(HttpStatusCode.OK)
.WithContentOf<int>()
.Passing(
x => x.Should().Be(3));

TestHost.Run<TestStartup>().Get("/returnthree/")
.Invoking(x => x.ExpectStatusCode(HttpStatusCode.NoContent))
.Should().Throw<ExpectedStatusCodeException>();
.Awaiting(x => x.ExpectStatusCode(HttpStatusCode.NoContent))
.Should()
.Throw<ExpectedStatusCodeException>();
}

[Fact]
public void WhenDeleteIsCalled_ThenAssertingItWorks()
public async Task WhenDeleteIsCalled_ThenAssertingItWorks()
{
TestHost.Run<TestStartup>().Delete("/something/abc")
await TestHost.Run<TestStartup>().Delete("/something/abc")
.ExpectStatusCode(HttpStatusCode.NoContent);

TestHost.Run<TestStartup>().Delete("/something/abc")
.Invoking(x => x.ExpectStatusCode(HttpStatusCode.NotFound))
.Awaiting(x => x.ExpectStatusCode(HttpStatusCode.NotFound))
.Should().Throw<ExpectedStatusCodeException>();
}

[Fact]
public void WhenPutIsCalled_ThenAssertingItWorks()
public async Task WhenPutIsCalled_ThenAssertingItWorks()
{
TestHost.Run<TestStartup>().Put("/returnsame/", new DummyRequest { Value = "3" })
await TestHost.Run<TestStartup>().Put("/returnsame/", new DummyRequest { Value = "3" })
.ExpectStatusCode(HttpStatusCode.OK)
.WithContentOf<DummyRequest>()
.Passing(x => x.Value.Should().Be("3"));

TestHost.Run<TestStartup>().Put("/returnsame/", new { value = 3 })
.Invoking(x => x.ExpectStatusCode(HttpStatusCode.NotFound))
.Awaiting(x => x.ExpectStatusCode(HttpStatusCode.NotFound))
.Should().Throw<ExpectedStatusCodeException>();
}

[Fact]
public void WhenPostIsCalled_ThenAssertingItWorks()
public async Task WhenPostIsCalled_ThenAssertingItWorks()
{
TestHost.Run<TestStartup>().Post("/returnsame/", new DummyRequest { Value = "3" })
await TestHost.Run<TestStartup>().Post("/returnsame/", new DummyRequest { Value = "3" })
.ExpectStatusCode(HttpStatusCode.OK)
.WithContentOf<DummyRequest>()
.Passing(x => x.Value.Should().Be("3"));

TestHost.Run<TestStartup>().Post("/returnsame/", new { value = 3 })
.Invoking(x => x.ExpectStatusCode(HttpStatusCode.NotFound))
.Awaiting(x => x.ExpectStatusCode(HttpStatusCode.NotFound))
.Should().Throw<ExpectedStatusCodeException>();
}

[Fact]
public void WhenNonAcceptedCodeIsExpected_ThenAcceptItAsResult()
public async Task WhenNonAcceptedCodeIsExpected_ThenAcceptItAsResult()
{
TestHost.Run<TestStartup>().Get("/errorcontent/")
await TestHost.Run<TestStartup>().Get("/errorcontent/")
.ExpectStatusCode(HttpStatusCode.NotFound)
.WithContentOf<DummyRequest>()
.Passing(x => x.Value.Should().Be("error"));
Expand All @@ -71,7 +77,7 @@ public void WhenNonAcceptedCodeIsExpected_ThenAcceptItAsResult()
public void WhenExpectedCodeIsNotDefinedOnError_ThenFail()
{
TestHost.Run<TestStartup>().Get("/errorcontent/")
.Invoking(x => x.WithContentOf<DummyRequest>())
.Awaiting(x => x.WithContentOf<DummyRequest>())
.Should().Throw<ExpectedStatusCodeException>();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

Expand All @@ -9,13 +10,14 @@ namespace Protacon.NetCore.WebApi.TestUtil.Tests.Tests
public class HeaderSupportTest
{
[Fact]
public void WhenHeadersAreDefined_ThenPassThemToApi()
public async Task WhenHeadersAreDefined_ThenPassThemToApi()
{
TestHost.Run<TestStartup>().Get("/headertest/",
await TestHost.Run<TestStartup>().Get("/headertest/",
headers: new Dictionary<string, string> {{"example", "somevalue"}})
.ExpectStatusCode(HttpStatusCode.NoContent);

TestHost.Run<TestStartup>().Invoking(x => x.Get("/headertest/",
TestHost.Run<TestStartup>()
.Awaiting(x => x.Get("/headertest/",
headers: new Dictionary<string, string> {{"somethingElse", "somevalue"}})
.ExpectStatusCode(HttpStatusCode.NoContent))
.Should().Throw<InvalidOperationException>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.Threading.Tasks;
using FluentAssertions;
using NSubstitute;
using Protacon.NetCore.WebApi.TestUtil.Tests.Dummy;
Expand All @@ -9,14 +10,14 @@ namespace Protacon.NetCore.WebApi.TestUtil.Tests.Tests
public class MockDepenciesTests
{
[Fact]
public void WhenHeadersAreDefined_ThenPassThemToApi()
public async Task WhenHeadersAreDefined_ThenPassThemToApi()
{
var host = TestHost.Run<TestStartup>();

// See TestStartup for information what is done before this.
host.Setup<IExternalDepency>(x => x.SomeCall(Arg.Is("abc")).Returns("3"));

host.Get("/external/abc")
await host.Get("/external/abc")
.ExpectStatusCode(HttpStatusCode.OK)
.WithContentOf<DummyRequest>()
.Passing(x => x.Value.Should().Be("3"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

Expand All @@ -10,18 +11,18 @@ namespace Protacon.NetCore.WebApi.TestUtil.Tests.Tests
public class NonCommonDatatypeTests
{
[Fact]
public void WhenFileIsDownloaded_ThenResultsCanBeAsserted()
public async Task WhenFileIsDownloaded_ThenResultsCanBeAsserted()
{
TestHost.Run<TestStartup>().Get("/file/")
await TestHost.Run<TestStartup>().Get("/file/")
.ExpectStatusCode(HttpStatusCode.OK)
.WithContentOf<Byte[]>()
.Passing(x => x.Length.Should().Be(4));
}

[Fact]
public void WhenHtmlPageIsReturned_ThenResultsCanBeAsserted()
public async Task WhenHtmlPageIsReturned_ThenResultsCanBeAsserted()
{
TestHost.Run<TestStartup>().Get("/page/")
await TestHost.Run<TestStartup>().Get("/page/")
.ExpectStatusCode(HttpStatusCode.OK)
.WithContentOf<string>()
.Passing(x => x.Should().Contain("Hello World"));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net;
using System.Threading.Tasks;
using FluentAssertions;
using Protacon.NetCore.WebApi.TestUtil.Extensions;
using Xunit;
Expand All @@ -12,15 +13,15 @@ public class WaitForStatusCodeTests
public void WhenErronousCodeIsReturned_ThenThrowErrorAfterTimeout()
{
TestHost.Run<TestStartup>().Get("/returnthree/")
.Invoking(x => x.WaitForStatusCode(HttpStatusCode.BadRequest, TimeSpan.FromSeconds(2)))
.Awaiting(x => x.WaitForStatusCode(HttpStatusCode.BadRequest, TimeSpan.FromSeconds(2)))
.Should().Throw<ExpectedStatusCodeException>();
}

[Fact]
public void WhenValidCodeIsReturned_ThenReturnWithoutError()
{
TestHost.Run<TestStartup>().Get("/returnthree/")
.Invoking(x => x.WaitForStatusCode(HttpStatusCode.OK, TimeSpan.FromSeconds(2)))
.Awaiting(x => x.WaitForStatusCode(HttpStatusCode.OK, TimeSpan.FromSeconds(2)))
.Should().NotThrow<Exception>();
}
}
Expand Down
24 changes: 3 additions & 21 deletions Protacon.NetCore.WebApi.TestUtil/CallData.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
using System;

namespace Protacon.NetCore.WebApi.TestUtil
namespace Protacon.NetCore.WebApi.TestUtil
{
public class CallData<T>
{
private readonly T _data;
internal readonly T Data;

public CallData(T data)
{
_data = data;
}

public CallData<T> Passing(Action<T> asserts)
{
asserts.Invoke(_data);
return this;
}

public TSelect Select<TSelect>(Func<T, TSelect> selector)
{
return selector.Invoke(_data);
}

public T Select()
{
return _data;
Data = data;
}
}
}
27 changes: 27 additions & 0 deletions Protacon.NetCore.WebApi.TestUtil/CallDataExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Threading.Tasks;

namespace Protacon.NetCore.WebApi.TestUtil
{
public static class CallDataExtensions
{
public static async Task<CallData<T>> Passing<T>(this Task<CallData<T>> dataWrapperTask, Action<T> asserts)
{
var wrappedData = await dataWrapperTask;
asserts(wrappedData.Data);
return wrappedData;
}

public static async Task<TSelect> Select<T, TSelect>(this Task<CallData<T>> dataWrapperTask, Func<T, TSelect> selector)
{
var wrappedData = await dataWrapperTask;
return selector.Invoke(wrappedData.Data);
}

public static async Task<T> Select<T>(this Task<CallData<T>> dataWrapperTask)
{
var data = await dataWrapperTask;
return data.Data;
}
}
}
100 changes: 100 additions & 0 deletions Protacon.NetCore.WebApi.TestUtil/CallExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Protacon.NetCore.WebApi.TestUtil
{
public static class CallExtensions
{
public static async Task<Call> ExpectStatusCode(this Task<Call> callTask, HttpStatusCode code)
{
var call = await callTask.ConfigureAwait(false);
var httpCall = await call.HttpTask.ConfigureAwait(false);

if (httpCall.StatusCode != code)
{
throw new ExpectedStatusCodeException($"Expected statuscode '{code}' but got '{(int)httpCall.StatusCode}'");
}

call.ExpectedStatusCode = code;

return call;
}

public static async Task<Call> HeaderPassing(this Task<Call> callTask, string header, Action<string> assertsForValue)
{
var call = await callTask.ConfigureAwait(false);
var response = await call.HttpTask.ConfigureAwait(false);

var match = response.Headers
.SingleOrDefault(x => x.Key == header);

if (match.Equals(default(KeyValuePair<string, IEnumerable<string>>)))
throw new InvalidOperationException($"Header '{header}' not found, available headers are '{HeadersAsReadableList(response)}'");

assertsForValue.Invoke(match.Value.Single());

return call;
}

private static string HeadersAsReadableList(HttpResponseMessage message)
{
return message.Headers.Select(x => x.Key.ToString()).Aggregate("", (a, b) => $"{a}, {b}");
}

public static async Task<CallData<T>> WithContentOf<T>(this Task<Call> callTask)
{
var call = await callTask.ConfigureAwait(false);
var result = await call.HttpTask.ConfigureAwait(false);
var code = (int)result.StatusCode;

var content = await result.Content.ReadAsByteArrayAsync().ConfigureAwait(false);

if ((code > 299 || code < 199) && code != (int?)call.ExpectedStatusCode)
throw new ExpectedStatusCodeException(
$"Tried to get data from non ok statuscode response, expected status is '2xx' or '{call.ExpectedStatusCode}' but got '{code}' with content '{Encoding.Default.GetString(content)}'");

if (!result.Content.Headers.Contains("Content-Type"))
throw new InvalidOperationException("Response didn't contain any 'Content-Type'. Reason may be that you didn't return anything?");

var contentType = result.Content.Headers.Single(x => x.Key == "Content-Type").Value.FirstOrDefault() ?? "";


switch (contentType)
{
case var ctype when ctype.StartsWith("application/json"):
return ParseJson<T>(content);
case "application/pdf":
if (typeof(T) != typeof(byte[]))
throw new InvalidOperationException("Only output type of 'byte[]' is supported for 'application/pdf'.");

return new CallData<T>((T)(object)content.ToArray());
default:
if (typeof(T) != typeof(string))
throw new InvalidOperationException($"Only output type of 'string' is supported for '{contentType}'.");

return new CallData<T>((T)(object)Encoding.Default.GetString(content));
}
}

private static CallData<T> ParseJson<T>(byte[] content)
{
var asString = Encoding.Default.GetString(content);

try
{
var asObject = JsonConvert.DeserializeObject<T>(asString);
return new CallData<T>(asObject);
}
catch (JsonSerializationException)
{
throw new InvalidOperationException($"Cannot serialize '{asString}' as type '{typeof(T)}'");
}
}
}
}
Loading

0 comments on commit 4b80ab5

Please sign in to comment.