Skip to content

Commit

Permalink
Add integration tests
Browse files Browse the repository at this point in the history
With in-memory EF Core for now
  • Loading branch information
joaopgrassi committed Jul 14, 2020
1 parent 9aca7bc commit 89afa40
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 1 deletion.
7 changes: 7 additions & 0 deletions BlogApp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlogApp.Api", "src\BlogApp.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlogApp.Data", "src\BlogApp.Data\BlogApp.Data.csproj", "{98EDD488-2615-446C-8DA7-DB41CAE4E1FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlogApp.Api.Tests", "tests\BlogApp.Api.Tests\BlogApp.Api.Tests.csproj", "{0FD10789-F06E-4B7A-BD0C-9A237C32690B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -25,13 +27,18 @@ Global
{98EDD488-2615-446C-8DA7-DB41CAE4E1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98EDD488-2615-446C-8DA7-DB41CAE4E1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98EDD488-2615-446C-8DA7-DB41CAE4E1FF}.Release|Any CPU.Build.0 = Release|Any CPU
{0FD10789-F06E-4B7A-BD0C-9A237C32690B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0FD10789-F06E-4B7A-BD0C-9A237C32690B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0FD10789-F06E-4B7A-BD0C-9A237C32690B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0FD10789-F06E-4B7A-BD0C-9A237C32690B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{81EE25D0-1A6B-413B-BC3D-738E7C46E14A} = {5B46D97D-B7F3-496F-8294-789901812B36}
{98EDD488-2615-446C-8DA7-DB41CAE4E1FF} = {5B46D97D-B7F3-496F-8294-789901812B36}
{0FD10789-F06E-4B7A-BD0C-9A237C32690B} = {A64981EA-8482-4379-9A3F-8BF3408ABC22}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3AA60DE2-E1FD-4C66-80BF-90F238F7807B}
Expand Down
2 changes: 1 addition & 1 deletion src/BlogApp.Api/Controllers/BlogsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public async Task<IActionResult> Create([FromBody] CreateBlogRequest createReque
[HttpPut("{id}")]
[ProducesResponseType(typeof(Blog), Status204NoContent)]
[ProducesDefaultResponseType(typeof(string))]
public async Task<IActionResult> Update(Guid id, [FromBody] CreateBlogRequest updateRequest, CancellationToken cancellationToken)
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBlogRequest updateRequest, CancellationToken cancellationToken)
{
var existingBlog = await _dbContext.Blogs
.FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
Expand Down
7 changes: 7 additions & 0 deletions src/BlogApp.Api/Controllers/Models/UpdateBlogRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace BlogApp.Api.Controllers.Models
{
public class UpdateBlogRequest
{
public string Url { get; set; } = null!;
}
}
24 changes: 24 additions & 0 deletions tests/BlogApp.Api.Tests/BlogApp.Api.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.5" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\BlogApp.Api\BlogApp.Api.csproj" />
</ItemGroup>

</Project>
59 changes: 59 additions & 0 deletions tests/BlogApp.Api.Tests/BlogWebApplicationFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using BlogApp.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Linq;

namespace BlogApp.Api.Tests
{
public class BlogWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Test");
builder.ConfigureServices(services =>
{
// Remove the app's ApplicationDbContext registration.
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<BlogDbContext>));
if (descriptor != null)
services.Remove(descriptor);
// Add ApplicationDbContext using an in-memory database for testing.
services.AddDbContext<BlogDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
// print EF debug logs during tests
var fac = LoggerFactory.Create(builder => { builder.AddDebug(); });
options.UseLoggerFactory(fac);
});
using (var scope = services.BuildServiceProvider().CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<BlogDbContext>();
// Ensure the database is created.
db.Database.EnsureCreated();
}
})
.ConfigureAppConfiguration((context, config) =>
{
//config.AddInMemoryCollection(new[]
//{
// // TODO: Add later the docker connection string
//});
})
.ConfigureLogging(builder =>
{
builder.AddDebug();
builder.SetMinimumLevel(LogLevel.Information);
});
}
}
}
82 changes: 82 additions & 0 deletions tests/BlogApp.Api.Tests/Controllers/BlogsControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using BlogApp.Api.Controllers.Models;
using BlogApp.Data.Entities;
using FluentAssertions;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace BlogApp.Api.Tests.Controllers
{
public sealed class BlogsControllerTests : IClassFixture<BlogWebApplicationFactory>
{
private readonly BlogWebApplicationFactory _factory;

public BlogsControllerTests(BlogWebApplicationFactory factory)
{
_factory = factory;
}

[Fact]
public async Task Create_ShouldCreateBlog()
{
// Arrange
var createRequest = new CreateBlogRequest
{
Url = "https://aspnet-core-is-cool.net"
};

var client = _factory.CreateClient();

// Act
var postResponse = await client.PostAsync("/v1/blogs", new JsonContent<CreateBlogRequest>(createRequest));
postResponse.EnsureSuccessStatusCode();
var blogCreateResponse = await postResponse.Content.ReadAsJsonAsync<Blog>();

// Assert - by calling the Get/id and comparing the results
var getResponse = await client.GetAsync($"/v1/blogs/{blogCreateResponse.Id}");
var blogGetResponse = await getResponse.Content.ReadAsJsonAsync<Blog>();

blogGetResponse.Should().BeEquivalentTo(blogCreateResponse);
}

[Fact]
public async Task Update_ShouldUpdateBlogUrl()
{
// Arrange
var client = _factory.CreateClient();

var initialBlog = await CreateRandomBlog(client);

var updateRequest = new UpdateBlogRequest
{
// not a real url but just something random.. we don't have any validation for now
Url = Guid.NewGuid().ToString()
};

// Act
var response = await client.PutAsync($"/v1/blogs/{initialBlog.Id}", new JsonContent<UpdateBlogRequest>(updateRequest));
response.EnsureSuccessStatusCode();

// Assert
var getResponse = await client.GetAsync($"/v1/blogs/{initialBlog.Id}");
var actual = await getResponse.Content.ReadAsJsonAsync<Blog>();

Assert.Equal(updateRequest.Url, actual.Url);
}

private async ValueTask<Blog> CreateRandomBlog(HttpClient client)
{
// Arrange
var createRequest = new CreateBlogRequest
{
Url = "https://aspnet-core-is-cool.net"
};

// Act
var postResponse = await client.PostAsync("/v1/blogs", new JsonContent<CreateBlogRequest>(createRequest));
postResponse.EnsureSuccessStatusCode();
return await postResponse.Content.ReadAsJsonAsync<Blog>();
}
}
}
27 changes: 27 additions & 0 deletions tests/BlogApp.Api.Tests/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:62732/",
"sslPort": 44313
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"BlogApp.Api.Tests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;https://localhost:5000"
}
}
}
32 changes: 32 additions & 0 deletions tests/BlogApp.Api.Tests/Utils/HttpContentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace BlogApp.Api.Tests
{
/// <summary>
/// Extension methods around <see cref="HttpContent"/>
/// </summary>
public static class HttpContentExtensions
{
/// <summary>
/// Reads as <see cref="HttpContent"/> as JSON string and deserializes to <typeparam name="TResult"></typeparam>
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="content">The content.</param>
public static async Task<TResult> ReadAsJsonAsync<TResult>(this HttpContent content)
{
try
{
var responseString = await content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<TResult>(responseString);
}
catch (Exception ex)
{
var rawJson = await content.ReadAsStringAsync().ConfigureAwait(false);
throw new Exception($"Failed to deserialize response:{ rawJson }", ex);
}
}
}
}
19 changes: 19 additions & 0 deletions tests/BlogApp.Api.Tests/Utils/JsonContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System.Net.Http;
using System.Net.Http.Headers;

namespace BlogApp.Api.Tests
{
/// <summary>
/// Helper class to convert a <see cref="StringContent"/> into a JsonContent
/// </summary>
/// <typeparam name="T"></typeparam>
public class JsonContent<T> : StringContent
{
public JsonContent(T entity)
: base(JsonConvert.SerializeObject(entity))
{
Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}
}

0 comments on commit 89afa40

Please sign in to comment.