From 8225c5ff0092f2d666a8c012dffbbc0b428c7d9b Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 17 Aug 2019 20:33:01 +0800 Subject: Finally solve the database conflict problem in unit tests. --- Timeline.Tests/AuthorizationUnitTest.cs | 11 +- Timeline.Tests/Helpers/MyWebApplicationFactory.cs | 81 ++++----- Timeline.Tests/TokenUnitTest.cs | 83 ++++----- Timeline.Tests/UserUnitTest.cs | 201 +++++++++++++--------- Timeline.Tests/UsernameValidatorUnitTest.cs | 23 ++- 5 files changed, 215 insertions(+), 184 deletions(-) diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs index 6f52a12d..4751e95f 100644 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ b/Timeline.Tests/AuthorizationUnitTest.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; +using System; using System.Net; using System.Threading.Tasks; using Timeline.Tests.Helpers; @@ -9,17 +10,23 @@ using Xunit.Abstractions; namespace Timeline.Tests { - public class AuthorizationUnitTest : IClassFixture> + public class AuthorizationUnitTest : IClassFixture>, IDisposable { private const string AuthorizeUrl = "Test/User/Authorize"; private const string UserUrl = "Test/User/User"; private const string AdminUrl = "Test/User/Admin"; private readonly WebApplicationFactory _factory; + private readonly Action _disposeAction; public AuthorizationUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) { - _factory = factory.WithTestLogging(outputHelper); + _factory = factory.WithTestConfig(outputHelper, out _disposeAction); + } + + public void Dispose() + { + _disposeAction(); } [Fact] diff --git a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs b/Timeline.Tests/Helpers/MyWebApplicationFactory.cs index b49756e4..dfadd1ae 100644 --- a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs +++ b/Timeline.Tests/Helpers/MyWebApplicationFactory.cs @@ -5,6 +5,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System; using Timeline.Entities; using Timeline.Services; using Timeline.Tests.Mock.Data; @@ -15,69 +16,57 @@ namespace Timeline.Tests.Helpers { public class MyWebApplicationFactory : WebApplicationFactory where TStartup : class { - // We should keep the connection, so the database is persisted but not recreate every time. - // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests . - private readonly SqliteConnection _databaseConnection; - - public MyWebApplicationFactory() : base() + protected override void ConfigureWebHost(IWebHostBuilder builder) { - _databaseConnection = new SqliteConnection("Data Source=:memory:;"); - _databaseConnection.Open(); - - InitDatabase(); + builder.ConfigureTestServices(services => + { + services.AddSingleton(); + }); } + } - private void InitDatabase() + public static class WebApplicationFactoryExtensions + { + public static WebApplicationFactory WithTestConfig(this WebApplicationFactory factory, ITestOutputHelper outputHelper, out Action disposeAction) where TEntry : class { - var options = new DbContextOptionsBuilder() - .UseSqlite(_databaseConnection) - .Options; + // We should keep the connection, so the database is persisted but not recreate every time. + // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests . + SqliteConnection _databaseConnection = new SqliteConnection("Data Source=:memory:;"); + _databaseConnection.Open(); - using (var context = new DatabaseContext(options)) { - context.Database.EnsureCreated(); - context.Users.AddRange(MockUsers.Users); - context.SaveChanges(); - } - } + var options = new DbContextOptionsBuilder() + .UseSqlite(_databaseConnection) + .Options; - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.ConfigureServices(services => - { - services.AddEntityFrameworkSqlite(); - services.AddDbContext(options => + using (var context = new DatabaseContext(options)) { - options.UseSqlite(_databaseConnection); - }); - }) - .ConfigureTestServices(services => - { - services.AddSingleton(); - }); - } + context.Database.EnsureCreated(); + context.Users.AddRange(MockUsers.Users); + context.SaveChanges(); + }; + } - protected override void Dispose(bool disposing) - { - if (disposing) + disposeAction = () => { _databaseConnection.Close(); _databaseConnection.Dispose(); - } - - base.Dispose(disposing); - } - } + }; - public static class WebApplicationFactoryExtensions - { - public static WebApplicationFactory WithTestLogging(this WebApplicationFactory factory, ITestOutputHelper outputHelper) where TEntry : class - { return factory.WithWebHostBuilder(builder => { - builder.ConfigureLogging(logging => + builder + .ConfigureLogging(logging => { logging.AddXunit(outputHelper); + }) + .ConfigureServices(services => + { + services.AddEntityFrameworkSqlite(); + services.AddDbContext(options => + { + options.UseSqlite(_databaseConnection); + }); }); }); } diff --git a/Timeline.Tests/TokenUnitTest.cs b/Timeline.Tests/TokenUnitTest.cs index b5d8a2c8..3babacf7 100644 --- a/Timeline.Tests/TokenUnitTest.cs +++ b/Timeline.Tests/TokenUnitTest.cs @@ -15,43 +15,36 @@ using Xunit.Abstractions; namespace Timeline.Tests { - public class TokenUnitTest : IClassFixture> + public class TokenUnitTest : IClassFixture>, IDisposable { private const string CreateTokenUrl = "token/create"; private const string VerifyTokenUrl = "token/verify"; private readonly WebApplicationFactory _factory; + private readonly Action _disposeAction; public TokenUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) { - _factory = factory.WithTestLogging(outputHelper); + _factory = factory.WithTestConfig(outputHelper, out _disposeAction); } - [Fact] - public async void CreateToken_MissingUsername() + public void Dispose() { - using (var client = _factory.CreateDefaultClient()) - { - await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl, - new CreateTokenRequest { Username = null, Password = "user" }); - } + _disposeAction(); } [Fact] - public async void CreateToken_InvalidModel_MissingPassword() + public async void CreateToken_InvalidModel() { using (var client = _factory.CreateDefaultClient()) { + // missing username + await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl, + new CreateTokenRequest { Username = null, Password = "user" }); + // missing password await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = null }); - } - } - - [Fact] - public async void CreateToken_InvalidModel_BadExpireOffset() - { - using (var client = _factory.CreateDefaultClient()) - { + // bad expire offset await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl, new CreateTokenRequest { @@ -101,10 +94,11 @@ namespace Timeline.Tests } [Fact] - public async void VerifyToken_InvalidModel_MissingToken() + public async void VerifyToken_InvalidModel() { using (var client = _factory.CreateDefaultClient()) { + // missing token await InvalidModelTestHelpers.TestPostInvalidModel(client, VerifyTokenUrl, new VerifyTokenRequest { Token = null }); } @@ -122,43 +116,42 @@ namespace Timeline.Tests } [Fact] - public async void VerifyToken_BadVersion_AND_UserNotExist() + public async void VerifyToken_BadVersion() { using (var client = _factory.CreateDefaultClient()) { + var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword)).Token; + using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. { // create a user for test var userService = scope.ServiceProvider.GetRequiredService(); + await userService.PatchUser(MockUsers.UserUsername, null, null); + } - const string username = "verifytokentest0"; - const string password = "12345678"; - - await userService.PutUser(username, password, false); - - // create a token - var token = (await client.CreateUserTokenAsync(username, password)).Token; - - // increase version - await userService.PatchUser(username, null, null); - - // test against bad version - var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); - response.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_BadVersion); - - - // create another token - var token2 = (await client.CreateUserTokenAsync(username, password)).Token; + var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); + response.Should().HaveStatusCodeBadRequest() + .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_BadVersion); + } + } - // delete user - await userService.DeleteUser(username); + [Fact] + public async void VerifyToken_UserNotExist() + { + using (var client = _factory.CreateDefaultClient()) + { + var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword)).Token; - // test against user not exist - var response2 = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); - response2.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_UserNotExist); + using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. + { + // create a user for test + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.DeleteUser(MockUsers.UserUsername); } + + var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token }); + response.Should().HaveStatusCodeBadRequest() + .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_UserNotExist); } } diff --git a/Timeline.Tests/UserUnitTest.cs b/Timeline.Tests/UserUnitTest.cs index 2aa89fe3..7bf12ad8 100644 --- a/Timeline.Tests/UserUnitTest.cs +++ b/Timeline.Tests/UserUnitTest.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; +using System; using System.Net.Http; using System.Threading.Tasks; using Timeline.Controllers; @@ -13,13 +14,19 @@ using Xunit.Abstractions; namespace Timeline.Tests { - public class UserUnitTest : IClassFixture> + public class UserUnitTest : IClassFixture>, IDisposable { private readonly WebApplicationFactory _factory; + private readonly Action _disposeAction; public UserUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) { - _factory = factory.WithTestLogging(outputHelper); + _factory = factory.WithTestConfig(outputHelper, out _disposeAction); + } + + public void Dispose() + { + _disposeAction(); } [Fact] @@ -28,9 +35,8 @@ namespace Timeline.Tests using (var client = await _factory.CreateClientAsAdmin()) { var res = await client.GetAsync("users"); - // Because tests are running asyncronized. So database may be modified and - // we can't check the exact user lists at this point. So only check the format. - res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson(); + res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson() + .Which.Should().BeEquivalentTo(MockUsers.UserInfos); } } @@ -58,118 +64,156 @@ namespace Timeline.Tests } [Fact] - public async Task Put_Patch_Delete_User() + public async Task Put_InvalidModel() { using (var client = await _factory.CreateClientAsAdmin()) { - const string username = "putpatchdeleteuser"; - const string password = "password"; - const string url = "users/" + username; - - // Put Invalid Model + const string url = "users/aaaaaaaa"; + // missing password await InvalidModelTestHelpers.TestPutInvalidModel(client, url, new UserPutRequest { Password = null, Administrator = false }); - await InvalidModelTestHelpers.TestPutInvalidModel(client, url, new UserPutRequest { Password = password, Administrator = null }); + // missing administrator + await InvalidModelTestHelpers.TestPutInvalidModel(client, url, new UserPutRequest { Password = "???", Administrator = null }); + } + } - async Task CheckAdministrator(bool administrator) + [Fact] + public async Task Put_BadUsername() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest { - var res = await client.GetAsync(url); - res.Should().HaveStatusCodeOk() - .And.Should().HaveBodyAsJson() - .Which.Administrator.Should().Be(administrator); - } + Password = "???", + Administrator = false + }); + res.Should().HaveStatusCodeBadRequest() + .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Put_BadUsername); + } + } - { - // Put Bad Username. - var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest - { - Password = password, - Administrator = false - }); - res.Should().HaveStatusCodeBadRequest() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Put_BadUsername); - } + private async Task CheckAdministrator(HttpClient client, string username, bool administrator) + { + var res = await client.GetAsync("users/" + username); + res.Should().HaveStatusCodeOk() + .And.Should().HaveBodyAsJson() + .Which.Administrator.Should().Be(administrator); + } + [Fact] + public async Task Put_Modiefied() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + var res = await client.PutAsJsonAsync("users/" + MockUsers.UserUsername, new UserPutRequest { - // Put Created. - var res = await client.PutAsJsonAsync(url, new UserPutRequest - { - Password = password, - Administrator = false - }); - res.Should().BePutCreated(); - await CheckAdministrator(false); - } + Password = "password", + Administrator = false + }); + res.Should().BePutModified(); + await CheckAdministrator(client, MockUsers.UserUsername, false); + } + } - { - // Put Modified. - var res = await client.PutAsJsonAsync(url, new UserPutRequest - { - Password = password, - Administrator = true - }); - res.Should().BePutModified(); - await CheckAdministrator(true); - } + [Fact] + public async Task Put_Created() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + const string username = "puttest"; + const string url = "users/" + username; - // Patch Not Exist + var res = await client.PutAsJsonAsync(url, new UserPutRequest { - var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); - res.Should().HaveStatusCodeNotFound() - .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Patch_NotExist); - } + Password = "password", + Administrator = false + }); + res.Should().BePutCreated(); + await CheckAdministrator(client, username, false); + } + } - // Patch Success + [Fact] + public async Task Patch_NotExist() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); + res.Should().HaveStatusCodeNotFound() + .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Patch_NotExist); + } + } + + [Fact] + public async Task Patch_Success() + { + using (var client = await _factory.CreateClientAsAdmin()) + { { - var res = await client.PatchAsJsonAsync(url, new UserPatchRequest { Administrator = false }); + var res = await client.PatchAsJsonAsync("users/" + MockUsers.UserUsername, + new UserPatchRequest { Administrator = false }); res.Should().HaveStatusCodeOk(); - await CheckAdministrator(false); + await CheckAdministrator(client, MockUsers.UserUsername, false); } + } + } - // Delete Deleted + [Fact] + public async Task Delete_Deleted() + { + using (var client = await _factory.CreateClientAsAdmin()) + { { + var url = "users/" + MockUsers.UserUsername; var res = await client.DeleteAsync(url); res.Should().BeDeleteDeleted(); var res2 = await client.GetAsync(url); res2.Should().HaveStatusCodeNotFound(); } + } + } - // Delete Not Exist + [Fact] + public async Task Delete_NotExist() + { + using (var client = await _factory.CreateClientAsAdmin()) + { { - var res = await client.DeleteAsync(url); + var res = await client.DeleteAsync("users/usernotexist"); res.Should().BeDeleteNotExist(); } } } - - public class ChangePasswordUnitTest : IClassFixture> + public class ChangePasswordUnitTest : IClassFixture>, IDisposable { private const string url = "userop/changepassword"; private readonly WebApplicationFactory _factory; + private readonly Action _disposeAction; public ChangePasswordUnitTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) { - _factory = factory.WithTestLogging(outputHelper); + _factory = factory.WithTestConfig(outputHelper, out _disposeAction); } - - [Fact] - public async Task InvalidModel_OldPassword() + public void Dispose() { - using (var client = await _factory.CreateClientAsUser()) - { - await InvalidModelTestHelpers.TestPostInvalidModel(client, url, new ChangePasswordRequest { OldPassword = null, NewPassword = "???" }); - } + _disposeAction(); } + [Fact] - public async Task InvalidModel_NewPassword() + public async Task InvalidModel() { using (var client = await _factory.CreateClientAsUser()) { - await InvalidModelTestHelpers.TestPostInvalidModel(client, url, new ChangePasswordRequest { OldPassword = "???", NewPassword = null }); + // missing old password + await InvalidModelTestHelpers.TestPostInvalidModel(client, url, + new ChangePasswordRequest { OldPassword = null, NewPassword = "???" }); + // missing new password + await InvalidModelTestHelpers.TestPostInvalidModel(client, url, + new ChangePasswordRequest { OldPassword = "???", NewPassword = null }); } } @@ -187,22 +231,13 @@ namespace Timeline.Tests [Fact] public async Task Success() { - const string username = "changepasswordtest"; - const string password = "password"; - - // create a new user to avoid interference - using (var client = await _factory.CreateClientAsAdmin()) - { - var res = await client.PutAsJsonAsync("users/" + username, new UserPutRequest { Password = password, Administrator = false }); - res.Should().BePutCreated(); - } - - using (var client = await _factory.CreateClientWithCredential(username, password)) + using (var client = await _factory.CreateClientAsUser()) { const string newPassword = "new"; - var res = await client.PostAsJsonAsync(url, new ChangePasswordRequest { OldPassword = password, NewPassword = newPassword }); + var res = await client.PostAsJsonAsync(url, + new ChangePasswordRequest { OldPassword = MockUsers.UserPassword, NewPassword = newPassword }); res.Should().HaveStatusCodeOk(); - await client.CreateUserTokenAsync(username, newPassword); + await client.CreateUserTokenAsync(MockUsers.UserUsername, newPassword); } } } diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 450564b7..20558d0e 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -14,13 +14,6 @@ namespace Timeline.Tests _validator = validator; } - [Fact] - public void NullShouldThrow() - { - _validator.Invoking(v => v.Validate(null, out string message)).Should().Throw(); - } - - private string FailAndMessage(string username) { var result = _validator.Validate(username, out var message); @@ -31,7 +24,21 @@ namespace Timeline.Tests private void Succeed(string username) { _validator.Validate(username, out var message).Should().BeTrue(); - message.Should().BeNull(); + message.Should().Be(ValidationConstants.SuccessMessage); + } + + [Fact] + public void Null() + { + FailAndMessage(null).Should().ContainEquivalentOf("null"); + } + + [Fact] + public void NotString() + { + var result = _validator.Validate(123, out var message); + result.Should().BeFalse(); + message.Should().ContainEquivalentOf("type"); } [Fact] -- cgit v1.2.3