Tuesday, June 12, 2012

Learning MVC: Unit Testing CRUD actions in MVC

Continuing the example with a recipe database, and having the most basic structure working (recipes belong to categories and consist of ingredients), it is time to remember about TDD (test driven development). To be honest, it is already too late, because in proper TDD tests are supposed to be writter before the code is. I'll do my best next time. This time, however, there are some simple tests I can think of which relate to the Ingredient entity

  • Test that an Ingredient can be inserted into the database
  • Test that an Ingredient can be edited
  • Test that an Ingredient can be deleted from the database
  • Test that an Ingredient can not be deleted if it is used by any Recipe
  • Test that an Ingredient with a name that is too short can not be created
  • Test that an Ingredient with a name that is too long can not be created
  • Test that an existing Ingredient can not be edited so that its name becomes too short
  • Test that an existing Ingredient can not be edited so that its name becomes too long

In fact, the last four tests are not related to database manipulations because the errors will be caught before the attempt to save data is made. It would be more appropriate to place them in a separate class, which will test the model and I'll return to them next time. The first four tests appear to test database operations.

Firstly, I would like to have each test start from a known state - for example, an almost empty database which has some minimal amount of seed data. To achieve that, I use the initializer class that inherits from DropCreateDatabaseAlways. For reference, here is the full listing - the only thing I care about is to override the Seed function. I will use the data to test that I can not delete the ingredient which is in use.

public class TestDatabaseInitializer : DropCreateDatabaseAlways<RecipesEntities>
{
 protected override void Seed(RecipesEntities context)
 {
  var category0 = new Category { CategoryName = "Mains", Description = "Main Dishes" };
  var category1 = new Category { CategoryName = "Desserts", Description = "Dessert Dishes" };
  var categories = new List<Category>() { category0, category1 };
  categories.ForEach(c => context.Categories.Add(c));

  var ingredient0 = new Ingredient { IngredientName = "Meat" };
  var ingredient1 = new Ingredient { IngredientName = "Fish" };
  var ingredient2 = new Ingredient { IngredientName = "Potato" };
  var ingredients = new List<Ingredient>() { ingredient0, ingredient1, ingredient2 };
  ingredients.ForEach(i => context.Ingredients.Add(i));

  var recipes = new List<Recipe>();
  recipes.Add(new Recipe { RecipeName = "Grilled fish with potatoes", Category = category0, Ingredients = new List<Ingredient>() { ingredient1, ingredient2 } });
  recipes.Add(new Recipe { RecipeName = "Grilled steak with potatoes", Category = category0, Ingredients = new List<Ingredient>() { ingredient0, ingredient2 } });
  recipes.ForEach(r => context.Recipes.Add(r));
 }
}

I added a small function to my test class to create a database.

[TestInitialize()]
public void SetupDatabase()
{
 Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
 Database.SetInitializer<RecipesEntities>(new TestDatabaseInitializer());
}

Now I think I'm ready to create a simple test.

[TestMethod()]
public void CreateTest()
{
 SetupDatabase();
 IngredientController target = new IngredientController();
 Ingredient ingredient = new Ingredient() { IngredientName = "test" };

 ActionResult actual = target.Create(ingredient);
 Assert.IsTrue(ingredient.IngredientID != 0);

 RecipesEntities db = new RecipesEntities();
 var newIngredient = db.Ingredients.Find(ingredient.IngredientID);
 Assert.AreEqual(ingredient.IngredientName, newIngredient.IngredientName);
}

I create the Ingredient, record it's IngredientID and make sure that I can retrieve it back from the database by ID after it's added. Deletion and editing tests are equally simple. Now the "negative" test: I'm not testing what I can do now, but rather what I can not do. I should not be able to delete the ingredient if it is used by any recipes - that would destroy referential integrity. Here is a simple test that passes:

[TestMethod()]
public void CanNotDeleteUsedIngredient2()
{
 SetupDatabase();
 IngredientController target = new IngredientController();
 RecipesEntities db = new RecipesEntities();
 var ingredient = db.Ingredients.Where(i => i.IngredientName == "Meat").FirstOrDefault();
 int id = ingredient.IngredientID;

 Assert.IsNotNull(ingredient);
 ActionResult actual = target.DeleteConfirmed(id);

 db = new RecipesEntities();
 var deletedIngredient = db.Ingredients.Find(id);
 Assert.IsNotNull(deletedIngredient);
}

Okay, I tried to delete the ingredient and then I verified and it is still in the database. That's expected, but that tells me nothing about the reason why the ingredient was not deleted. Maybe the whole database is offline or there is an uncommitted transaction. To improve the test, I would like to know something about the reason. I need to check for the errors in the ModelState. Fortunately, I can access the ModelState from the ActionResult. Here is what I could do to return the first error that is found in the ModelState:

public string GetFirstErrorMessage(ActionResult result)
{
 ViewResult vr = (ViewResult)result;

 foreach (ModelState error in vr.ViewData.ModelState.Values)
 {
  foreach (var innerError in error.Errors)
  {
   if (!string.IsNullOrEmpty(innerError.ErrorMessage))
   {
    return innerError.ErrorMessage;
   }
  }
 }
 return string.Empty;
}

Now I can modify the test to check the ErrorMessage. The check is rather lame at the moment - the error message is created dynamically to tell the user what recipes exactly use the ingredient. So I do not want to check the full error message and I'm satisfied with the fact that the first 10 characters are what I expect. Here is the slightly modified test:

[TestMethod()]
public void CanNotDeleteUsedIngredient()
{
 SetupDatabase();
 IngredientController target = new IngredientController();
 RecipesEntities db = new RecipesEntities();
 var ingredient = db.Ingredients.Where(i => i.IngredientName == "Meat").FirstOrDefault();
 int id = ingredient.IngredientID;

 Assert.IsNotNull(ingredient);

 ActionResult actual = target.DeleteConfirmed(id);
 Assert.AreEqual(GetFirstErrorMessage(actual).Substring(0, 10), "Cannot del");

 db = new RecipesEntities();
 var deletedIngredient = db.Ingredients.Find(id);
 Assert.IsNotNull(deletedIngredient);
}

References:

Code First Entity Framework Unit Test Examples
Exercise 2: Testing CRUD actions
How to get all Errors from asp.net mvc modelState?
How to get the Model from an ActionResult? by . Also posted on my website

1 comment:

Roy Anderson said...

Where have you implemented the ingredient amount? I don't see it. As such the example is incomplete without it.