单元测试涉及一些UI元素

时间:2016-01-15 23:53:44

标签: c# wpf unit-testing user-interface mvvm

我应该编写如下的单元测试吗?

代码:

public ObservableCollection<DXTabItem> Tabs { get; private set; }
public ICommand CustomersCommand { get; private set; }

CustomersCommand = new DelegateCommand(OpenCustomers);

private void OpenCustomers()
{
   var projectService = new ProjectService(Project.FilePath);
   var vm = new CustomersViewModel(projectService);
   AddTab("Customers", new CustomersView(vm));
}

public void AddTab(string tabName, object content, bool allowHide = true)
{
    Tabs.Add(new DXTabItem { Header = tabName, Content = content });
}

测试:

[TestMethod]
public void CustomerCommandAddsTab()
{
    _vm.CustomersCommand.Execute(null);
    Assert.AreEqual("Customers", _vm.Tabs[1].Header);
}

XAML:

<dx:DXTabControl ItemsSource="{Binding Tabs}" />

我正在使用TDD方法,所以这是工作代码,并且它在本地传递测试,但是在服务器CI构建上它未通过此测试,因为视图(CustomersView)内部有一些东西没有工作。所以我意识到了这个测试,尽管它的简单实际上是在打破MVVM。我通过引用ViewModelDXTabItems内部编写UI代码,甚至新建了View

这样的事情的正确方法是什么?我是不是应该编写这样的测试(并且依赖于自动化测试),或者我应该以某种方式重构它以使ViewModel不包含UI元素,关于我应该如何做的提示将是有用的。

澄清:

这种设计的原因是每个选项卡包含一个不同的视图,例如Customers选项卡包含CustomersView,而另一个选项卡将包含完全不同的数据和表示。因此很难定义一种机制,允许以MVVM方式实现这一点。至少答案并非微不足道。

1 个答案:

答案 0 :(得分:2)

如果DXTabItem是从TabItem派生的,那么这不是MVVM,在MVVM中,您永远不会直接在视图模型中访问视图元素。您应该做的是为您的选项卡创建一个视图模型(例如TabViewModel),将选项卡更改为ObservableCollection<TabViewModel>并将选项卡控件的ItemsSource属性绑定到该属性以创建GUI标签本身。

对于CI失败,您不应该在单元测试中创建GUI元素(即CustomersView)。你唯一一次这样做是在集成测试期间,这是一个不同的鱼。视图应该只通过数据绑定机制松散地耦合到视图模型,您应该能够运行和测试整个应用程序而无需创建单个视图对象。

更新:实际上这很容易......一旦你知道如何! :)有几种不同的方法可以实现您的目标,但最常见的两种方法是数据模板和触发器。

使用数据模板,您依赖于视图模型应该代表GUI背后的逻辑这一事实。如果您有一个客户端选项卡和一个产品选项卡(比如说),则那些应该具有相应的视图模型,即ClientPage和ProductPage。您可能希望为这些创建基类(例如TabViewModel),在这种情况下,您的视图模型集合将是ObservableCollection<TabViewModel>,如上所述,否则只需将其设为ObservableCollection<object>。然后,您可以使用数据模板指定要为每个选项卡创建的视图:

<DataTemplate DataType="{x:Type vm:ClientPage>
    <view:ClientView />
</DataTemplate>

<DataTemplate DataType="{x:Type vm:ProductPage>
    <view:ProductView />
</DataTemplate>

ListBox和其他集合元素将自动应用这些数据模板,或者您可以显式指定ListBox.ItemTemplate并在需要时使用ContentControl。

第二种方法是使用数据触发器。如果您的页面已修复,那么我发现在视图模型层中创建枚举会有所帮助,原因我会在一分钟内解释:

public enum PageType : int
{
    Client,
    Product,
    ... etc ...
}

回到你的XAML,你会想要为每一个创建一个页面,你可以在你的VM中做到这一点,如果你喜欢虽然这是一个很容易的任务我通常在XAML中这样做:

<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="PageType">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="vm:PageType" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>

现在您可以创建一个TabControl并将ItemsSource绑定到此对象,并为枚举中的每个项目显示一个单独的选项卡:

    <TabControl ItemsSource="{Binding Source={StaticResource PageType}}"
                SelectedIndex="{Binding CurrentPage, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}"
                IsSynchronizedWithCurrentItem="True">

CurrentPage当然是MainTypeModel中PageType

类型的属性
    private PageType _CurrentPage;
    public PageType CurrentPage
    {
        get { return _CurrentPage; }
        set { _CurrentPage = value; RaisePropertyChanged(); }
    }

XAML不够智能处理枚举,所以你还需要EnumToIntConverter的代码,它可以在两者之间进行转换:

public class EnumToIntConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return (int)value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return Enum.ToObject(targetType, value);
    }

    #endregion
}

使用这样的枚举可能看起来比需要的工作多一些,但它确实意味着您的视图模型代码现在可以通过执行类似`this.CurrentPage = PageType.Client'的任何时候设置活动页面。这在应用程序的后期阶段尤其方便,您可能希望在应用程序的其他位置有一个产品列表,并且您希望为用户提供一个打开产品页面的按钮(例如)。这为您的整个应用程序提供了对标签行为的大量控制。当然,这也意味着每当用户更改Tabs时(即当this.CurrentPage更改值时)都会收到通知,这对于按需加载数据非常有用,可以提高应用程序的性能...如果您更改了稍后您的枚举中的页面顺序,因为您的视图模型代码正在检查枚举而不是整数页码!

我没有展示的另一件事是如何在每个页面上显示相应的子内容,就像我说的那样,这是通过列表框项目样式中的数据触发器完成的:

        <TabControl.Resources>

            <Style TargetType="{x:Type TabItem}" BasedOn="{StaticResource {x:Type TabItem}}">
                <Style.Triggers>

                    <!-- Client -->
                    <DataTrigger Binding="{Binding}" Value="{x:Static vm:PageType.Client}">
                        <Setter Property="Header" Value="Client" />
                        <Setter Property="Content">
                            <Setter.Value>
                                <view:ClientView DataContext="{Binding ElementName=parentTab, Path=DataContext.ClientPage"/>
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>

                    <!-- Product -->
                    <DataTrigger Binding="{Binding}" Value="{x:Static vm:PageType.Product}">
                        <Setter Property="Header" Value="Product" />
                        <Setter Property="Content">
                            <Setter.Value>
                                <view:ProductView DataContext="{Binding ElementName=parentTab, Path=DataContext.ProductPage"/>
                            </Setter.Value>
                        </Setter>
                    </DataTrigger>

正如您所看到的,每个DataTrigger只是检查它的DataContext被设置为哪个枚举,并相应地设置它自己的标题和内容。