Asp.Net Core Web Api Integration testing using EntityFrameworkCore LocalDb and XUnit2
Compared to unit testing, Integration testing gives us a better return on test investment.
Nowadays, we have a lot of tools to set it up easily, and it can go faster and gives us more confidence with code we have changing knowing we have not breaking existing functionalities.
It can help us to catch regression errors, before we deploy to a test environment, and fewer bugs in new features, resulting Higher velocity and higher quality solution
Unit testing test behaviour of a class and drive its implementation, run in memory and mocks the Data Access Laywer while Integration testing test the entire system and focus on interactions between many classes, uses the real database and test the Data Access Laywer and its value is to catch regressions. So Integration testing may catch errors that passes on unit testing
In my previous tutorial , Asp.Net Core Web Api Integration testing using InMemory EntityFrameworkCore Sqlite and XUnit2 I used SqlLite InMemory
Note : InMemory is not a relational database. It is designed to be a general purpose database for testing, and is not designed to mimic a relational database. So it will allow us to save data that would violate referential integrity constraints in a relational database.
In this tutorial, I will talk about Asp.Net Web Api Core Integration testing using EntityFrameworkCore LocalDB and Xunit2
So lets go ahead and implement some integration tests for the solution discussed on this tutorial Token Based Authentication using Asp.net Web Api Core
Lets start and create a XUnit Test Project using Visual Studio 2017
Add reference to the project TokenAuthWebApiCore.Server that represent our system under test
INTEGRATION TESTING USING LOCAL DB
Lets install some dependencies
Lets create a Generic TestFixture class, here we are going to create a Test Server to target our API Server http://localhost:58834
So we build a host by creating an instance of WebHostBuilder. And our tests classes will inherits from IClassFixture<TestFixture<OurTestStartup>>
public class TestFixture<TStartup> : IDisposable where TStartup : class { private readonly TestServer _testServer; public HttpClient HttpClient { get; } public TestFixture() { var webHostBuilder = new WebHostBuilder().UseStartup<TStartup>(); _testServer = new TestServer(webHostBuilder); HttpClient = _testServer.CreateClient(); HttpClient.BaseAddress = new Uri("http://localhost:58834"); } public void Dispose() { HttpClient.Dispose(); _testServer.Dispose(); } }
Next, open Startup.cs file of project TokenAuthWebApiCore.Server and add 2 virtual methods SetUpDataBase and EnsureDatabaseCreated. So this 2 methods will be overriden in our test StartUp class to use a test database like SqlServer Local DB or any other database system
public virtual void SetUpDataBase(IServiceCollection services) { services.AddDbContext<SecurityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SecurityConnection"), sqlOptions => sqlOptions.MigrationsAssembly("TokenAuthWebApiCore.Server"))); } public virtual void EnsureDatabaseCreated(SecurityContext dbContext) { // run Migrations dbContext.Database.Migrate(); }
Here we are using LocalDB, so lets create a TestStartupLocalDb class that inherits from Startup
Next , overrides SetUpDataBase and Configure LocalDB, so we will use LocalDB instead of the production database
public override void SetUpDataBase(IServiceCollection services) { var connectionStringBuilder = new SqlConnectionStringBuilder { DataSource = @"(LocalDB)\MSSQLLocalDB", InitialCatalog = "VS2017Db_TokenAuthWebApiCore.Server.Local", IntegratedSecurity = true, }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqlConnection(connectionString); services .AddEntityFrameworkSqlServer() .AddDbContext<SecurityContext>( options => options.UseSqlServer(connection, sqlOptions => sqlOptions.MigrationsAssembly("TokenAuthWebApiCore.Server")) ); }
Next , overrides EnsureDatabaseCreated to ensures that the database for the context exists. Here I will destroy and recreate the database
public override void EnsureDatabaseCreated(SecurityContext dbContext) { DestroyDatabase(); CreateDatabase(); }
Here is complete code of TestStartupLocalDb class
public class TestStartupLocalDb : Startup, IDisposable { public TestStartupLocalDb(IHostingEnvironment env) : base(env) { } public override void SetUpDataBase(IServiceCollection services) { var connectionStringBuilder = new SqlConnectionStringBuilder { DataSource = @"(LocalDB)\MSSQLLocalDB", InitialCatalog = "VS2017Db_TokenAuthWebApiCore.Server.Local", IntegratedSecurity = true, }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqlConnection(connectionString); services .AddEntityFrameworkSqlServer() .AddDbContext<SecurityContext>( options => options.UseSqlServer(connection, sqlOptions => sqlOptions.MigrationsAssembly("TokenAuthWebApiCore.Server")) ); } public override void EnsureDatabaseCreated(SecurityContext dbContext) { DestroyDatabase(); CreateDatabase(); } public void Dispose() { DestroyDatabase(); } private static void CreateDatabase() { ExecuteSqlCommand(Master, $@" IF(db_id(N'VS2017Db_TokenAuthWebApiCore.Server.Local') IS NULL) BEGIN CREATE DATABASE [VS2017Db_TokenAuthWebApiCore.Server.Local] ON (NAME = 'VS2017Db_TokenAuthWebApiCore.Server.Local', FILENAME = '{Filename}') END"); var connectionStringBuilder = new SqlConnectionStringBuilder { DataSource = @"(LocalDB)\MSSQLLocalDB", InitialCatalog = "VS2017Db_TokenAuthWebApiCore.Server.Local", IntegratedSecurity = true, }; var connectionString = connectionStringBuilder.ToString(); var optionsBuilder = new DbContextOptionsBuilder<SecurityContext>(); optionsBuilder.UseSqlServer(connectionString); using (var context = new SecurityContext(optionsBuilder.Options)) { context.Database.Migrate(); context.SaveChanges(); } } private static void DestroyDatabase() { var fileNames = ExecuteSqlQuery(Master, @" SELECT [physical_name] FROM [sys].[master_files] WHERE [database_id] = DB_ID('VS2017Db_TokenAuthWebApiCore.Server.Local')", row => (string)row["physical_name"]); if (fileNames.Any()) { ExecuteSqlCommand(Master, @" ALTER DATABASE [VS2017Db_TokenAuthWebApiCore.Server.Local] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; EXEC sp_detach_db 'VS2017Db_TokenAuthWebApiCore.Server.Local', 'true'"); fileNames.ForEach(File.Delete); } if (File.Exists(Filename)) { File.Delete(Filename); } if (File.Exists(LogFilename)) { File.Delete(LogFilename); } } private static void ExecuteSqlCommand( SqlConnectionStringBuilder connectionStringBuilder, string commandText) { using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString)) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = commandText; command.ExecuteNonQuery(); } } } private static List<T> ExecuteSqlQuery<T>( SqlConnectionStringBuilder connectionStringBuilder, string queryText, Func<SqlDataReader, T> read) { var result = new List<T>(); using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString)) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = queryText; using (var reader = command.ExecuteReader()) { while (reader.Read()) { result.Add(read(reader)); } } } } return result; } private static SqlConnectionStringBuilder Master => new SqlConnectionStringBuilder { DataSource = @"(LocalDB)\MSSQLLocalDB", InitialCatalog = "master", IntegratedSecurity = true }; private static string Filename => Path.Combine( Path.GetDirectoryName( typeof(TestStartupLocalDb).GetTypeInfo().Assembly.Location), "VS2017Db_TokenAuthWebApiCore.Server.Local.mdf"); private static string LogFilename => Path.Combine( Path.GetDirectoryName( typeof(TestStartupLocalDb).GetTypeInfo().Assembly.Location), "VS2017Db_TokenAuthWebApiCore.Server.Local_log.ldf"); }
Next, create our first Test class AuthControllerRegisterUserTest and inherits it from IClassFixture<TestFixture<TestStartupLocalDb >>
public class AuthControllerRegisterUserTest : IClassFixture<TestFixture<TestStartupLocalDb>> { private HttpClient Client { get; } public AuthControllerRegisterUserTest(TestFixture<TestStartupLocalDb> fixture) { Client = fixture.HttpClient; } [Theory] [InlineData("", "", "")] [InlineData("", "WebApiCore1#", "WebApiCore1#")] [InlineData("", "", "WebApiCore1#")] [InlineData("", "WebApiCore1#", "")] [InlineData("simpleuser@yopmail.com", "WebApiChggore1#", "WebApiCore1#")] [InlineData("simpleuser", "WebApiCore1#", "WebApiCore1#")] public async Task WhenNoRegisteredUser_SignUpWithModelError_ReturnBadRequest(string email, string passWord, string confirmPassword) { // Arrange var obj = new RegisterViewModel { Email = email, Password = passWord, ConfirmPassword = confirmPassword }; string stringData = JsonConvert.SerializeObject(obj); var contentData = new StringContent(stringData, Encoding.UTF8, "application/json"); // Act var response = await Client.PostAsync($"/api/auth/register", contentData); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task WhenNoRegisteredUser_SignUp_WithValidModelState_Return_OK() { // Arrange var obj = new RegisterViewModel { Email = "simpleuser@yopmail.com", Password = "WebApiCore1#", ConfirmPassword = "WebApiCore1#" }; string stringData = JsonConvert.SerializeObject(obj); var contentData = new StringContent(stringData, Encoding.UTF8, "application/json"); // Act var response = await Client.PostAsync($"/api/auth/register", contentData); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } }
Next, lets order tests methods , because to be able to get a existing user, we must register it first. So create a PriorityOrderer class that implement ITestCaseOrderer and TestPriorityAttribute class and implement them as follow
Next, create our first Test class AuthControllerTokenTestand inherits it from IClassFixture<TestFixture<TestStartupLocalDb >> and implement it
Decorate test methods with TestPriority(1) and TestPriority(2). So the method decorated with TestPriority(2) will be executed after the method decorated with TestPriority(1) that is UserRegistration Test
[TestCaseOrderer("TokenAuthWebApiCore.Server.IntegrationTest.Setup.PriorityOrderer", "TokenAuthWebApiCore.Server.IntegrationTest")] public class AuthControllerTokenTest : IClassFixture<TestFixture<TestStartupLocalDb>> { private HttpClient Client { get; } public AuthControllerTokenTest(TestFixture<TestStartupLocalDb> fixture) { Client = fixture.HttpClient; } [Fact(DisplayName = "WhenNoRegisteredUser_SignUpForToken_WithValidModelState_Return_OK"), TestPriority(1)] public async Task WhenNoRegisteredUser_SignUpForToken_WithValidModelState_Return_OK() { // Arrange var obj = new RegisterViewModel { Email = "simpleuser@yopmail.com", Password = "WebApiCore1#", ConfirmPassword = "WebApiCore1#" }; string stringData = JsonConvert.SerializeObject(obj); var contentData = new StringContent(stringData, Encoding.UTF8, "application/json"); // Act var response = await Client.PostAsync($"/api/auth/register", contentData); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact(DisplayName = "WhenRegisteredUser_SignIn_WithValidModelState_Return_ValidToken"), TestPriority(2)] public async Task WhenRegisteredUser_SignIn_WithValidModelState_Return_ValidToken() { // Arrange var obj = new LoginViewModel { Email = "simpleuser@yopmail.com", Password = "WebApiCore1#" }; string stringData = JsonConvert.SerializeObject(obj); var contentData = new StringContent(stringData, Encoding.UTF8, "application/json"); // Act var response = await Client.PostAsync($"/api/auth/token", contentData); response.EnsureSuccessStatusCode(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var jwToken = JsonConvert.DeserializeObject<JwToken>( await response.Content.ReadAsStringAsync()); Assert.True(jwToken.Expiration > DateTime.UtcNow); Assert.True(jwToken.Token.Split('.').Length == 3); } }
Create an AssemblyInfo.cs file and add the line [assembly: CollectionBehavior(DisableTestParallelization = true)] to disable parallelism execution.
using System.Reflection; using System.Runtime.InteropServices; using Xunit; [assembly: AssemblyCopyright("Copyright © 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] [assembly: Guid("c5d63a13-e363-43eb-a685-09b81e5a2613")] [assembly: CollectionBehavior(DisableTestParallelization = true)]
Thank you for reading
sample code is available here Asp.Net-Web-Api-Core-Integration-testing-using-EntityFrameworkCore-LocalDB-and-Xunit2