【问题标题】:How to unit test ViewModel consuming methods?如何对 ViewModel 使用方法进行单元测试?
【发布时间】:2013-10-24 15:15:53
【问题描述】:

我想测试一个以 ViewModel 作为参数的辅助方法。我遇到的问题是测试似乎需要我实例化并分配我的 ViewModel 使用的所有模型。在我下面给出的示例中,这没什么大不了的,因为只有几个,但在我正在处理的实际 VM 中却有很多。有没有其他方法可以做到这一点,这样我就不必创建和分配每个对象?

用于说明目的的示例代码...

模型

public class Meal
{
    public int MealID { get; set; }

    public string Title { get; set; }
    public decimal Cost { get; set; }
}

public class Beverage
{
    public int BeverageID { get; set; }

    public string Title { get; set; }
    public decimal Cost { get; set; }
}

public class Desert
{
    public int DesertID { get; set; }

    public string Title { get; set; }
    public decimal Cost { get; set; }
}

视图模型

public class DinnerViewModel
{
    public Meal Meal { get; set; }
    public Beverage Beverage { get; set; }
    public Desert Desert { get; set; }

    public decimal SalesTax { get; set; }
    public bool SeniorDiscount { get; set; }
}

帮手

public class Calculator
{
    public decimal Total(DinnerViewModel dvm)
    {
        decimal subtotal = dvm.Meal.Cost + dvm.Beverage.Cost + dvm.Desert.Cost;

        if (dvm.SeniorDiscount)
        {
            subtotal = subtotal - (subtotal * 0.1M);
        }

        return subtotal + (subtotal * dvm.SalesTax);
    }
}

测试

[TestMethod]
public void CalculatorReturnsCorrectTotalForNonSenior()
{
    DinnerViewModel dvm = new DinnerViewModel();
    dvm.Meal.Cost = 7M;
    dvm.Beverage.Cost = 1M;
    dvm.Desert.Cost = 2M;
    dvm.SalesTax = 0.08M;
    dvm.SeniorDiscount = false;

    Calculator calc = new Calculator();

    decimal expected = 10.80M;
    decimal actual = calc.Total(dvm);

    Assert.AreEqual(expected, actual, "The actual value does not match the expected value.");
}

这会导致“NullReferenceException”错误。正如我所说,我可以创建和分配必要的对象...

[...]
Meal meal = new Meal();
dvm.Meal = meal;
dvm.Meal.Cost = 7M;
[...]

...一旦全部完成,测试就会成功——但这似乎需要在更大的 VM 上进行大量工作,我觉得我必须做一些事情来让这更容易。

【问题讨论】:

    标签: c# unit-testing viewmodel


    【解决方案1】:

    您会看到 NullReferenceException,因为 Meal、Beverage 和 Desert 实例在您实例化它时没有分配给 DinnerViewModel 的相应属性。所以,所有这些属性都是null。您应该在访问这些属性之前创建并分配新对象。您可以使用视图模型的构造函数:

    public class DinnerViewModel
    {
        public DinnerViewModel()
        {
            Meal = new Meal();
            Beverage = new Beverage();
            Desert = new Desert();
        }
    
        public Meal Meal { get; set; }
        public Beverage Beverage { get; set; }
        public Desert Desert { get; set; }
    
        public decimal SalesTax { get; set; }
        public bool SeniorDiscount { get; set; }
    }
    

    我还喜欢创建一些返回存根进行测试的辅助方法。它消除了重复并使您的测试清晰:

    private DinnerViewModel CreateTenDollarsDinner()
    {
        return new DinnerViewModel {
            Meal = new Meal { Cost = 7M },
            Beverage = new Beverage { Cost = 1M },
            Desert = new Desert { Cost = 2M },
            SalesTax = 0.08M,
            SeniorDiscount = false
        };
    }
    
    [TestMethod]
    public void CalculatorReturnsCorrectTotalForNonSenior()
    {
        DinnerViewModel dvm = CreateTenDollarsDinner();    
        Calculator calc = new Calculator();
        Assert.AreEqual(10.80M, calc.Total(dvm));
    }
    

    【讨论】:

    • 我发现我做错的另一件事在我的示例代码中并不明显。当一个模型有一个像public int AnotherModelID { get; set; }这样的FK和一个像public virtual AnotherModel AnotherModel { get; set; }这样的导航属性时,例如,可以在视图中访问像vm.Model.AnotherModel.Property这样的属性,但在测试时这似乎不起作用。除了在构造函数中实例化模型之外,我还必须将模型添加到 VM 中,例如 AnotherModel AnotherModel = new AnotherModel();
    【解决方案2】:

    要么你的模型(Meal、Beaverage 和 Dessert)也是 ViewModel,要么你的 DinnerViewModel 不是真正的 ViewModel。

    ViewModel 的目的是为 View 提供直接可用的值。

    根据您的单元测试示例,将您的 Calculator.Total 辅助方法直接放在 DinnerViewModel 并将总计算值作为属性公开给视图会更合适:

    public class DinerViewModel
    {
        public Meal Meal { get; set; }
        public Beverage Beverage { get; set; }
        public Desert Desert { get; set; }
    
        public decimal SalesTax { get; set; }
        public bool SeniorDiscount { get; set; }
    
        public decimal TotalCostOfDinner
        {
            get
            {
                decimal subtotal = 0M;
                if(Meal != null) subtotal += Meal.Cost;
                if(Beverage != null) subtotal += Beverage.Cost;
                if(Desert != null) subtotal += Desert.Cost;
    
                if (SeniorDiscount) subtotal -= subtotal * 0.1M;
    
                return subtotal + (subtotal * SalesTax);
            }
        }
    }
    

    现在您的视图可以直接从 DinnerViewModel 获取正确的 TotalCostOfDinner。

    现在进行单元测试:

    [TestMethod]
    public void TotalCostOfDinnerReturnsCorrectTotalForNonSenior()
    {
        DinnerViewModel dvm = new DinnerViewModel
        {
            Meal = new Meal { Cost = 7M },
            Beverage = new Beverage { Cost = 1M },
            Desert = new Desert { Cost = 2M },
            SalesTax = 0.08M,
            SeniorDiscount = false
        };
    
    
        decimal expected = 10.80M;
        decimal actual = dvm.TotalCostOfDinner;
    
        Assert.AreEqual(expected, actual, "The actual value does not match the expected value.");
    }
    

    【讨论】:

    • 我的实际 ViewModel 用于创建库存数据输入表单(向数据库添加新项目)。为了在一个页面中完成所有这些,我必须使用 VM 将许多不同的模型组合在一起。提交表单后,我必须将 VM 中的数据映射到其对应的模型中,以便将所有内容保存到数据库中。我没有在 Controller 中进行此映射,而是将逻辑移到了 Helper 中。 Controller 将 VM 发送给 Helper,Helper 完成它的工作,将新 Item 保存到数据库,如果一切顺利,则返回 true
    猜你喜欢
    • 2013-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-29
    • 1970-01-01
    • 2012-06-01
    相关资源
    最近更新 更多