在FakeDbSet实现中伪造find方法

时间:2013-12-12 08:37:31

标签: c# unit-testing visual-studio-2012 moq

我想对采用FormCollection且具有Find方法的控制器进行单元测试。我已经成功实现了假DbContextFakeDbSet(来自here),以便我可以在我的测试中使用假对象。

我想测试的具体方法如下:

    //
    // 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));
    }

这是我使用OrderController

设置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);
    }

然后测试就这样确认未经证实的订单正在被确认。

    [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;
    }

不幸的是我收到以下错误消息:

  

测试名称:TestSettleOrdersPost   结果消息:

     

System.NullReferenceException:未将对象引用设置为实例   一个对象。结果StackTrace:at   Controllers.OrderController.SettleOrders(FormCollection c)in   Controllers \ OrderController.cs:第121行

这可能与假的Find方法有关,没有做它应该做的事情。我使用的find方法如下所示:

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;
    }
}

我不知道如何改进此代码。看着它我相信它应该有效。由于我是一个为期7天的单位测试用户,因此这种信念并不值得。我希望有人可以帮助我。

2 个答案:

答案 0 :(得分:2)

我认为您可以在通用Find类中包含FakeDbSet<T>方法实现。 例如,如果您的所有entites都具有映射到数据库中主键的属性Id,则可以执行以下操作:

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;
  }
}

如果您的密钥属性不是Id,您可以尝试通过反射确定如何识别实体的密钥属性的某些算法(例如,如果Order类具有OrderId密钥属性,则可以尝试通过使用反射获取类名并将其与“Id”字符串连接来确定它。

答案 1 :(得分:1)

我遇到同样的问题:

    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);
    }

但是,这很有效,我需要重新配置模型并手动设置密钥,类似于使用 EntityTypeConfiguration