Adding Tests and Refactoring

Functional Tests for RazorPages added
This commit is contained in:
Steve Smith
2018-05-31 12:28:55 -04:00
parent 5fb9e741dd
commit 814d3e249c
22 changed files with 275 additions and 142 deletions

View File

@@ -1,8 +1,7 @@
using ApplicationCore.Interfaces; using ApplicationCore.Interfaces;
using Ardalis.GuardClauses;
using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
namespace ApplicationCore.Entities.BuyerAggregate namespace ApplicationCore.Entities.BuyerAggregate
{ {
@@ -14,13 +13,15 @@ namespace ApplicationCore.Entities.BuyerAggregate
public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly(); public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly();
protected Buyer() private Buyer()
{ {
// required by EF
} }
public Buyer(string identity) : this() public Buyer(string identity) : this()
{ {
IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity)); Guard.Against.NullOrEmpty(identity, nameof(identity));
IdentityGuid = identity;
} }
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
namespace ApplicationCore.Entities.OrderAggregate namespace ApplicationCore.Entities.OrderAggregate
{ {

View File

@@ -1,21 +1,29 @@
namespace ApplicationCore.Entities.OrderAggregate using Ardalis.GuardClauses;
namespace ApplicationCore.Entities.OrderAggregate
{ {
/// <summary> /// <summary>
/// Represents the item that was ordered. If catalog item details change, details of /// Represents a snapshot of the item that was ordered. If catalog item details change, details of
/// the item that was part of a completed order should not change. /// the item that was part of a completed order should not change.
/// </summary> /// </summary>
public class CatalogItemOrdered // ValueObject public class CatalogItemOrdered // ValueObject
{ {
public CatalogItemOrdered(int catalogItemId, string productName, string pictureUri) public CatalogItemOrdered(int catalogItemId, string productName, string pictureUri)
{ {
Guard.Against.OutOfRange(catalogItemId, nameof(catalogItemId), 1, int.MaxValue);
Guard.Against.NullOrEmpty(productName, nameof(productName));
Guard.Against.NullOrEmpty(pictureUri, nameof(pictureUri));
CatalogItemId = catalogItemId; CatalogItemId = catalogItemId;
ProductName = productName; ProductName = productName;
PictureUri = pictureUri; PictureUri = pictureUri;
} }
private CatalogItemOrdered() private CatalogItemOrdered()
{ {
// required by EF // required by EF
} }
public int CatalogItemId { get; private set; } public int CatalogItemId { get; private set; }
public string ProductName { get; private set; } public string ProductName { get; private set; }
public string PictureUri { get; private set; } public string PictureUri { get; private set; }

View File

@@ -1,4 +1,5 @@
using ApplicationCore.Interfaces; using ApplicationCore.Interfaces;
using Ardalis.GuardClauses;
using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -9,13 +10,18 @@ namespace ApplicationCore.Entities.OrderAggregate
{ {
private Order() private Order()
{ {
// required by EF
} }
public Order(string buyerId, Address shipToAddress, List<OrderItem> items) public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
{ {
Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
Guard.Against.Null(shipToAddress, nameof(shipToAddress));
Guard.Against.Null(items, nameof(items));
BuyerId = buyerId;
ShipToAddress = shipToAddress; ShipToAddress = shipToAddress;
_orderItems = items; _orderItems = items;
BuyerId = buyerId;
} }
public string BuyerId { get; private set; } public string BuyerId { get; private set; }
@@ -43,6 +49,5 @@ namespace ApplicationCore.Entities.OrderAggregate
} }
return total; return total;
} }
} }
} }

View File

@@ -9,9 +9,11 @@ namespace ApplicationCore.Entities.OrderAggregate
public decimal UnitPrice { get; private set; } public decimal UnitPrice { get; private set; }
public int Units { get; private set; } public int Units { get; private set; }
protected OrderItem() private OrderItem()
{ {
// required by EF
} }
public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units)
{ {
ItemOrdered = itemOrdered; ItemOrdered = itemOrdered;

View File

@@ -1,5 +1,4 @@
using ApplicationCore.Exceptions; using ApplicationCore.Exceptions;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
namespace Ardalis.GuardClauses namespace Ardalis.GuardClauses

View File

@@ -1,5 +1,4 @@
using ApplicationCore.Entities.OrderAggregate; using ApplicationCore.Entities.OrderAggregate;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ApplicationCore.Interfaces namespace ApplicationCore.Interfaces

View File

@@ -16,7 +16,7 @@ namespace Infrastructure.Data
{ {
return _dbContext.Orders return _dbContext.Orders
.Include(o => o.OrderItems) .Include(o => o.OrderItems)
.Include("OrderItems.ItemOrdered") .Include($"{nameof(Order.OrderItems)}.{nameof(OrderItem.ItemOrdered)}")
.FirstOrDefault(); .FirstOrDefault();
} }
@@ -24,7 +24,7 @@ namespace Infrastructure.Data
{ {
return _dbContext.Orders return _dbContext.Orders
.Include(o => o.OrderItems) .Include(o => o.OrderItems)
.Include("OrderItems.ItemOrdered") .Include($"{nameof(Order.OrderItems)}.{nameof(OrderItem.ItemOrdered)}")
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
} }

View File

@@ -69,28 +69,5 @@ namespace Microsoft.eShopWeb.Controllers
}; };
return View(viewModel); return View(viewModel);
} }
private OrderViewModel GetOrder()
{
var order = new OrderViewModel()
{
OrderDate = DateTimeOffset.Now.AddDays(-1),
OrderNumber = 12354,
Status = "Submitted",
Total = 123.45m,
ShippingAddress = new Address("123 Main St.", "Kent", "OH", "United States", "44240")
};
order.OrderItems.Add(new OrderItemViewModel()
{
ProductId = 1,
PictureUrl = "",
ProductName = "Something",
UnitPrice = 5.05m,
Units = 2
});
return order;
}
} }
} }

View File

@@ -31,13 +31,15 @@ namespace Microsoft.eShopWeb
public void ConfigureDevelopmentServices(IServiceCollection services) public void ConfigureDevelopmentServices(IServiceCollection services)
{ {
// use in-memory database // use in-memory database
ConfigureTestingServices(services); ConfigureInMemoryDatabases(services);
// use real database // use real database
// ConfigureProductionServices(services); // ConfigureProductionServices(services);
ConfigureServices(services);
} }
public void ConfigureTestingServices(IServiceCollection services)
private void ConfigureInMemoryDatabases(IServiceCollection services)
{ {
// use in-memory database // use in-memory database
services.AddDbContext<CatalogContext>(c => services.AddDbContext<CatalogContext>(c =>
@@ -46,8 +48,6 @@ namespace Microsoft.eShopWeb
// Add Identity DbContext // Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options => services.AddDbContext<AppIdentityDbContext>(options =>
options.UseInMemoryDatabase("Identity")); options.UseInMemoryDatabase("Identity"));
ConfigureServices(services);
} }
public void ConfigureProductionServices(IServiceCollection services) public void ConfigureProductionServices(IServiceCollection services)
@@ -86,6 +86,10 @@ namespace Microsoft.eShopWeb
options.ExpireTimeSpan = TimeSpan.FromHours(1); options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.LoginPath = "/Account/Signin"; options.LoginPath = "/Account/Signin";
options.LogoutPath = "/Account/Signout"; options.LogoutPath = "/Account/Signout";
options.Cookie = new CookieBuilder
{
IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy
};
}); });
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));

View File

@@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" /> <ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" />
<ProjectReference Include="..\..\src\WebRazorPages\WebRazorPages.csproj" />
<ProjectReference Include="..\..\src\Web\Web.csproj" /> <ProjectReference Include="..\..\src\Web\Web.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,17 +1,27 @@
using Microsoft.eShopWeb.ViewModels; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.eShopWeb;
using Microsoft.eShopWeb.ViewModels;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
namespace FunctionalTests.Web.Controllers namespace FunctionalTests.Web.Controllers
{ {
public class ApiCatalogControllerList : BaseWebTest public class ApiCatalogControllerList : IClassFixture<CustomWebApplicationFactory<Startup>>
{ {
public ApiCatalogControllerList(CustomWebApplicationFactory<Startup> factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact] [Fact]
public async Task ReturnsFirst10CatalogItems() public async Task ReturnsFirst10CatalogItems()
{ {
var response = await _client.GetAsync("/api/catalog/list"); var response = await Client.GetAsync("/api/catalog/list");
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync(); var stringResponse = await response.Content.ReadAsStringAsync();
var model = JsonConvert.DeserializeObject<CatalogIndexViewModel>(stringResponse); var model = JsonConvert.DeserializeObject<CatalogIndexViewModel>(stringResponse);
@@ -22,7 +32,7 @@ namespace FunctionalTests.Web.Controllers
[Fact] [Fact]
public async Task ReturnsLast2CatalogItemsGivenPageIndex1() public async Task ReturnsLast2CatalogItemsGivenPageIndex1()
{ {
var response = await _client.GetAsync("/api/catalog/list?page=1"); var response = await Client.GetAsync("/api/catalog/list?page=1");
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync(); var stringResponse = await response.Content.ReadAsStringAsync();
var model = JsonConvert.DeserializeObject<CatalogIndexViewModel>(stringResponse); var model = JsonConvert.DeserializeObject<CatalogIndexViewModel>(stringResponse);

View File

@@ -1,89 +0,0 @@
using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using Microsoft.eShopWeb;
using Microsoft.AspNetCore.Hosting;
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
{
public abstract class BaseWebTest
{
protected readonly HttpClient _client;
protected string _contentRoot;
public BaseWebTest()
{
_client = GetClient();
}
protected HttpClient GetClient()
{
var startupAssembly = typeof(Startup).GetTypeInfo().Assembly;
_contentRoot = GetProjectPath("src", startupAssembly);
var builder = new WebHostBuilder()
.UseContentRoot(_contentRoot)
.UseEnvironment("Testing")
.UseStartup<Startup>();
var server = new TestServer(builder);
// 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>
/// Gets the full path to the target project path that we wish to test
/// </summary>
/// <param name="solutionRelativePath">
/// The parent directory of the target project.
/// e.g. src, samples, test, or test/Websites
/// </param>
/// <param name="startupAssembly">The target project's assembly.</param>
/// <returns>The full path to the target project.</returns>
protected static string GetProjectPath(string solutionRelativePath, Assembly startupAssembly)
{
// Get name of the target project which we want to test
var projectName = startupAssembly.GetName().Name;
// Get currently executing test project path
var applicationBasePath = AppContext.BaseDirectory;
// Find the folder which contains the solution file. We then use this information to find the target
// project which we want to test.
var directoryInfo = new DirectoryInfo(applicationBasePath);
do
{
var solutionFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, "eShopOnWeb.sln"));
if (solutionFileInfo.Exists)
{
return Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath, projectName));
}
directoryInfo = directoryInfo.Parent;
}
while (directoryInfo.Parent != null);
throw new Exception($"Solution root could not be located using application root {applicationBasePath}.");
}
}
}

View File

@@ -10,10 +10,7 @@ namespace FunctionalTests.Web.Controllers
{ {
public CatalogControllerIndex(CustomWebApplicationFactory<Startup> factory) public CatalogControllerIndex(CustomWebApplicationFactory<Startup> factory)
{ {
Client = factory.CreateClient(new WebApplicationFactoryClientOptions Client = factory.CreateClient();
{
AllowAutoRedirect = false
});
} }
public HttpClient Client { get; } public HttpClient Client { get; }

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Infrastructure.Identity;
namespace FunctionalTests.Web.Controllers namespace FunctionalTests.Web.Controllers
{ {
@@ -14,7 +15,6 @@ namespace FunctionalTests.Web.Controllers
{ {
protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void ConfigureWebHost(IWebHostBuilder builder)
{ {
builder.UseEnvironment("Testing");
builder.ConfigureServices(services => builder.ConfigureServices(services =>
{ {
// Create a new service provider. // Create a new service provider.
@@ -30,6 +30,12 @@ namespace FunctionalTests.Web.Controllers
options.UseInternalServiceProvider(serviceProvider); options.UseInternalServiceProvider(serviceProvider);
}); });
services.AddDbContext<AppIdentityDbContext>(options =>
{
options.UseInMemoryDatabase("Identity");
options.UseInternalServiceProvider(serviceProvider);
});
// Build the service provider. // Build the service provider.
var sp = services.BuildServiceProvider(); var sp = services.BuildServiceProvider();

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.eShopWeb;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace FunctionalTests.Web.Controllers
{
public class OrderIndexOnGet : IClassFixture<CustomWebApplicationFactory<Startup>>
{
public OrderIndexOnGet(CustomWebApplicationFactory<Startup> factory)
{
Client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsRedirectGivenAnonymousUser()
{
var response = await Client.GetAsync("/Order/Index");
var redirectLocation = response.Headers.Location.OriginalString;
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Contains("Account/Signin", redirectLocation);
}
}
}

View File

@@ -0,0 +1,70 @@
using Infrastructure.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.eShopWeb;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;
using Infrastructure.Identity;
namespace FunctionalTests.WebRazorPages
{
public class CustomWebRazorPagesApplicationFactory<TStartup>
: WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<CatalogContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
services.AddDbContext<AppIdentityDbContext>(options =>
{
options.UseInMemoryDatabase("Identity");
options.UseInternalServiceProvider(serviceProvider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<CatalogContext>();
var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
var logger = scopedServices
.GetRequiredService<ILogger<CustomWebRazorPagesApplicationFactory<TStartup>>>();
// Ensure the database is created.
db.Database.EnsureCreated();
try
{
// Seed the database with test data.
CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test messages. Error: {ex.Message}");
}
}
});
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.eShopWeb.RazorPages;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace FunctionalTests.WebRazorPages
{
public class HomePageOnGet : IClassFixture<CustomWebRazorPagesApplicationFactory<Startup>>
{
public HomePageOnGet(CustomWebRazorPagesApplicationFactory<Startup> factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsHomePageWithProductListing()
{
// Arrange & Act
var response = await Client.GetAsync("/");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
// Assert
Assert.Contains(".NET Bot Black Sweatshirt", stringResponse);
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.eShopWeb;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace FunctionalTests.WebRazorPages
{
public class OrderIndexOnGet : IClassFixture<CustomWebRazorPagesApplicationFactory<Startup>>
{
public OrderIndexOnGet(CustomWebRazorPagesApplicationFactory<Startup> factory)
{
Client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsRedirectGivenAnonymousUser()
{
var response = await Client.GetAsync("/Order/Index");
var redirectLocation = response.Headers.Location.OriginalString;
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Contains("Account/Signin", redirectLocation);
}
}
}

View File

@@ -5,7 +5,7 @@ using Xunit;
namespace UnitTests.ApplicationCore.Entities.BasketTests namespace UnitTests.ApplicationCore.Entities.BasketTests
{ {
public class AddItem public class Total
{ {
private int _testCatalogItemId = 123; private int _testCatalogItemId = 123;
private decimal _testUnitPrice = 1.23m; private decimal _testUnitPrice = 1.23m;

View File

@@ -0,0 +1,41 @@
using ApplicationCore.Entities.OrderAggregate;
using System.Collections.Generic;
using UnitTests.Builders;
using Xunit;
namespace UnitTests.ApplicationCore.Entities.OrderTests
{
public class Total
{
private decimal _testUnitPrice = 42m;
[Fact]
public void IsZeroForNewOrder()
{
var order = new OrderBuilder().WithNoItems();
Assert.Equal(0, order.Total());
}
[Fact]
public void IsCorrectGiven1Item()
{
var builder = new OrderBuilder();
var items = new List<OrderItem>
{
new OrderItem(builder.TestCatalogItemOrdered, _testUnitPrice, 1)
};
var order = new OrderBuilder().WithItems(items);
Assert.Equal(_testUnitPrice, order.Total());
}
[Fact]
public void IsCorrectGiven3Items()
{
var builder = new OrderBuilder();
var order = builder.WithDefaultValues();
Assert.Equal(builder.TestUnitPrice * builder.TestUnits, order.Total());
}
}
}

View File

@@ -1,7 +1,5 @@
using ApplicationCore.Entities.OrderAggregate; using ApplicationCore.Entities.OrderAggregate;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
namespace UnitTests.Builders namespace UnitTests.Builders
{ {
@@ -34,5 +32,17 @@ namespace UnitTests.Builders
_order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), itemList); _order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), itemList);
return _order; return _order;
} }
public Order WithNoItems()
{
_order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), new List<OrderItem>());
return _order;
}
public Order WithItems(List<OrderItem> items)
{
_order = new Order(TestBuyerId, new AddressBuilder().WithDefaultValues(), items);
return _order;
}
} }
} }