Refactoring and Adding Tests (#58)

* Moving Identity seeding to its own class and method.

* Adding tests for AddItem

* Added catalog api controller and functional tests
Added and cleaned up some comments

* Adding integration tests for OrderRepository

* Getting integration test for order working with inmemory db
This commit is contained in:
Steve Smith
2017-10-20 12:52:42 -04:00
committed by GitHub
parent 32950aa175
commit 0eb4d72b89
17 changed files with 306 additions and 94 deletions

View File

@@ -1,5 +1,7 @@
namespace Microsoft.eShopWeb.ApplicationCore.Entities namespace Microsoft.eShopWeb.ApplicationCore.Entities
{ {
// This can easily be modified to be BaseEntity<T> and public T Id to support different key types.
// Using non-generic integer types for simplicity and to ease caching logic
public class BaseEntity public class BaseEntity
{ {
public int Id { get; set; } public int Id { get; set; }

View File

@@ -5,6 +5,5 @@
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public int Quantity { get; set; } public int Quantity { get; set; }
public int CatalogItemId { get; set; } public int CatalogItemId { get; set; }
// public CatalogItem Item { get; set; }
} }
} }

View File

@@ -1,29 +0,0 @@
using System;
namespace ApplicationCore.Exceptions
{
/// <summary>
/// Note: No longer required.
/// </summary>
public class CatalogImageMissingException : Exception
{
public CatalogImageMissingException(string message,
Exception innerException = null)
: base(message, innerException: innerException)
{
}
public CatalogImageMissingException(Exception innerException)
: base("No catalog image found for the provided id.",
innerException: innerException)
{
}
public CatalogImageMissingException() : base()
{
}
public CatalogImageMissingException(string message) : base(message)
{
}
}
}

View File

@@ -12,7 +12,6 @@ namespace ApplicationCore.Services
public string ComposePicUri(string uriTemplate) public string ComposePicUri(string uriTemplate)
{ {
return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl); return uriTemplate.Replace("http://catalogbaseurltobereplaced", _catalogSettings.CatalogBaseUrl);
} }
} }
} }

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
namespace Infrastructure.Identity
{
public class AppIdentityDbContextSeed
{
public static async Task SeedAsync(UserManager<ApplicationUser> userManager)
{
var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" };
await userManager.CreateAsync(defaultUser, "Pass@word1");
}
}
}

View File

@@ -0,0 +1,10 @@
using Microsoft.eShopWeb.Services;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Controllers.Api
{
[Route("api/[controller]/[action]")]
public class BaseApiController : Controller
{ }
}

View File

@@ -0,0 +1,21 @@
using Microsoft.eShopWeb.Services;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Controllers.Api
{
public class CatalogController : BaseApiController
{
private readonly ICatalogService _catalogService;
public CatalogController(ICatalogService catalogService) => _catalogService = catalogService;
[HttpGet]
public async Task<IActionResult> List(int? brandFilterApplied, int? typesFilterApplied, int? page)
{
var itemsPage = 10;
var catalogModel = await _catalogService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied);
return Ok(catalogModel);
}
}
}

View File

@@ -18,25 +18,20 @@ namespace Microsoft.eShopWeb
using (var scope = host.Services.CreateScope()) using (var scope = host.Services.CreateScope())
{ {
var services = scope.ServiceProvider; var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try try
{ {
var catalogContext = services.GetRequiredService<CatalogContext>(); var catalogContext = services.GetRequiredService<CatalogContext>();
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
CatalogContextSeed.SeedAsync(catalogContext, loggerFactory) CatalogContextSeed.SeedAsync(catalogContext, loggerFactory)
.Wait(); .Wait();
// move to IdentitySeed method
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>(); var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" }; AppIdentityDbContextSeed.SeedAsync(userManager).Wait();
userManager.CreateAsync(defaultUser, "Pass@word1").Wait();
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine(ex.ToString()); var logger = loggerFactory.CreateLogger<Program>();
//var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex, "An error occurred seeding the DB.");
//logger.LogError(ex, "An error occurred seeding the DB.");
} }
} }

View File

@@ -30,29 +30,54 @@ namespace Microsoft.eShopWeb
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services) public void ConfigureDevelopmentServices(IServiceCollection services)
{ {
// use in-memory database
ConfigureTestingServices(services);
// use real database
// ConfigureProductionServices(services);
}
public void ConfigureTestingServices(IServiceCollection services)
{
// use in-memory database
services.AddDbContext<CatalogContext>(c =>
c.UseInMemoryDatabase("Catalog"));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseInMemoryDatabase("Identity"));
ConfigureServices(services);
}
public void ConfigureProductionServices(IServiceCollection services)
{
// use real database
services.AddDbContext<CatalogContext>(c => services.AddDbContext<CatalogContext>(c =>
{ {
try try
{ {
//c.UseInMemoryDatabase("Catalog");
// Requires LocalDB which can be installed with SQL Server Express 2016 // Requires LocalDB which can be installed with SQL Server Express 2016
// https://www.microsoft.com/en-us/download/details.aspx?id=54284 // https://www.microsoft.com/en-us/download/details.aspx?id=54284
c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection")); c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection"));
} }
catch (System.Exception ex ) catch (System.Exception ex)
{ {
var message = ex.Message; var message = ex.Message;
} }
}); });
// Add Identity DbContext // Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options => services.AddDbContext<AppIdentityDbContext>(options =>
//options.UseInMemoryDatabase("Identity"));
options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection"))); options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));
ConfigureServices(services);
}
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>() services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppIdentityDbContext>() .AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
@@ -129,22 +154,5 @@ namespace Microsoft.eShopWeb
await context.Response.WriteAsync(sb.ToString()); await context.Response.WriteAsync(sb.ToString());
})); }));
} }
// moved to Program.cs
//public void ConfigureDevelopment(IApplicationBuilder app,
// IHostingEnvironment env,
// ILoggerFactory loggerFactory,
// UserManager<ApplicationUser> userManager,
// CatalogContext catalogContext)
//{
// Configure(app, env);
// //Seed Data
// CatalogContextSeed.SeedAsync(app, catalogContext, loggerFactory)
// .Wait();
// var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" };
// userManager.CreateAsync(defaultUser, "Pass@word1").Wait();
//}
} }
} }

View File

@@ -0,0 +1,34 @@
using System.IO;
using Xunit;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Microsoft.eShopWeb.ViewModels;
using System.Linq;
namespace FunctionalTests.Web.Controllers
{
public class ApiCatalogControllerList : BaseWebTest
{
[Fact]
public async Task ReturnsFirst10CatalogItems()
{
var response = await _client.GetAsync("/api/catalog/list");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
var model = JsonConvert.DeserializeObject<CatalogIndexViewModel>(stringResponse);
Assert.Equal(10, model.CatalogItems.Count());
}
[Fact]
public async Task ReturnsLast2CatalogItemsGivenPageIndex1()
{
var response = await _client.GetAsync("/api/catalog/list?page=1");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
var model = JsonConvert.DeserializeObject<CatalogIndexViewModel>(stringResponse);
Assert.Equal(2, model.CatalogItems.Count());
}
}
}

View File

@@ -6,6 +6,11 @@ using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.eShopWeb; using Microsoft.eShopWeb;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Logging;
using Infrastructure.Data;
using Infrastructure.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
namespace FunctionalTests.Web.Controllers namespace FunctionalTests.Web.Controllers
{ {
@@ -25,12 +30,25 @@ namespace FunctionalTests.Web.Controllers
_contentRoot = GetProjectPath("src", startupAssembly); _contentRoot = GetProjectPath("src", startupAssembly);
var builder = new WebHostBuilder() var builder = new WebHostBuilder()
.UseContentRoot(_contentRoot) .UseContentRoot(_contentRoot)
.UseEnvironment("Testing")
.UseStartup<Startup>(); .UseStartup<Startup>();
var server = new TestServer(builder); var server = new TestServer(builder);
var client = server.CreateClient();
return client; // seed data
using (var scope = server.Host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var catalogContext = services.GetRequiredService<CatalogContext>();
CatalogContextSeed.SeedAsync(catalogContext, loggerFactory)
.Wait();
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
AppIdentityDbContextSeed.SeedAsync(userManager).Wait();
}
return server.CreateClient();
} }
/// <summary> /// <summary>

View File

@@ -1,29 +0,0 @@
using System.IO;
using Xunit;
using System.Threading.Tasks;
namespace FunctionalTests.Web.Controllers
{
public class CatalogControllerGetImage : BaseWebTest
{
//[Fact]
// GetImage replaced by static file middleware
public async Task ReturnsFileContentResultGivenValidId()
{
var testFilePath = Path.Combine(_contentRoot, "pics//1.png");
var expectedFileBytes = File.ReadAllBytes(testFilePath);
var response = await _client.GetAsync("/catalog/pic/1");
response.EnsureSuccessStatusCode();
var streamResponse = await response.Content.ReadAsStreamAsync();
byte[] byteResult;
using (var ms = new MemoryStream())
{
streamResponse.CopyTo(ms);
byteResult = ms.ToArray();
}
Assert.Equal(expectedFileBytes, byteResult);
}
}
}

View File

@@ -19,6 +19,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Web\Web.csproj" /> <ProjectReference Include="..\..\src\Web\Web.csproj" />
<ProjectReference Include="..\UnitTests\UnitTests.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,46 @@
using ApplicationCore.Entities.OrderAggregate;
using Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using UnitTests.Builders;
using Xunit;
using Xunit.Abstractions;
namespace IntegrationTests.Repositories.OrderRepositoryTests
{
public class GetById
{
private readonly CatalogContext _catalogContext;
private readonly OrderRepository _orderRepository;
private OrderBuilder OrderBuilder { get; } = new OrderBuilder();
private readonly ITestOutputHelper _output;
public GetById(ITestOutputHelper output)
{
_output = output;
var dbOptions = new DbContextOptionsBuilder<CatalogContext>()
.UseInMemoryDatabase(databaseName: "TestCatalog")
.Options;
_catalogContext = new CatalogContext(dbOptions);
_orderRepository = new OrderRepository(_catalogContext);
}
[Fact]
public void GetsExistingOrder()
{
var existingOrder = OrderBuilder.WithDefaultValues();
_catalogContext.Orders.Add(existingOrder);
_catalogContext.SaveChanges();
int orderId = existingOrder.Id;
_output.WriteLine($"OrderId: {orderId}");
var orderFromRepo = _orderRepository.GetById(orderId);
Assert.Equal(OrderBuilder.TestBuyerId, orderFromRepo.BuyerId);
// Note: Using InMemoryDatabase OrderItems is available. Will be null if using SQL DB.
var firstItem = orderFromRepo.OrderItems.FirstOrDefault();
Assert.Equal(OrderBuilder.TestUnits, firstItem.Units);
}
}
}

View File

@@ -0,0 +1,57 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Linq;
using Xunit;
namespace UnitTests.ApplicationCore.Entities.BasketTests
{
public class AddItem
{
private int _testCatalogItemId = 123;
private decimal _testUnitPrice = 1.23m;
private int _testQuantity = 2;
[Fact]
public void AddsBasketItemIfNotPresent()
{
var basket = new Basket();
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
var firstItem = basket.Items.Single();
Assert.Equal(_testCatalogItemId, firstItem.CatalogItemId);
Assert.Equal(_testUnitPrice, firstItem.UnitPrice);
Assert.Equal(_testQuantity, firstItem.Quantity);
}
[Fact]
public void IncrementsQuantityOfItemIfPresent()
{
var basket = new Basket();
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
var firstItem = basket.Items.Single();
Assert.Equal(_testQuantity * 2, firstItem.Quantity);
}
[Fact]
public void KeepsOriginalUnitPriceIfMoreItemsAdded()
{
var basket = new Basket();
basket.AddItem(_testCatalogItemId, _testUnitPrice, _testQuantity);
basket.AddItem(_testCatalogItemId, _testUnitPrice * 2, _testQuantity);
var firstItem = basket.Items.Single();
Assert.Equal(_testUnitPrice, firstItem.UnitPrice);
}
[Fact]
public void DefaultsToQuantityOfOne()
{
var basket = new Basket();
basket.AddItem(_testCatalogItemId, _testUnitPrice);
var firstItem = basket.Items.Single();
Assert.Equal(1, firstItem.Quantity);
}
}
}

View File

@@ -0,0 +1,28 @@
using ApplicationCore.Entities.OrderAggregate;
namespace UnitTests.Builders
{
public class AddressBuilder
{
private Address _address;
public string TestStreet => "123 Main St.";
public string TestCity => "Kent";
public string TestState => "OH";
public string TestCountry => "USA";
public string TestZipCode => "44240";
public AddressBuilder()
{
_address = WithDefaultValues();
}
public Address Build()
{
return _address;
}
public Address WithDefaultValues()
{
_address = new Address(TestStreet, TestCity, TestState, TestCountry, TestZipCode);
return _address;
}
}
}

View File

@@ -0,0 +1,38 @@
using ApplicationCore.Entities.OrderAggregate;
using System;
using System.Collections.Generic;
using System.Text;
namespace UnitTests.Builders
{
public class OrderBuilder
{
private Order _order;
public string TestBuyerId => "12345";
public int TestCatalogItemId => 234;
public string TestProductName => "Test Product Name";
public string TestPictureUri => "http://test.com/image.jpg";
public decimal TestUnitPrice = 1.23m;
public int TestUnits = 3;
public CatalogItemOrdered TestCatalogItemOrdered { get; }
public OrderBuilder()
{
TestCatalogItemOrdered = new CatalogItemOrdered(TestCatalogItemId, TestProductName, TestPictureUri);
_order = WithDefaultValues();
}
public Order Build()
{
return _order;
}
public Order WithDefaultValues()
{
var orderItem = new OrderItem(TestCatalogItemOrdered, TestUnitPrice, TestUnits);
var itemList = new List<OrderItem>() { orderItem };
_order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), itemList);
return _order;
}
}
}