2

I want to unit test a controller that takes a FormCollection and has the Find method. I have successfully implemented a fake DbContext and a FakeDbSet (from here) so that I can use the fake objects in my tests.

The specific method I want to test looks like this:

    //
    // POST: /Order/SettleOrders

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult SettleOrders(FormCollection c)
    {
        int i = 0;
        if (ModelState.IsValid)
        {
            var ordersCollection = new List<Order>();

            // Get values of form
            var OrderIdArray = c.GetValues("item.OrderId");
            var UnitPriceArray = c.GetValues("item.UnitPrice");

            for (i = 0; i < OrderIdArray.Count(); i++)
            {
                // Find order in database and update the unitprice and isconfirmed status
                Order order = db.Orders.Find(Convert.ToInt32(OrderIdArray[i]));
                order.UnitPrice = Convert.ToDecimal(UnitPriceArray[i]);
                order.IsConfirmed = true;
                db.SetModified(order);
                ordersCollection.Add(order);
            }
            db.SaveChanges();
        }

        // Return orders of this date to view
        var currentDate = DateTime.Today;
        var orders = db.Orders.Include(o => o.Product)
                                .Include(o => o.User)
                                .Where(o => o.Date == currentDate);
        return View("Confirmation", orders.ToList().OrderBy(o => o.User.Name));
    }

This is how I set up the test of my OrderController using Moq:

[TestInitialize]
    public void OrderControllerTestInitialize()
    {
        // Arrange 
        var unconfirmedMemoryItems = new FakeOrderSet
        {
            // @TODO Tests/methods should ideally not be dependent on DateTime.Today...
            new Order { OrderId = 1, UnitPrice = 1.00M, Quantity = 2, Date = DateTime.Today, IsConfirmed = false },
            new Order { OrderId = 2, UnitPrice = 2.00M, Quantity = 1, Date = DateTime.Today, IsConfirmed = false }
        };

        // Create mock unit of work
        var unconfirmedMockData = new Mock<ISeashellBrawlContext>();
        unconfirmedMockData.Setup(m => m.Orders).Returns(confirmedMemoryItems);

        // Setup controller
        unconfirmedOrderController = new OrderController(confirmedMockData.Object);
    }

Then the test goes likes this to confirm the unconfirmed orders are becoming confirmed.

    [TestMethod]
    public void TestSettleOrdersPost()
    {
        // Invoke
        FormCollection form = CreatesettleOrdersPostFormCollection();
        var viewResult = unconfirmedOrderController.SettleOrders(form) as ViewResult;
        var ordersFromView = (IEnumerable<Order>)viewResult.Model;

        // Assert
        Assert.AreEqual(3, ordersFromView.ElementAt(0).Quantity,
            "New item should be added to older one since it has same index and is of same date");
        Assert.AreEqual(true, ordersFromView.ElementAt(0).IsConfirmed,
           "The item should also be set to confirmed");
    }

    // Helper methods

    private static FormCollection CreatesettleOrdersPostFormCollection()
    {
        FormCollection form = new FormCollection();
        form.Add("item.OrderId", "1");
        form.Add("item.UnitPrice", "2.00");
        form.Add("item.OrderId", "2");
        form.Add("item.UnitPrice", "3.00");
        return form;
    }

Unfortunately I get the following error message:

Test Name: TestSettleOrdersPost Result Message:

System.NullReferenceException: Object reference not set to an instance of an object. Result StackTrace: at Controllers.OrderController.SettleOrders(FormCollection c) in Controllers\OrderController.cs:line 121

This probably has to do with the fake Find method not doing what it is supposed to do. The find method I use looks like this:

class FakeOrderSet : FakeDbSet<Order>
{
    public override Order Find(params object[] keyValues)
    {
        var id = Convert.ToInt32(keyValues[0]);
        Order o = null;

        IQueryable<Order> keyQuery = this.AsQueryable<Order>();
        foreach (var order in keyQuery)
        {
            if (order.OrderId == id)
            {
                o = order;
            }
        }

        return o;
    }
}

I have no idea how to improve this code. Looking at it I am convinced it should work. Since I am a 7-day noob to unit testing this conviction is of not much worth though. I hope someone can help me out.

user2609980
  • 10,264
  • 15
  • 74
  • 143
  • _"This probably has to do"_ ... put a breakpoint before line 121 and see what happens. – CodeCaster Dec 12 '13 at 08:43
  • @CodeCaster How do I put a breakpoint in code when I am doing unittests? I cannot manage to do that and I can also not write to console... Having these tools available would be a huge help. Ah I found an answer: `Debug` -> `Tests In Current Context` (or otherwise) – user2609980 Dec 12 '13 at 08:45
  • 1
    See http://stackoverflow.com/questions/12594018/cannot-debug-a-unit-testing-project-in-visual-studio-2012 and http://stackoverflow.com/questions/4103712/stepping-through-and-debugging-code-in-unit-tests. Right-click the test in Test Explorer and click Debug if other ways don't work. – CodeCaster Dec 12 '13 at 08:47

2 Answers2

2

I think you can include Find method implementation in your generic FakeDbSet<T> class. For example if all your entites have property Id that is mapped to primary key in database you can do following:

public class FakeDbSet<T>
{
  ....

  public T Find(params object[] keyvalues)
  {
    var keyProperty = typeof(T).GetProperty(
            "Id", 
            BindingFlags.Instance | BindingFlags.Public |  BindingFlags.IgnoreCase);
    var result = this.SingleOrDefault(obj => 
        keyProperty.GetValue(obj).ToString() == keyValues.First().ToString());
    return result;
  }
}

If your key property is not Id you can try to determine some algorith how to recognize key property for the entity via reflection (for example if Order class have OrderId key property you can try to determine it by getting class name with reflection and concatenating it with "Id" string).

  • Yes this was an option, and then also make all your fake objects get assigned an id. I did not get this to work though. Fortunately I found what the mistake was (I did not post a bit of code related to that since I thought it was not relevant), my method did work since I should have also initialized a User `FakeDbSet` since it was writing something to that! The `Find` method was working after all. – user2609980 Dec 12 '13 at 10:37
1

I have the same problem:

    public override TEntity Find(params object[] keyValues)
    {
        if (keyValues.Length == 0)
            throw new ArgumentNullException("keyValues");
        return this.SingleOrDefault(GenerateKeyFilter(keyValues));
    }

    private Expression<Func<TEntity, bool>> GenerateKeyFilter(object[] keyValues)
    {
        var conditions = new List<BinaryExpression>();
        var objectParam = Expression.Parameter(typeof(TEntity));

        var keyFields = !!!Helper.KeyFields<TEntity>();

        if (keyFields.Count != keyValues.Length)
            throw new KeyNotFoundException();
        for (var c = 0; c < keyFields.Count; c++)
            conditions.Add(Expression.MakeBinary(
                ExpressionType.Equal,
                Expression.MakeMemberAccess(objectParam, keyFields[c]),
                Expression.Constant(keyValues[c], keyFields[c].PropertyType)
                ));
        var result = conditions[0];
        for (var n = 1; n < conditions.Count; n++)
            result = Expression.And(result, conditions[n]);
        return Expression.Lambda<Func<TEntity, bool>>(result, objectParam);
    }

This works well, however, to create the tests I had to reconfigure the models and manually set the keys, similar to use of EntityTypeConfiguration.

Glayson
  • 31
  • 2