This is a sample project demonstrating integration tests that include the Data Access Layer (DAL). This is using .Net Entity Framework version 6 and Microsoft’s MSTEST. See other blog posts regarding integration testing and unit testing here:
http://solidfish.com/unit-testing-vs-integration-testing/
http://solidfish.com/unit-testing-with-dependency-injection/
This example uses database migrations as part of the tests. It drops and recreates the database before running each test case. A TestSetup.cs class is created for this database setup:
TestSetup.cs public static class TestSetup { public static void CreateDatabase() { // Create database ExecuteSqlCommand(Master, $@" CREATE DATABASE [Widgets] ON (NAME = 'Widgets', FILENAME = '{Filename}')"); // Run the database migration var migration = new MigrateDatabaseToLatestVersion<WidgetsContext, WidgetsConfiguration>(); migration.InitializeDatabase(new WidgetsContext()); } public static void DestroyDatabase() { var fileNames = ExecuteSqlQuery(Master, @" SELECT [physical_name] FROM [sys].[master_files] WHERE [database_id] = DB_ID('Widgets')", row => (string)row["physical_name"]); if (fileNames.Any()) { ExecuteSqlCommand(Master, @" ALTER DATABASE [Widgets] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; EXEC sp_detach_db 'Widgets'"); fileNames.ForEach(File.Delete); } } ...
Using MSTEST’s initializer and cleanup attributes, the test class calls the TestSetup before and after running each test method.
UserServiceTest.cs [TestClass] public class UserServiceTests { [TestInitialize] public void Start() { TestSetup.DestroyDatabase(); TestSetup.CreateDatabase(); } [TestCleanup] public void End() { TestSetup.DestroyDatabase(); } [TestMethod] public void Create_ValidUser_Success() { // Arrange var configuration = new WidgetsMappingConfiguration(); var context = new DataContext("Widgets", configuration); var repository = new Repository(context); var userService = new UserService(repository); var email = "mockuser@widgets.com"; // Act User user = userService.CreateUser(email); context.SaveChanges(); // Assert Assert.IsTrue(user.UserId != 0); Assert.IsTrue(user.Email == email); } [TestMethod] public void Create_InvalidUser_Fail() { // Arrange var configuration = new WidgetsMappingConfiguration(); var context = new DataContext("Widgets", configuration); var repository = new Repository(context); var userService = new UserService(repository); var email = string.Empty; // Act User user = userService.CreateUser(email); context.SaveChanges(); // Assert Assert.IsNull(user); } ...
The database migration scripts are setup with the following PM console commands:
PM> Add-Migration Migration Scaffolding migration 'Migration'. The Designer Code for this migration file includes a snapshot of your current Code First model. This snapshot is used to calculate the changes to your model when you scaffold the next migration. If you make additional changes to your model that you want to include in this migration, then you can re-scaffold it by running 'Add-Migration Migration' again.
Before updating our database, we need to make sure we’re using the correct connection string. By default, data migration looks in the startup project for a connection string. Since our startup project is empty, we need to specify where it can find that connection string. I have it defined in the app.config of the Widgets.Data project and can reference it using the System.Configuration.ConfigurationManager.This can be seen on the WidgetsContext class:
public WidgetsContext() : base(ConnectionString) { } private static string ConnectionString { get { return System.Configuration.ConfigurationManager.ConnectionStrings["Widgets"].ConnectionString; } }
With the correct connection string configured we can run the database migration command.
PM> Update-Database -Verbose Using StartUp project 'Widgets'. Using NuGet project 'Widgets.Data'. Specify the '-Verbose' flag to view the SQL statements being applied to the target database. Target database is: 'Widgets' (DataSource: (LocalDB)\MSSQLLocalDb, Provider: System.Data.SqlClient, Origin: UserCode). Applying explicit migrations: [201801042306331_Migration]. Applying explicit migration: 201801042306331_Migration. CREATE TABLE [dbo].[Cart] ( [CartId] [int] NOT NULL IDENTITY, [UserId] [int] NOT NULL, [CreatedAt] [datetime] NOT NULL, CONSTRAINT [PK_dbo.Cart] PRIMARY KEY ([CartId]) ) CREATE INDEX [IX_UserId] ON [dbo].[Cart]([UserId]) CREATE TABLE [dbo].[Widget] ( [WidgetId] [int] NOT NULL IDENTITY, [Description] [nvarchar](100) NOT NULL, [Price] [decimal](18, 2) NOT NULL, [Cart_CartId] [int], CONSTRAINT [PK_dbo.Widget] PRIMARY KEY ([WidgetId]) ) CREATE INDEX [IX_Cart_CartId] ON [dbo].[Widget]([Cart_CartId]) CREATE TABLE [dbo].[User] ( [UserId] [int] NOT NULL IDENTITY, [Email] [nvarchar](100) NOT NULL, CONSTRAINT [PK_dbo.User] PRIMARY KEY ([UserId]) ) CREATE UNIQUE INDEX [IX_U_Email] ON [dbo].[User]([Email]) ALTER TABLE [dbo].[Cart] ADD CONSTRAINT [FK_dbo.Cart_dbo.User_UserId] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([UserId]) ON DELETE CASCADE ALTER TABLE [dbo].[Widget] ADD CONSTRAINT [FK_dbo.Widget_dbo.Cart_Cart_CartId] FOREIGN KEY ([Cart_CartId]) REFERENCES [dbo].[Cart] ([CartId]) CREATE TABLE [dbo].[__MigrationHistory] ( [MigrationId] [nvarchar](150) NOT NULL, [ContextKey] [nvarchar](300) NOT NULL, [Model] [varbinary](max) NOT NULL, [ProductVersion] [nvarchar](32) NOT NULL, CONSTRAINT [PK_dbo.__MigrationHistory] PRIMARY KEY ([MigrationId], [ContextKey]) ) INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion]) VALUES (N'201801042306331_Migration', N'Widgets.Data.Migrations.WidgetsConfiguration', 0x1F8B080000000...000 , N'6.2.0-61023') Running Seed method. PM>
More details here
https://msdn.microsoft.com/en-us/library/jj591621(v=vs.113).aspx
Unit Testing with Highway.Data Framework
Explored a new abstraction framework called Highway.Data which sits on top of an ORM (an abstraction framework that sits on top of an abstraction framework… 🙂 ). It is an open source project created by a group called hwyfwk. They have a few other tools to help with testing and scaffolding other common design patterns into a .Net project. Highway.Data supports Entity Framework 6 as well as other ORMs like nHibernate.
http://hwyfwk.com/projects/data/
Highway.Data simplifies the EF context model by creating a single abstracted Repository.
Git Repository
https://github.com/johnlee/Widgets.IntegrationTests
Reference Links
WebAPI 5 using XUNIT
https://www.strathweb.com/2012/06/asp-net-web-api-integration-testing-with-in-memory-hosting/
http://dotnetliberty.com/index.php/2015/12/17/asp-net-5-web-api-integration-testing/
http://xunit.github.io/docs/getting-started-desktop#run-tests-visualstudio
http://www.almguide.com/2015/06/running-xunit-tests-in-tfs-build-vnext/
WebAPI Integration Tests using NUNIT
https://stackoverflow.com/questions/29966982/integration-tests-for-webapi-and-transactions
Highway Framework
http://hwyfwk.com/projects/data/start.html
https://github.com/HighwayFramework/Highway.Data
Pluralsight Course
Integration Testing of Entity Framework Applications (uses Highway Framework with NUNIT)
Michael Perry
https://app.pluralsight.com/library/courses/entity-framework-applications-integration-testing