├── .gitignore ├── .vscode └── tasks.json ├── AspNetCoreTesting.Api.Tests ├── AspNetCoreTesting.Api.Tests.csproj ├── Extensions │ └── SqlCommandExtensions.cs ├── Infrastructure │ ├── SettingsControllerTestBase.cs │ ├── TestBase.cs │ ├── TestHelper.cs │ └── UserControllerTestBase.cs ├── SettingsControllerTests.cs ├── SettingsControllerTestsWithTestBase.cs ├── SettingsControllerTestsWithTestHelper.cs ├── TestRunStart.cs ├── UsersControllerTests.cs ├── UsersControllerTestsWithTestBase.cs └── UsersControllerTestsWithTestHelper.cs ├── AspNetCoreTesting.Api.sln ├── AspNetCoreTesting.Api ├── AspNetCoreTesting.Api.csproj ├── AspNetCoreTesting.Api.csproj.user ├── Controllers │ ├── SettingsController.cs │ └── UsersController.cs ├── Data │ ├── ApiContext.cs │ ├── Entities │ │ └── User.cs │ └── Migrations │ │ └── DbMigration.cs ├── Models │ └── AddUserModel.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── DummyNotificationService.cs │ ├── INotificationService.cs │ ├── IUsers.cs │ └── Users.cs ├── appsettings.Test.json └── appsettings.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | obj/ 3 | bin/ -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/AspNetCoreTesting.Api.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/Extensions/SqlCommandExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System.Threading.Tasks; 3 | 4 | namespace AspNetCoreTesting.Api.Tests.Extensions 5 | { 6 | public static class SqlCommandExtensions 7 | { 8 | public static Task AddUser(this SqlCommand cmd, int id = 1, string firstName = "John", string lastName = "Doe") 9 | { 10 | cmd.CommandText = "SET IDENTITY_INSERT Users ON; " + 11 | "INSERT INTO Users (Id, FirstName, LastName) " + 12 | $"VALUES ({id}, '{firstName}', '{lastName}'); " + 13 | "SET IDENTITY_INSERT Users OFF;"; 14 | return cmd.ExecuteNonQueryAsync(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/Infrastructure/SettingsControllerTestBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Storage; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace AspNetCoreTesting.Api.Tests.Infrastructure 9 | { 10 | public abstract class SettingsControllerTestBase : TestBase 11 | { 12 | protected Task RunTest(Func test, string? environmentName = null, IDictionary? configuration = null) 13 | { 14 | return RunTestInternal( 15 | services => Task.CompletedTask, 16 | client => test(client), 17 | services => Task.CompletedTask, 18 | environmentName, 19 | configuration 20 | ); 21 | } 22 | 23 | protected override void ConfigureTestServices(IServiceCollection services) {} 24 | protected override IEnumerable InitializeTransactions(IServiceProvider services) 25 | => new IDbContextTransaction[0]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/Infrastructure/TestBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.AspNetCore.TestHost; 4 | using Microsoft.EntityFrameworkCore.Storage; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Net.Http; 10 | using System.Net.Http.Headers; 11 | using System.Threading.Tasks; 12 | 13 | namespace AspNetCoreTesting.Api.Tests.Infrastructure 14 | { 15 | public abstract class TestBase 16 | { 17 | protected async Task RunTestInternal(Func populateDatabase, Func test, 18 | Func validateDatabase, string? environmentName = null, 19 | IDictionary? configuration = null) 20 | { 21 | var application = new WebApplicationFactory().WithWebHostBuilder(builder => { 22 | builder.ConfigureTestServices(ConfigureTestServices); 23 | 24 | if (environmentName != null) 25 | builder.UseEnvironment(environmentName); 26 | 27 | if (configuration != null) 28 | { 29 | var config = new ConfigurationBuilder().AddInMemoryCollection(configuration).Build(); 30 | builder.UseConfiguration(config); 31 | } 32 | }); 33 | 34 | using (var services = application.Services.CreateScope()) 35 | { 36 | IEnumerable transactions = new IDbContextTransaction[0]; 37 | try 38 | { 39 | transactions = InitializeTransactions(services.ServiceProvider); 40 | 41 | await populateDatabase(services.ServiceProvider); 42 | 43 | var client = application.CreateClient(); 44 | 45 | await test(client); 46 | 47 | await validateDatabase(services.ServiceProvider); 48 | } 49 | finally 50 | { 51 | foreach (var transaction in transactions) 52 | { 53 | transaction.Rollback(); 54 | } 55 | } 56 | } 57 | } 58 | 59 | protected abstract void ConfigureTestServices(IServiceCollection services); 60 | protected abstract IEnumerable InitializeTransactions(IServiceProvider services); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/Infrastructure/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using Bazinga.AspNetCore.Authentication.Basic; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Mvc.Testing; 5 | using Microsoft.AspNetCore.TestHost; 6 | using Microsoft.Data.SqlClient; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Net.Http; 14 | using System.Net.Http.Headers; 15 | using System.Threading.Tasks; 16 | 17 | namespace AspNetCoreTesting.Api.Tests.Infrastructure 18 | { 19 | public class TestHelper where TEntryPoint : class 20 | { 21 | private Dictionary> _dbContexts = new(); 22 | private Dictionary> _dbPreparations = new(); 23 | private List> _serviceOverrides = new(); 24 | private List> _clientPreparations = new(); 25 | private Dictionary> _postTestDbValidations = new(); 26 | private string? _environmentName; 27 | private IDictionary? _config; 28 | 29 | public TestHelper AddDbContext(string connectionString) where TContext : DbContext 30 | { 31 | _dbContexts.Add(typeof(TContext), x => 32 | { 33 | var options = new DbContextOptionsBuilder() 34 | .UseSqlServer(connectionString) 35 | .Options; 36 | return (TContext)Activator.CreateInstance(typeof(TContext), options)!; 37 | }); 38 | 39 | return this; 40 | } 41 | 42 | public TestHelper PrepareDb(Func callback) where TContext : DbContext 43 | { 44 | _dbPreparations.Add(typeof(TContext), callback); 45 | 46 | return this; 47 | } 48 | 49 | public TestHelper AddFakeAuth(string username, string password) 50 | { 51 | _serviceOverrides.Add(services => 52 | { 53 | services.AddAuthentication() 54 | .AddBasicAuthentication(credentials => Task.FromResult(credentials.username == username && credentials.password == password)); 55 | 56 | services.AddAuthorization(config => 57 | { 58 | config.DefaultPolicy = new AuthorizationPolicyBuilder(config.DefaultPolicy) 59 | .AddAuthenticationSchemes(BasicAuthenticationDefaults.AuthenticationScheme) 60 | .Build(); 61 | }); 62 | }); 63 | 64 | return this; 65 | } 66 | 67 | public TestHelper AddFakeClientAuth(string username, string password) 68 | { 69 | _clientPreparations.Add(client => 70 | { 71 | var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{username}:{password}")); 72 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); 73 | }); 74 | 75 | return this; 76 | } 77 | 78 | public TestHelper OverrideServices(Action callback) 79 | { 80 | _serviceOverrides.Add(callback); 81 | 82 | return this; 83 | } 84 | 85 | public TestHelper ValidatePostTestDb(Func callback) where TContext : DbContext 86 | { 87 | _postTestDbValidations.Add(typeof(TContext), callback); 88 | 89 | return this; 90 | } 91 | 92 | public TestHelper SetEnvironmentName(string name) 93 | { 94 | _environmentName = name; 95 | 96 | return this; 97 | } 98 | 99 | public TestHelper AddConfiguration(IDictionary config) 100 | { 101 | _config = config; 102 | 103 | return this; 104 | } 105 | 106 | public async Task Run(Func test) 107 | { 108 | var application = new WebApplicationFactory().WithWebHostBuilder(builder => { 109 | if (_environmentName != null) 110 | builder.UseEnvironment(_environmentName); 111 | 112 | if (_config != null) 113 | { 114 | var config = new ConfigurationBuilder() 115 | .AddInMemoryCollection(_config) 116 | .Build(); 117 | builder.UseConfiguration(config); 118 | } 119 | 120 | builder.ConfigureTestServices(services => { 121 | 122 | foreach (var key in _dbContexts.Keys) 123 | { 124 | services.AddSingleton(key, x => _dbContexts[key](x)); 125 | } 126 | 127 | foreach (var fn in _serviceOverrides) 128 | { 129 | fn(services); 130 | } 131 | }); 132 | }); 133 | 134 | using (var services = application.Services.CreateScope()) 135 | { 136 | var transactions = new Dictionary(); 137 | 138 | try 139 | { 140 | foreach (var key in _dbContexts.Keys) 141 | { 142 | var ctx = (DbContext)services.ServiceProvider.GetRequiredService(key); 143 | 144 | transactions.Add(key, ctx.Database.BeginTransaction()); 145 | 146 | if (_dbPreparations.ContainsKey(key)) 147 | { 148 | 149 | var conn = ctx.Database.GetDbConnection(); 150 | using (var cmd = conn.CreateCommand()) 151 | { 152 | cmd.Transaction = transactions[key].GetDbTransaction(); 153 | await _dbPreparations[key]((SqlCommand)cmd); 154 | } 155 | } 156 | } 157 | 158 | var client = application.CreateClient(); 159 | 160 | foreach (var fn in _clientPreparations) 161 | { 162 | fn(client); 163 | } 164 | 165 | await test(client); 166 | 167 | foreach (var key in _dbContexts.Keys) 168 | { 169 | if (_postTestDbValidations.ContainsKey(key)) 170 | { 171 | var ctx = (DbContext)services.ServiceProvider.GetRequiredService(key); 172 | var conn = ctx.Database.GetDbConnection(); 173 | 174 | using (var cmd = conn.CreateCommand()) 175 | { 176 | cmd.Transaction = transactions.GetValueOrDefault(key)?.GetDbTransaction(); 177 | await _postTestDbValidations[key]((SqlCommand)cmd); 178 | } 179 | } 180 | } 181 | } 182 | finally 183 | { 184 | foreach (var tran in transactions.Values) 185 | { 186 | tran.Rollback(); 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/Infrastructure/UserControllerTestBase.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data; 2 | using AspNetCoreTesting.Api.Services; 3 | using Bazinga.AspNetCore.Authentication.Basic; 4 | using FakeItEasy; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Data.Common; 12 | using System.Net.Http; 13 | using System.Net.Http.Headers; 14 | using System.Threading.Tasks; 15 | 16 | namespace AspNetCoreTesting.Api.Tests.Infrastructure 17 | { 18 | public abstract class UserControllerTestBase : TestBase 19 | { 20 | private const string Username = "Test"; 21 | private const string Password = "test"; 22 | private readonly string base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{Username}:{Password}")); 23 | private const string SqlConnectionString = "Server=localhost,14331;Database=AspNetCoreTesting;User Id=sa;Password=P@ssword123"; 24 | protected INotificationService NotificationServiceFake = A.Fake(); 25 | 26 | protected Task RunTest(Func test, Func? populateDatabase = null, Func? validateDatabase = null, bool addAuth = true) 27 | { 28 | return RunTestInternal( 29 | async services => 30 | { 31 | if (populateDatabase != null) 32 | { 33 | var ctx = services.GetRequiredService(); 34 | var conn = ctx.Database.GetDbConnection(); 35 | using (var cmd = conn.CreateCommand()) 36 | { 37 | cmd.Transaction = ctx.Database.CurrentTransaction?.GetDbTransaction(); 38 | await populateDatabase(cmd); 39 | } 40 | } 41 | }, 42 | client => { 43 | if (addAuth) 44 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); 45 | return test(client); 46 | }, 47 | async services => 48 | { 49 | if (validateDatabase != null) 50 | { 51 | var ctx = services.GetRequiredService(); 52 | var conn = ctx.Database.GetDbConnection(); 53 | using (var cmd = conn.CreateCommand()) 54 | { 55 | cmd.Transaction = ctx.Database.CurrentTransaction?.GetDbTransaction(); 56 | await validateDatabase(cmd); 57 | } 58 | } 59 | } 60 | ); 61 | } 62 | 63 | protected override void ConfigureTestServices(IServiceCollection services) 64 | { 65 | var options = new DbContextOptionsBuilder() 66 | .UseSqlServer(SqlConnectionString) 67 | .Options; 68 | services.AddSingleton(options); 69 | services.AddSingleton(); 70 | services.AddSingleton(NotificationServiceFake); 71 | 72 | services.AddAuthentication() 73 | .AddBasicAuthentication(credentials => Task.FromResult(credentials.username == Username && credentials.password == Password)); 74 | 75 | services.AddAuthorization(config => 76 | { 77 | config.DefaultPolicy = new AuthorizationPolicyBuilder(config.DefaultPolicy) 78 | .AddAuthenticationSchemes(BasicAuthenticationDefaults.AuthenticationScheme) 79 | .Build(); 80 | }); 81 | } 82 | protected override IEnumerable InitializeTransactions(IServiceProvider services) 83 | { 84 | var ctx = services.GetRequiredService(); 85 | return new[] { ctx.Database.BeginTransaction() }; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/SettingsControllerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.Extensions.Configuration; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace AspNetCoreTesting.Api.Tests 10 | { 11 | public class SettingsControllerTests 12 | { 13 | [Fact] 14 | public async Task Gets_production_setting_if_no_other_configuration_is_set_up() 15 | { 16 | var application = new WebApplicationFactory(); 17 | 18 | var client = application.CreateClient(); 19 | 20 | var response = await client.GetAsync("/settings/mysetting"); 21 | 22 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 23 | Assert.Equal("Production setting", await response.Content.ReadAsStringAsync()); 24 | } 25 | 26 | [Fact] 27 | public async Task Gets_test_setting_if_environment_name_is_set_to_Test() 28 | { 29 | var application = new WebApplicationFactory().WithWebHostBuilder(builder => { 30 | builder.UseEnvironment("Test"); 31 | }); 32 | 33 | var client = application.CreateClient(); 34 | 35 | var response = await client.GetAsync("/settings/mysetting"); 36 | 37 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 38 | Assert.Equal("Test setting", await response.Content.ReadAsStringAsync()); 39 | } 40 | 41 | [Fact] 42 | public async Task Gets_in_memory_setting_if_set() 43 | { 44 | var application = new WebApplicationFactory().WithWebHostBuilder(builder => { 45 | builder.ConfigureAppConfiguration(config => { 46 | config.AddInMemoryCollection(new Dictionary { 47 | { "MySetting", "In-memory setting" } 48 | }); 49 | }); 50 | }); 51 | 52 | var client = application.CreateClient(); 53 | 54 | var response = await client.GetAsync("/settings/mysetting"); 55 | 56 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 57 | Assert.Equal("In-memory setting", await response.Content.ReadAsStringAsync()); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/SettingsControllerTestsWithTestBase.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Tests.Infrastructure; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace AspNetCoreTesting.Api.Tests 8 | { 9 | public class SettingsControllerTestsWithTestBase : SettingsControllerTestBase 10 | { 11 | [Fact] 12 | public Task Gets_production_setting_if_no_other_configuration_is_set_up() 13 | => RunTest( 14 | test: async client => 15 | { 16 | var response = await client.GetAsync("/settings/mysetting"); 17 | 18 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 19 | Assert.Equal("Production setting", await response.Content.ReadAsStringAsync()); 20 | } 21 | ); 22 | 23 | [Fact] 24 | public Task Gets_test_setting_if_environment_name_is_set_to_Test() 25 | => RunTest( 26 | test: async client => 27 | { 28 | var response = await client.GetAsync("/settings/mysetting"); 29 | 30 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 31 | Assert.Equal("Test setting", await response.Content.ReadAsStringAsync()); 32 | }, 33 | "Test" 34 | ); 35 | 36 | [Fact] 37 | public Task Gets_in_memory_setting_if_set() 38 | => RunTest( 39 | test: async client => 40 | { 41 | var response = await client.GetAsync("/settings/mysetting"); 42 | 43 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 44 | Assert.Equal("In-memory setting", await response.Content.ReadAsStringAsync()); 45 | }, 46 | configuration: new Dictionary { 47 | { "MySetting", "In-memory setting" } 48 | } 49 | ); 50 | } 51 | } -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/SettingsControllerTestsWithTestHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using AspNetCoreTesting.Api.Tests.Infrastructure; 5 | using Xunit; 6 | 7 | namespace AspNetCoreTesting.Api.Tests 8 | { 9 | public class SettingsControllerTestsWithTestHelper 10 | { 11 | [Fact] 12 | public Task Gets_production_setting_if_no_other_configuration_is_set_up() 13 | => new TestHelper() 14 | .Run(async client => { 15 | var response = await client.GetAsync("/settings/mysetting"); 16 | 17 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 18 | Assert.Equal("Production setting", await response.Content.ReadAsStringAsync()); 19 | }); 20 | 21 | [Fact] 22 | public Task Gets_test_setting_if_environment_name_is_set_to_Test() 23 | => new TestHelper() 24 | .SetEnvironmentName("Test") 25 | .Run(async client => 26 | { 27 | var response = await client.GetAsync("/settings/mysetting"); 28 | 29 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 30 | Assert.Equal("Test setting", await response.Content.ReadAsStringAsync()); 31 | }); 32 | 33 | [Fact] 34 | public Task Gets_in_memory_setting_if_set() 35 | => new TestHelper() 36 | .AddConfiguration(new Dictionary { 37 | { "MySetting", "In-memory setting" } 38 | }) 39 | .Run(async client => 40 | { 41 | var response = await client.GetAsync("/settings/mysetting"); 42 | 43 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 44 | Assert.Equal("In-memory setting", await response.Content.ReadAsStringAsync()); 45 | }); 46 | } 47 | } -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/TestRunStart.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data; 2 | using Microsoft.EntityFrameworkCore; 3 | using Xunit.Abstractions; 4 | using Xunit.Sdk; 5 | 6 | [assembly: Xunit.TestFramework("AspNetCoreTesting.Api.Tests.TestRunStart", "AspNetCoreTesting.Api.Tests")] 7 | 8 | namespace AspNetCoreTesting.Api.Tests 9 | { 10 | public class TestRunStart : XunitTestFramework 11 | { 12 | public TestRunStart(IMessageSink messageSink) : base(messageSink) 13 | { 14 | var options = new DbContextOptionsBuilder() 15 | .UseSqlServer("Server=localhost,14331;Database=AspNetCoreTesting;User Id=sa;Password=P@ssword123;"); 16 | var dbContext = new ApiContext(options.Options); 17 | dbContext.Database.EnsureCreated(); 18 | dbContext.Database.Migrate(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/UsersControllerTests.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data; 2 | using AspNetCoreTesting.Api.Data.Entities; 3 | using AspNetCoreTesting.Api.Services; 4 | using Bazinga.AspNetCore.Authentication.Basic; 5 | using FakeItEasy; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc.Testing; 8 | using Microsoft.AspNetCore.TestHost; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.EntityFrameworkCore.Storage; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Newtonsoft.Json.Linq; 13 | using System; 14 | using System.Net; 15 | using System.Net.Http.Headers; 16 | using System.Net.Http.Json; 17 | using System.Security.Claims; 18 | using System.Threading.Tasks; 19 | using Xunit; 20 | 21 | namespace AspNetCoreTesting.Api.Tests 22 | { 23 | public class UsersControllerTests 24 | { 25 | private const string Username = "Test"; 26 | private const string Password = "test"; 27 | private readonly string base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{Username}:{Password}")); 28 | private const string SqlConnectionString = "Server=localhost,14331;Database=AspNetCoreTesting;User Id=sa;Password=P@ssword123"; 29 | private INotificationService NotificationServiceFake = A.Fake(); 30 | 31 | [Fact] 32 | public async Task Get_returns_401_Unauthorized_if_not_authenticated() 33 | { 34 | var application = GetWebApplication(); 35 | 36 | var client = application.CreateClient(); 37 | 38 | var response = await client.GetAsync("/users"); 39 | 40 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 41 | } 42 | 43 | [Fact] 44 | public async Task Get_returns_all_users() 45 | { 46 | var application = GetWebApplication(); 47 | 48 | using (var services = application.Services.CreateScope()) 49 | { 50 | IDbContextTransaction? transaction = null; 51 | try 52 | { 53 | var ctx = services.ServiceProvider.GetRequiredService(); 54 | transaction = ctx.Database.BeginTransaction(); 55 | 56 | var conn = ctx.Database.GetDbConnection(); 57 | using (var cmd = conn.CreateCommand()) 58 | { 59 | cmd.Transaction = transaction.GetDbTransaction(); 60 | cmd.CommandText = "SET IDENTITY_INSERT Users ON; " + 61 | "INSERT INTO Users (Id, FirstName, LastName) VALUES" + 62 | "(1, 'John', 'Doe'), " + 63 | "(2, 'Jane', 'Doe'); " + 64 | "SET IDENTITY_INSERT Users OFF;"; 65 | await cmd.ExecuteNonQueryAsync(); 66 | } 67 | 68 | var client = application.CreateClient(); 69 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); 70 | 71 | var response = await client.GetAsync("/users"); 72 | 73 | dynamic users = JArray.Parse(await response.Content.ReadAsStringAsync()); 74 | 75 | Assert.Equal(2, users.Count); 76 | Assert.Equal("John", (string)users[0].firstName); 77 | Assert.Equal("Doe", (string)users[1].lastName); 78 | } 79 | finally 80 | { 81 | transaction?.Rollback(); 82 | } 83 | } 84 | } 85 | 86 | [Fact] 87 | public async Task Put_returns_Created_if_successful() 88 | { 89 | var application = GetWebApplication(); 90 | 91 | using (var services = application.Services.CreateScope()) 92 | { 93 | IDbContextTransaction? transaction = null; 94 | try 95 | { 96 | var ctx = services.ServiceProvider.GetRequiredService(); 97 | transaction = ctx.Database.BeginTransaction(); 98 | 99 | var client = application.CreateClient(); 100 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); 101 | 102 | var response = await client.PutAsJsonAsync("/users/", new { firstName = "John", lastName = "Doe" }); 103 | 104 | dynamic user = JObject.Parse(await response.Content.ReadAsStringAsync()); 105 | 106 | Assert.Equal("John", (string)user.firstName); 107 | Assert.Equal("Doe", (string)user.lastName); 108 | Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode); 109 | Assert.Matches("^http:\\/\\/localhost\\/users\\/\\d+$", response.Headers.Location!.AbsoluteUri.ToLower()); 110 | 111 | var userId = int.Parse(response.Headers.Location!.PathAndQuery.Substring(response.Headers.Location!.PathAndQuery.LastIndexOf("/") + 1)); 112 | 113 | var conn = ctx.Database.GetDbConnection(); 114 | using (var cmd = conn.CreateCommand()) 115 | { 116 | cmd.Transaction = transaction.GetDbTransaction(); 117 | cmd.CommandText = $"SELECT TOP 1 * FROM Users WHERE Id = {userId}"; 118 | using (var rs = await cmd.ExecuteReaderAsync()) 119 | { 120 | Assert.True(await rs.ReadAsync()); 121 | Assert.Equal("John", rs["FirstName"]); 122 | Assert.Equal("Doe", rs["LastName"]); 123 | } 124 | } 125 | } 126 | finally 127 | { 128 | transaction?.Rollback(); 129 | } 130 | } 131 | } 132 | 133 | [Fact] 134 | public async Task Put_returns_sends_notification_if_successful() 135 | { 136 | var application = GetWebApplication(); 137 | 138 | using (var services = application.Services.CreateScope()) 139 | { 140 | IDbContextTransaction? transaction = null; 141 | try 142 | { 143 | var ctx = services.ServiceProvider.GetRequiredService(); 144 | transaction = ctx.Database.BeginTransaction(); 145 | 146 | var client = application.CreateClient(); 147 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); 148 | 149 | var response = await client.PutAsJsonAsync("/users/", new { firstName = "John", lastName = "Doe" }); 150 | 151 | A.CallTo(() => 152 | NotificationServiceFake.SendUserCreatedNotification(A.That.Matches(x => x.FirstName == "John" && x.LastName == "Doe")) 153 | ).MustHaveHappened(); 154 | } 155 | finally 156 | { 157 | transaction?.Rollback(); 158 | } 159 | } 160 | } 161 | 162 | private WebApplicationFactory GetWebApplication() 163 | => new WebApplicationFactory().WithWebHostBuilder(builder => 164 | { 165 | builder.ConfigureTestServices(services => 166 | { 167 | var options = new DbContextOptionsBuilder() 168 | .UseSqlServer(SqlConnectionString) 169 | .Options; 170 | services.AddSingleton(options); 171 | services.AddSingleton(); 172 | services.AddSingleton(NotificationServiceFake); 173 | 174 | services.AddAuthentication() 175 | .AddBasicAuthentication(credentials => Task.FromResult(credentials.username == Username && credentials.password == Password)); 176 | 177 | services.AddAuthorization(config => 178 | { 179 | config.DefaultPolicy = new AuthorizationPolicyBuilder(config.DefaultPolicy) 180 | .AddAuthenticationSchemes(BasicAuthenticationDefaults.AuthenticationScheme) 181 | .Build(); 182 | }); 183 | }); 184 | }); 185 | } 186 | } -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/UsersControllerTestsWithTestBase.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data.Entities; 2 | using AspNetCoreTesting.Api.Tests.Infrastructure; 3 | using FakeItEasy; 4 | using Newtonsoft.Json.Linq; 5 | using System.Net; 6 | using System.Net.Http.Json; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace AspNetCoreTesting.Api.Tests 11 | { 12 | public class UsersControllerTestsWithTestBase : UserControllerTestBase 13 | { 14 | [Fact] 15 | public Task Get_returns_401_Unauthorized_if_not_authenticated() 16 | => RunTest( 17 | test: async client => 18 | { 19 | var response = await client.GetAsync("/users"); 20 | 21 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 22 | } 23 | , addAuth: false); 24 | 25 | [Fact] 26 | public Task Get_returns_all_users() 27 | => RunTest( 28 | populateDatabase: async cmd => 29 | { 30 | cmd.CommandText = "SET IDENTITY_INSERT Users ON; " + 31 | "INSERT INTO Users (Id, FirstName, LastName) VALUES" + 32 | "(1, 'John', 'Doe'), " + 33 | "(2, 'Jane', 'Doe'); " + 34 | "SET IDENTITY_INSERT Users OFF;"; 35 | await cmd.ExecuteNonQueryAsync(); 36 | }, 37 | test: async client => 38 | { 39 | var response = await client.GetAsync("/users"); 40 | 41 | dynamic users = JArray.Parse(await response.Content.ReadAsStringAsync()); 42 | 43 | Assert.Equal(2, users.Count); 44 | Assert.Equal("John", (string)users[0].firstName); 45 | Assert.Equal("Doe", (string)users[1].lastName); 46 | } 47 | ); 48 | 49 | [Fact] 50 | public Task Put_returns_Created_if_successful() 51 | { 52 | var userId = -1; 53 | 54 | return RunTest( 55 | test: async client => 56 | { 57 | var response = await client.PutAsJsonAsync("/users/", new { firstName = "John", lastName = "Doe" }); 58 | 59 | dynamic user = JObject.Parse(await response.Content.ReadAsStringAsync()); 60 | 61 | Assert.Equal("John", (string)user.firstName); 62 | Assert.Equal("Doe", (string)user.lastName); 63 | Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode); 64 | Assert.Matches("^http:\\/\\/localhost\\/users\\/\\d+$", response.Headers.Location!.AbsoluteUri.ToLower()); 65 | 66 | userId = int.Parse(response.Headers.Location!.PathAndQuery.Substring(response.Headers.Location!.PathAndQuery.LastIndexOf("/") + 1)); 67 | }, 68 | validateDatabase: async cmd => 69 | { 70 | cmd.CommandText = $"SELECT TOP 1 * FROM Users WHERE Id = {userId}"; 71 | using (var rs = await cmd.ExecuteReaderAsync()) 72 | { 73 | Assert.True(await rs.ReadAsync()); 74 | Assert.Equal("John", rs["FirstName"]); 75 | Assert.Equal("Doe", rs["LastName"]); 76 | } 77 | } 78 | ); 79 | } 80 | 81 | [Fact] 82 | public Task Put_returns_sends_notification_if_successful() 83 | => RunTest(async client => 84 | { 85 | var response = await client.PutAsJsonAsync("/users/", new { firstName = "John", lastName = "Doe" }); 86 | 87 | A.CallTo(() => 88 | NotificationServiceFake.SendUserCreatedNotification(A.That.Matches(x => x.FirstName == "John" && x.LastName == "Doe")) 89 | ).MustHaveHappened(); 90 | }); 91 | } 92 | } -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.Tests/UsersControllerTestsWithTestHelper.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data; 2 | using AspNetCoreTesting.Api.Data.Entities; 3 | using AspNetCoreTesting.Api.Services; 4 | using AspNetCoreTesting.Api.Tests.Extensions; 5 | using AspNetCoreTesting.Api.Tests.Infrastructure; 6 | using FakeItEasy; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Newtonsoft.Json.Linq; 9 | using System.Net; 10 | using System.Net.Http.Json; 11 | using System.Threading.Tasks; 12 | using Xunit; 13 | 14 | namespace AspNetCoreTesting.Api.Tests 15 | { 16 | public class UsersControllerTestsWithTestHelper 17 | { 18 | private const string SqlConnectionString = "Server=localhost,14331;Database=AspNetCoreTesting;User Id=sa;Password=P@ssword123"; 19 | private INotificationService NotificationServiceFake = A.Fake(); 20 | 21 | [Fact] 22 | public Task Get_returns_401_Unauthorized_if_not_authenticated() 23 | => GetTestRunner(false) 24 | .Run(async client => { 25 | var response = await client.GetAsync("/users"); 26 | 27 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 28 | }); 29 | 30 | [Fact] 31 | public Task Get_returns_all_users() 32 | => GetTestRunner() 33 | .PrepareDb(async cmd => { 34 | await cmd.AddUser(1, "John", "Doe"); 35 | await cmd.AddUser(2,"Jane", "Doe"); 36 | }) 37 | .Run(async client => { 38 | var response = await client.GetAsync("/users"); 39 | 40 | dynamic users = JArray.Parse(await response.Content.ReadAsStringAsync()); 41 | 42 | Assert.Equal(2, users.Count); 43 | Assert.Equal("John", (string)users[0].firstName); 44 | Assert.Equal("Doe", (string)users[1].lastName); 45 | }); 46 | 47 | [Fact] 48 | public Task Get_ID_returns_User_if_it_exists() 49 | => GetTestRunner() 50 | .PrepareDb(async cmd => { 51 | await cmd.AddUser(); 52 | }) 53 | .Run(async client => { 54 | var response = await client.GetAsync("/users/1"); 55 | 56 | dynamic user = JObject.Parse(await response.Content.ReadAsStringAsync()); 57 | 58 | Assert.Equal(1, (int)user.id); 59 | Assert.Equal("John", (string)user.firstName); 60 | Assert.Equal("Doe", (string)user.lastName); 61 | }); 62 | 63 | [Fact] 64 | public Task Get_ID_returns_404_if_user_id_does_not_exist() 65 | => GetTestRunner() 66 | .Run(async client => { 67 | var response = await client.GetAsync("/users/1"); 68 | 69 | Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode); 70 | }); 71 | 72 | [Fact] 73 | public Task Put_returns_BadRequest_if_missing_first_name() 74 | => GetTestRunner() 75 | .Run(async client => { 76 | var response = await client.PutAsJsonAsync("/users/", new { lastName = "Doe" }); 77 | 78 | Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode); 79 | }); 80 | 81 | [Fact] 82 | public Task Put_returns_BadRequest_if_missing_last_name() 83 | => GetTestRunner() 84 | .Run(async client => { 85 | var response = await client.PutAsJsonAsync("/users/", new { firstName = "John" }); 86 | 87 | Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode); 88 | }); 89 | 90 | [Fact] 91 | public Task Put_returns_Created_if_successful() 92 | { 93 | var userId = -1; 94 | 95 | return GetTestRunner() 96 | .ValidatePostTestDb(async cmd => { 97 | cmd.CommandText = $"SELECT TOP 1 * FROM Users WHERE Id = {userId}"; 98 | using (var rs = await cmd.ExecuteReaderAsync()) 99 | { 100 | Assert.True(await rs.ReadAsync()); 101 | Assert.Equal("John", rs["FirstName"]); 102 | Assert.Equal("Doe", rs["LastName"]); 103 | } 104 | }) 105 | .Run(async client => { 106 | var response = await client.PutAsJsonAsync("/users/", new { firstName = "John", lastName = "Doe" }); 107 | 108 | dynamic user = JObject.Parse(await response.Content.ReadAsStringAsync()); 109 | 110 | Assert.Equal("John", (string)user.firstName); 111 | Assert.Equal("Doe", (string)user.lastName); 112 | 113 | Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode); 114 | Assert.Matches("^http:\\/\\/localhost\\/users\\/\\d+$", response.Headers.Location!.AbsoluteUri.ToLower()); 115 | 116 | userId = int.Parse(response.Headers.Location!.PathAndQuery.Substring(response.Headers.Location!.PathAndQuery.LastIndexOf("/") + 1)); 117 | }); 118 | } 119 | 120 | [Fact] 121 | public Task Put_returns_sends_notification_if_successful() 122 | => GetTestRunner() 123 | .Run(async client => { 124 | await client.PutAsJsonAsync("/users/", new { firstName = "John", lastName = "Doe" }); 125 | A.CallTo(() => 126 | NotificationServiceFake.SendUserCreatedNotification(A.That.Matches(x => x.FirstName == "John" && x.LastName == "Doe")) 127 | ).MustHaveHappened(); 128 | }); 129 | 130 | private TestHelper GetTestRunner(bool addClientAuth = true) 131 | { 132 | var helper = new TestHelper() 133 | .AddDbContext(SqlConnectionString) 134 | .AddFakeAuth("Test", "test") 135 | .OverrideServices(services => { 136 | services.AddSingleton(NotificationServiceFake); 137 | }); 138 | 139 | if (addClientAuth) 140 | helper.AddFakeClientAuth("Test", "test"); 141 | 142 | return helper; 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /AspNetCoreTesting.Api.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31912.275 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreTesting.Api", "AspNetCoreTesting.Api\AspNetCoreTesting.Api.csproj", "{CB103A30-79A3-4FCE-8971-F223618BEDE3}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreTesting.Api.Tests", "AspNetCoreTesting.Api.Tests\AspNetCoreTesting.Api.Tests.csproj", "{50503364-8AE4-417C-9EB0-4F8269887A64}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {CB103A30-79A3-4FCE-8971-F223618BEDE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {CB103A30-79A3-4FCE-8971-F223618BEDE3}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {CB103A30-79A3-4FCE-8971-F223618BEDE3}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {CB103A30-79A3-4FCE-8971-F223618BEDE3}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {50503364-8AE4-417C-9EB0-4F8269887A64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {50503364-8AE4-417C-9EB0-4F8269887A64}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {50503364-8AE4-417C-9EB0-4F8269887A64}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {50503364-8AE4-417C-9EB0-4F8269887A64}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {47D6D7CE-0449-49FE-B90A-11B523EC2749} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/AspNetCoreTesting.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/AspNetCoreTesting.Api.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | MvcControllerEmptyScaffolder 5 | root/Common/MVC/Controller 6 | 7 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Controllers/SettingsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace AspNetCoreTesting.Api.Controllers 5 | { 6 | [Route("[controller]")] 7 | [ApiController] 8 | [AllowAnonymous] 9 | public class SettingsController : ControllerBase 10 | { 11 | private readonly IConfiguration _config; 12 | 13 | public SettingsController(IConfiguration config) 14 | { 15 | _config = config; 16 | } 17 | 18 | [HttpGet("mysetting")] 19 | public ActionResult GetSetting() 20 | { 21 | return Ok(_config["MySetting"]); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data.Entities; 2 | using AspNetCoreTesting.Api.Models; 3 | using AspNetCoreTesting.Api.Services; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace AspNetCoreTesting.Api.Controllers 7 | { 8 | [Route("[controller]")] 9 | [ApiController] 10 | public class UsersController : ControllerBase 11 | { 12 | private readonly IUsers _users; 13 | 14 | public UsersController(IUsers users) 15 | { 16 | _users = users; 17 | } 18 | 19 | [HttpGet()] 20 | public async Task> GetUsers() 21 | { 22 | return Ok(await _users.All()); 23 | } 24 | 25 | [HttpGet("{id}")] 26 | public async Task> GetUserById(int id) 27 | { 28 | var user = await _users.WithId(id); 29 | return user != null ? Ok(user) : NotFound(); 30 | } 31 | 32 | [HttpPut("")] 33 | public async Task> AddUser(AddUserModel model) 34 | { 35 | if (!ModelState.IsValid) 36 | { 37 | return BadRequest(ModelState); 38 | } 39 | var user = await _users.Add(model.FirstName!, model.LastName!); 40 | return CreatedAtAction("GetUserById", new { id = user.Id }, user); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Data/ApiContext.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace AspNetCoreTesting.Api.Data 5 | { 6 | public class ApiContext : DbContext 7 | { 8 | public ApiContext(DbContextOptions options) : base(options) 9 | { 10 | } 11 | 12 | protected override void OnModelCreating(ModelBuilder modelBuilder) 13 | { 14 | modelBuilder.Entity(x => { 15 | x.ToTable("Users"); 16 | }); 17 | } 18 | 19 | #nullable disable 20 | public DbSet Users { get; set; } 21 | #nullable enable 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Data/Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCoreTesting.Api.Data.Entities 2 | { 3 | public class User 4 | { 5 | public User() 6 | { 7 | 8 | } 9 | 10 | public static User Create(string firstName, string lastName) => 11 | new User { FirstName = firstName, LastName = lastName }; 12 | 13 | public int Id { get; private set; } 14 | public string FirstName { get; private set; } = string.Empty; 15 | public string LastName { get; private set; } = string.Empty; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Data/Migrations/DbMigration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | using AspNetCoreTesting.Api.Data; 4 | 5 | namespace AspNetCoreTesting.Api.Migrations 6 | { 7 | [Migration("DbMigration")] 8 | [DbContext(typeof(ApiContext))] 9 | public class DbMigration : Migration 10 | { 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "Users", 15 | columns: table => new 16 | { 17 | Id = table.Column(type: "int", nullable: false) 18 | .Annotation("SqlServer:Identity", "1, 1"), 19 | FirstName = table.Column(type: "nvarchar(max)", nullable: false), 20 | LastName = table.Column(type: "nvarchar(max)", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Users", x => x.Id); 25 | }); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable(name: "Users"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Models/AddUserModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace AspNetCoreTesting.Api.Models 4 | { 5 | public class AddUserModel 6 | { 7 | [Required] 8 | public string? FirstName { get; set; } 9 | [Required] 10 | public string? LastName { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data; 2 | using AspNetCoreTesting.Api.Services; 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc.Authorization; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | builder.Services.AddControllers(options => { 11 | options.Filters.Add(new AuthorizeFilter()); 12 | }); 13 | 14 | builder.Services.AddDbContext(x => { 15 | x.UseSqlServer(builder.Configuration.GetConnectionString("Sql")); 16 | }); 17 | builder.Services.AddScoped(); 18 | builder.Services.AddSingleton(); 19 | 20 | builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 21 | .AddJwtBearer(); 22 | 23 | builder.Services.AddAuthorization(config => 24 | { 25 | config.DefaultPolicy = new AuthorizationPolicyBuilder() 26 | .RequireAuthenticatedUser() 27 | .Build(); 28 | }); 29 | 30 | var app = builder.Build(); 31 | 32 | app.UseHttpsRedirection(); 33 | 34 | app.UseAuthorization(); 35 | 36 | app.MapControllers(); 37 | 38 | app.Run(); 39 | 40 | public partial class Program 41 | { 42 | 43 | } 44 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:31923", 8 | "sslPort": 44347 9 | } 10 | }, 11 | "profiles": { 12 | "AspNetCoreTesting.Api": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "weatherforecast", 17 | "applicationUrl": "https://localhost:7143;http://localhost:5143", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "weatherforecast", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Services/DummyNotificationService.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data.Entities; 2 | 3 | namespace AspNetCoreTesting.Api.Services 4 | { 5 | public class DummyNotificationService : INotificationService 6 | { 7 | public Task SendUserCreatedNotification(User user) 8 | { 9 | Console.WriteLine($"User {user.FirstName} {user.LastName} was added!"); 10 | return Task.CompletedTask; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Services/INotificationService.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data.Entities; 2 | 3 | namespace AspNetCoreTesting.Api.Services 4 | { 5 | public interface INotificationService 6 | { 7 | Task SendUserCreatedNotification(User user); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Services/IUsers.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data.Entities; 2 | 3 | namespace AspNetCoreTesting.Api.Services 4 | { 5 | public interface IUsers 6 | { 7 | Task All(); 8 | Task WithId(int id); 9 | Task Add(string firstName, string lastName); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/Services/Users.cs: -------------------------------------------------------------------------------- 1 | using AspNetCoreTesting.Api.Data; 2 | using AspNetCoreTesting.Api.Data.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace AspNetCoreTesting.Api.Services 6 | { 7 | public class Users : IUsers 8 | { 9 | private readonly ApiContext _context; 10 | private readonly INotificationService _notificationService; 11 | 12 | public Users(ApiContext context, INotificationService notificationService) 13 | { 14 | _context = context; 15 | _notificationService = notificationService; 16 | } 17 | 18 | public Task All() 19 | { 20 | return _context.Users.ToArrayAsync(); 21 | } 22 | 23 | public Task WithId(int id) 24 | { 25 | return _context.Users.FirstOrDefaultAsync(x => x.Id == id); 26 | } 27 | 28 | public async Task Add(string firstName, string lastName) 29 | { 30 | var user = User.Create(firstName, lastName); 31 | await _context.AddAsync(user); 32 | await _context.SaveChangesAsync(); 33 | await _notificationService.SendUserCreatedNotification(user); 34 | return user; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/appsettings.Test.json: -------------------------------------------------------------------------------- 1 | { 2 | "MySetting": "Test setting" 3 | } 4 | -------------------------------------------------------------------------------- /AspNetCoreTesting.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "Sql": "Server=.;Database=TestingDemo;User Id=sa;Password=P@ssword123" 11 | }, 12 | "MySetting": "Production setting" 13 | } 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Example of Integration Testing with ASP.NET Core including a database 2 | 3 | This repo contains code that demonstrates how you can do integration testing with ASP.NET Core applications that include authentication and databases. 4 | 5 | The code is explained in my blog post [Integration Testing with ASP.NET Core](https://www.fearofoblivion.com/asp-net-core-integration-testing), so if you are curious, I suggest having a look at that! 6 | 7 | ## Background 8 | 9 | Having a database often introduces some complexity when it comes to running integration tests, as the data in the database needs to be consistent. This is obviously something that might become a bit complicated when you have tests that add or delete data. Not to mention, that the data needs to contain all possible permutations needed to perform the required tests. Something that often leads to complications when new areas are tested, which requires changes to old tests, due to the data being updated to include the data required for the new tests. 10 | 11 | In this sample, the code uses a transaction around each test, allowing the database to be populated with required data before each test, and then rolled back to its original "empty" state when completed. This also allows you to validate the contents in the database as part of the test if needed. Without seeing data from other tests that are being run. 12 | 13 | __Caveat:__ There might be edge cases where the transactions cause deadlocks. However, so far, this has not been observed while using this way of working. 14 | 15 | The sample also includes some code to handle authentication when doing ASP.NET Core integration. In this particular example, the solution adds a separate Basic Auth scheme, and updates the default authorization policy to check the new scheme as well. This should work in a lot of cases. However, in some cases, a more complex solution might need to be implemented. 16 | 17 | ## Source code 18 | 19 | The source code consists of 2 applications, a Web API that is to be tested, and a test project using xUnit to run the tests. 20 | 21 | The test project contains 3 different versions of tests. The first and most simple, is the [UsersControllerTests](./AspNetCoreTesting.Api.Tests/UsersControllerTests.cs) class. This class does all set up inside a helper method in the test class. 22 | 23 | However, as this becomes tedious and annoying if there are many test classes, there is a cleaner version called [UsersControllerTestsWithTestBase](./AspNetCoreTesting.Api.Tests/UsersControllerTestsWithTestBase.cs), which uses 2 base classes to perform the set-up. This version also uses a more functional syntax, allowing each test method to be a bit cleaner, while still able to perform everything it needs. 24 | 25 | Finally, there one version called [UsersControllerTestsWithTestHelper](AspNetCoreTesting.Api.Tests/UsersControllerTestsWithTestHelper.cs) that uses a helper class to do the set-up. The helper class is using a fluid syntax that gives the test methods an easy way to set up and run the tests as needed. This implementation also enables the use of extension methods to add custom functionality. 26 | 27 | __Note:__ The [TestHelper](AspNetCoreTesting.Api.Tests/Infrastructure/TestHelper.cs) is a quick simple implementation that probably needs a bit more work. But it is there for you to see. 28 | 29 | ## Running the tests 30 | 31 | The tests are dependent on a database (obviously). The easiest way to set this up is to run the following command, and start a Docker container 32 | 33 | ```bash 34 | > docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=P@ssword123" \ 35 | -p 14331:1433 --name sql_test --hostname sql_test \ 36 | -d mcr.microsoft.com/mssql/server:2019-latest 37 | ``` 38 | 39 | This creates a new SQL Server Docker container that exposes the server on port 14331 instead of the default 1433. This way, it won't interfere with any existing SQL Server on your machine. 40 | 41 | Before each test run, EF Core is used to ensure the database is created, and then the applications migrations are applied to the database. This is done in the [TestRunStart](AspNetCoreTesting.Api.Tests/TestRunStart.cs) class, which uses the Xunit.TestFramework attribute to get it to run during each test run. 42 | 43 | ## Feedback 44 | 45 | I am always interested in feedback, so if you have any, feel free to reach out on Twitter. I'm available at [@ZeroKoll](https://twitter.com/ZeroKoll). 46 | 47 | When it comes to this code, I'm curious about your preference when it comes to using a base class or a helper. I'm torn myself, so feedback would be very much appreciated! --------------------------------------------------------------------------------