Perhaps you’ve just read my previous post Database to full asp.net Mvc app in 5 mins (or perhaps not) but in any case you find yourself thinking, “..thats all very well but what about testability? I want to know that my approach is testable, and that it supports unit tests, and mocking, and repositories and the like, otherwise the QA team is going to be ALL over me..”
Well, lets see how you can go from an existing database to a unit-test-enabled, asp.net webapi/mvc based app with mockable repositories etc, in just 15 minutes.
[You will need Visual Studio (2012+) and the CodeTrigger For Visual Studio extension (Full Trial, or Professional Version, v4.8.6.7+), Windows 7.1+, .Net Framework 4.6.1+. Once you got that setup, make a note of the time and begin]
Step 1. Start a new CodeTrigger project within Visual Studio, give it a name TestableWebApiMvc, and select the ‘DB to Multi-tier Mvc ..’ wizard. Make sure you click the sub-option ‘Enable Test’, new in 4.8.6.7. This small, innocuous option heralds significant changes in the generated code, specifically support for mockable repositories etc. Click next.
Step 2. Select your data source type, and configure the connection settings. Click ‘Connect’ to verify the settings, and click ‘Create’. Here we are using SQL Server, just to vary things since in the last post we used Oracle (and next post we might use MYSQL). We will be using the Northwind test database, feel free to use any other, but you will need to edit your code when we refer to entities in the northwind database such as ‘Categories’ or ‘Products’.
Step 3. Voila, the multi-tier Visual Studio project is generated and pre-built. (If you don’t have nuget automatic package download enabled, you may have missing package build errors at this stage, in this case, enable nuget automatic package download on your solution, or otherwise download the missing asp.net core packages from nuget, and rebuild. If you do have nuget automatic package download enabled then this is all done for you).
Note that this wizard will attempt to automatically create the identity tables required by Asp.Net identity, in your database. If those tables already exist then they will not be regenerated.
In the ‘Schema Objects’ tab, select any tables you want to be represented in the final application. For this illustration we use the the Categories and the Products table.
Step 4. Click on the ‘Business Objects’ tab. The business model is generated from your previous selections and all related business model entities are displayed for your selection, along with their relationships. Select ‘BOCategories’ and ‘BOProducts ‘, as the objects we will be working with in our tests.
Step 5. Shoot! Click the red button to generate the selected code classes. Stand back and be happy as CodeTrigger spews out several thousand lines of required code in a matter of seconds! At this point the project/solution will build itself and report happy. If there’s any outstanding tasks, they might be reported a the ‘Things to do’ tab that sometimes pops up, but I find that useful only when I’ve changed some of the default automatic code generation settings (like automatic sql scripting or automatic project file update).
Step 6. Hit F5 to build and run. Enjoy the sight of your fully functioning Asp.Net Mvc App. Login using an auto generated login for debug purposes.
Check the time, 5 mins! So far so good. We have a fully functioning basic app, albeit auto-generated. Now we have 10 mins left to setup the unit testing framework.
Step 7. Add a Unit Test project to the solution
Add references to the business, data and mvc projects to the unit test project.
Add the System.Net.Http, and System.Web.Http references to the unit project.
Its important to get the right references for System.Web.Http and System.Net.Http.
On my system System.Web.Http is the one in the packages folder {project }\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45 while the System.Net.Http is from C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1
Step 8. Create the mock repositories for testing your code without involving the database. Add a folder called MockRepositories to the Unit Test project and add the following classes to it.
MockRepositoryBase.cs
using System;
using System.Collections;
using System.Collections.Generic;
using TestableWebApiMvc.Data.Interfaces;
namespace UnitTestProject1.MockRepositories
{
public class MockRepositoryBase<T>
{
public virtual T New() { return default(T); }
public virtual IList<T> SelectAll() { return null; }
public virtual Int32 SelectAllCount() { return 0; }
public virtual IDictionary<string, IList<object>> SelectAllByCriteriaProjection(IList<IDataProjection> dataProjection, IList<IDataCriterion> listCriterion, IList<IDataOrderBy> listOrder, IDataSkip dataSkip, IDataTake dataTake) { return null; }
public virtual IList<T> SelectAllByCriteria(IList<IDataCriterion> listCriterion, IList<IDataOrderBy> listOrder, IDataSkip dataSkip, IDataTake dataTake) { return null; }
public virtual Int32 SelectAllByCriteriaCount(IList<IDataCriterion> listCriterion) { return 0; }
public virtual INorthwindDb_BaseData BaseData(T tbase) { return null; }
public INorthwindDb_TxConnectionProvider ConnectionProvider { get { return null; } set { } }
public Int32 TransactionCount { get; set; }
}
}
MockCategoriesRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using TestableWebApiMvc.Data;
using TestableWebApiMvc.Data.Interfaces;
using TestableWebApiMvc.Business.Repository.Interfaces;
namespace UnitTestProject1.MockRepositories
{
public class MockCategoriesRepository : MockRepositoryBase<IDAOCategories>, ICategoriesRepository
{
private IList<IDAOCategories> _categoriesList;
public MockCategoriesRepository(IList<IDAOCategories> categoriesList)
{ _categoriesList = categoriesList; }
public void Insert(IDAOCategories daoCategories)
{ _categoriesList.Add(daoCategories); }
public void Update(IDAOCategories daoCategories)
{
var category = SelectOne(daoCategories.CategoryID);
category.CategoryName = daoCategories.CategoryName;
category.Description = daoCategories.Description;
category.Picture = daoCategories.Picture;
}
public void Delete(IDAOCategories daoCategories)
{ _categoriesList.Remove(SelectOne(daoCategories.CategoryID)); }
public IDAOCategories SelectOne(Int32? categoryID)
{ return _categoriesList.Single((x) => x.CategoryID == categoryID); }
public override IList<IDAOCategories> SelectAllByCriteria(IList<IDataCriterion> listCriterion,
IList<IDataOrderBy> listOrder, IDataSkip dataSkip, IDataTake dataTake)
{
//handle search by id
if (((DataCriterionEq)listCriterion[0]).PropertyName == "CategoryID")
{
int categoryID = (int)((DataCriterionEq)listCriterion[0]).PropertyValue;
return _categoriesList.Where((x) => x.CategoryID == categoryID).ToList();
}
return null;
}
}
}
Step 9. Write a couple of basic Unit tests. Expand the UnitTest1 class with your unit tests, here are a couple of tests, GetCategory_ShouldFindCategory , GetCategory_ShouldNotFindCategory
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web.Http.Results;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitTestProject1.MockRepositories;
using TestableWebApiMvc.Data;
using TestableWebApiMvc.Data.Interfaces;
using TestableWebApiMvc.Mvc.SampleApiControllers;
using TestableWebApiMvc.Mvc.SampleViewModels;
using TestableWebApiMvc.Business.Repository;
using TestableWebApiMvc.Business.Repository.Interfaces;
using TestableWebApiMvc.Business;
namespace UnitTestProject1
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public async Task GetCategory_ShouldFindCategory()
{
CategoriesController catController = new CategoriesController();
catController.CategoriesRepository = new MockCategoriesRepository(new List<IDAOCategories> {
new DAOCategories() {CategoryID = 1, CategoryName = "Category One", Description = "First Category"},
new DAOCategories() {CategoryID = 2, CategoryName = "Category Two", Description = "Second Category"},
});
var result = await catController.GetCategories(1) as OkNegotiatedContentResult<CategoriesVm>;
Assert.IsNotNull(result);
Assert.AreEqual("Category One", result.Content.CategoryName);
}
[TestMethod]
public async Task GetCategory_ShouldNotFindCategory()
{
CategoriesController catController = new CategoriesController();
catController.CategoriesRepository = new MockCategoriesRepository(new List<IDAOCategories>{
new DAOCategories() {CategoryID = 1, CategoryName = "Category One", Description = "First Category"},
new DAOCategories() {CategoryID = 2, CategoryName = "Category Two", Description = "Second Category"},
});
var result = await catController.GetCategories(5) as NotFoundResult;
Assert.IsNotNull(result);
}
}
}
Running these tests gives you the following results, ie expected pass, and expected (and handled) fail.
Step 10. Add some Transaction tests. These tests will test that a couple of operations are submitted successfully as a single transaction, and if an error occurs, then all the operations are rolled back as a single transaction. To run these tests, we will be connecting to the database so we will be using a concrete repository rather than the mocked repository. First thing to do is to add an App.Config file to the Unit Test Project, with the appropriate connection string, you can copy this from the generated Web.config file in the Mvc project.
UnitTestProject1 – App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="CONNECTION-GUID-FROM-MVC-WEBCONFIG" value="Data Source=MYSERVER;Initial Catalog=Northwind; Connect Timeout=120;Integrated Security=SSPI;Persist Security Info=False;Packet Size=4096" />
</appSettings>
</configuration>
Next, Create a representative webapi controller action with a transactional operation we want to test. In the Mvc project, SampleApiControllers folder, add a class called CategoryProductsController.cs, with the following code:
CategoryProductsController.cs
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Description;
using System.Threading.Tasks;
using TestableWebApiMvc.Mvc.SampleViewModels;
using TestableWebApiMvc.Business.Interfaces;
using TestableWebApiMvc.Business.Repository;
using TestableWebApiMvc.Business.Repository.Interfaces;
namespace TestableWebApiMvc.Mvc.SampleApiControllers
{
public class CategoryProductsController : ApiController
{
/*get the relevant repositories from the repository factory*/
private static RF _rf = RF.New();
private IProductsRepository _productsRepo = _rf.ProductsRepository;
private ICategoriesRepository _categoriesRepo = _rf.CategoriesRepository;
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutCategoryProduct(CategoriesVm vm1, ProductsVm vm2)
{
if (!ModelState.IsValid) { return BadRequest(ModelState); }
var result = Task.Factory.StartNew(() =>
{
IUnitOfWork uow = new UnitOfWorkImp(new IRepositoryConnection[] { _categoriesRepo, _productsRepo });
/*get the domain objects from the viewmodel, and queue them up for transaction*/
var boCategory = vm1.BOCategories(_categoriesRepo);
uow.Update(boCategory);
var boProduct = vm2.BOProducts(_productsRepo);
uow.Update(boProduct);
string err;
/*commit transaction if all ok, rollback if not, and report error*/
if (!uow.Commit(out err))
{
var resp = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(err) };
throw new HttpResponseException(resp);
}
return true;
});
await result;
if (!result.Result)
return NotFound();
return StatusCode(HttpStatusCode.NoContent);
}
}
}
Next, expand the UnitTest1.cs class by adding the two new tests, plus a couple of support functions:
[TestMethod]
public async Task UpdateCategoryProduct_ShouldCommitTransaction()
{
/**********************set up the test******************/
RF _rf = RF.New();
ICategoriesRepository categoriesRepository = _rf.CategoriesRepository;
IProductsRepository productsRepository = _rf.ProductsRepository;
//create a test category and product
BOCategories testCategory = CreateTestCategory(categoriesRepository);
BOProducts testProduct = CreateTestProduct(productsRepository, (int)testCategory.CategoryID);
int testCategoryID = (int)testCategory.CategoryID;
int testProductID = (int)testProduct.ProductID;
/***********************end of test setup*****************/
//run the test, update the category name, and product name
testCategory.CategoryName = "Updated";
testProduct.ProductName = "Updated";
CategoryProductsController categoryProductsController = new CategoryProductsController();
var result = await categoryProductsController.PutCategoryProduct(new CategoriesVm(testCategory), new ProductsVm(testProduct));
//check the category name was updated in repository
BOCategories checkCategory = new BOCategories() { Repository = categoriesRepository };
checkCategory.Init(testCategoryID);
StringAssert.Equals(checkCategory.CategoryName, "Updated");
//check the product name was updated in repository
BOProducts checkProduct = new BOProducts() { Repository = productsRepository };
checkProduct.Init(testProductID);
StringAssert.Equals(checkProduct.ProductName, "Updated");
/**********tear down the test******************************************/
testProduct.Delete();
testCategory.Delete();
/**********end of test tear down***************************************/
}
[TestMethod]
public async Task UpdateCategoryProduct_ShouldRollbackTransaction()
{
/**********************set up the test******************/
RF _rf = RF.New();
ICategoriesRepository categoriesRepository = _rf.CategoriesRepository;
IProductsRepository productsRepository = _rf.ProductsRepository;
//create a test category and product
BOCategories testCategory = CreateTestCategory(categoriesRepository);
BOProducts testProduct = CreateTestProduct(productsRepository, (int)testCategory.CategoryID);
int testCategoryID = (int)testCategory.CategoryID;
/***********************end of test setup*****************/
//run the test, update the category name, but also update the product with an invalid categoryid,
//so the transaction will be rolled back
testCategory.CategoryName = "Updated";
testProduct.CategoryID = 2000000; //invalid category id
CategoryProductsController categoryProductsController = new CategoryProductsController();
try
{
var result = await categoryProductsController.PutCategoryProduct(new CategoriesVm(testCategory), new ProductsVm(testProduct));
Assert.Fail();//should not get here.
}
catch (System.Web.Http.HttpResponseException ex)
{
string errorMsg = ex.Response.Content.ReadAsStringAsync().Result;
StringAssert.Contains(errorMsg, "FOREIGN KEY");
//so we know the product update failed. Check the 'Updated' value of category has also been rolled back.
BOCategories checkCategory = new BOCategories() { Repository = categoriesRepository };
checkCategory.Init(testCategoryID);
StringAssert.Equals(checkCategory.CategoryName, "Not Updated");
}
/**********tear down the test******************************************/
testProduct.Delete();
testCategory.Delete();
/**********end of test tear down***************************************/
}
private BOCategories CreateTestCategory(ICategoriesRepository categoriesRepository)
{
BOCategories testCategory = new BOCategories() { Repository = categoriesRepository };
testCategory.CategoryName = "Not Updated";
testCategory.Description = "Test Category";
testCategory.SaveNew();
return testCategory;
}
private BOProducts CreateTestProduct(IProductsRepository productsRepository, int validCategoryID)
{
BOProducts testProduct = new BOProducts() { Repository = productsRepository };
testProduct.ProductName = "New Test Product";
testProduct.Discontinued = false;
testProduct.CategoryID = validCategoryID;
testProduct.SaveNew();
return testProduct;
}
Finally, run the tests and confirm that one transaction is committed successfully, and the other is rolled back successfully.
Step 11. Check the time. All told, 15 mins? maybe 20? Never mind, time well spent and job well done.