享受阅读!
我发现阅读this文章的正确方法
这是一个漫长的故事,大多数人可以跳过它 :) 但那些想要了解问题和解决方案的人必须全部阅读!
我是QA,一段时间以前负责产品自动化的工作。幸运的是,这种自动化不是在某个测试工具中进行,而是在Visual Studio中进行,因此与开发最为接近。
我们使用的自动化框架由MbUnit(Gallio作为运行器)和MINT(我们合作的客户编写的MbUnit补充)组成。MbUnit为我们提供了测试夹具和测试,而MINT添加了额外的小层-测试中的操作。例如,夹具称为“FilteringFixture”。它包括像“TestingFilteringById”或“TestingFilteringWithSpecialChars”之类的多个测试。每个测试由操作组成,这些操作是我们测试的原子单位。操作的示例包括-“打开应用程序(参数)”,“打开过滤对话框”等。
我们已经有很多测试,其中包含很多操作,乱七八糟。它们使用产品QA的内部API。此外,我们开始调查一种新的自动化方法——通过Microsoft UI Automation进行UI自动化(抱歉,这是重复的)。因此,对于经理来说,“导出器”或“报告工具”的必要性变得非常严重。任务简述:
一段时间以前,我接到了一个任务,需要开发一个应用程序,可以解析 DLL (其中包含测试夹具、测试方法和操作 - 是我们基于单元测试的自动化框架的单元),并将其结构以人类可读的格式(TXT、HTML、CSV、XML 或其他格式)导出。我决定使用 WPF 和纯 MVVM 来实现它(这两个东西对我来说都是全新的)。对我来说,最困难的两个问题分别是 MVVM 方法本身,以及将 MVVM 绑定到 TreeView 控件。我跳过了 MVVM 划分的部分,这是一个单独文章的主题。下面的步骤是关于在 MVVM 方式下绑定到 TreeView 的。
public class MintFixutre : IMintEntity
{
private readonly string _name;
private readonly int _ordinalNumber;
private readonly List<MintTest> _tests = new List<MintTest>();
public MintFixutre(string fixtureName, int ordinalNumber)
{
_name = fixtureName;
if (ordinalNumber <= 0)
throw new ArgumentException("Ordinal number must begin from 1");
_ordinalNumber = ordinalNumber;
}
public List<MintTest> Tests
{
get { return _tests; }
}
public string Name { get { return _name; }}
public bool IsParent { get { return true; } }
public int OrdinalNumber { get { return _ordinalNumber; } }
}
public class MintTest : IMintEntity
{
private readonly string _name;
private readonly int _ordinalNumber;
private readonly List<MintAction> _actions = new List<MintAction>();
public MintTest(string testName, int ordinalNumber)
{
if (string.IsNullOrWhiteSpace(testName))
throw new ArgumentException("Test name cannot be null or space filled");
_name = testName;
if (ordinalNumber <= 0)
throw new ArgumentException("OrdinalNumber must begin from 1");
_ordinalNumber = ordinalNumber;
}
public List<MintAction> Actions
{
get { return _actions; }
}
public string Name { get { return _name; } }
public bool IsParent { get { return true; } }
public int OrdinalNumber { get { return _ordinalNumber; } }
}
public class MintAction : IMintEntity
{
private readonly string _name;
private readonly int _ordinalNumber;
public MintAction(string actionName, int ordinalNumber)
{
_name = actionName;
if (ordinalNumber <= 0)
throw new ArgumentException("Ordinal numbers must begins from 1");
_ordinalNumber = ordinalNumber;
}
public string Name { get { return _name; } }
public bool IsParent { get { return false; } }
public int OrdinalNumber { get { return _ordinalNumber; } }
}
顺便说一下,我还创建了一个接口,它实现了所有的实体。这样的接口可以帮助你以后使用。不确定是否应该添加List<IMintEntity>
类型的Childrens
属性或类似的内容?
public interface IMintEntity
{
string Name { get; }
bool IsParent { get; }
int OrdinalNumber { get; }
}
_fixtures
列表中。private void ParseDllToEntityModel()
{
_fixutres = new List<MintFixutre>();
// enumerating Fixtures
int f = 1;
foreach (Type fixture in AssemblyTests.GetTypes().Where(t => t.GetCustomAttributes(typeof(TestFixtureAttribute), false).Length > 0))
{
var tempFixture = new MintFixutre(fixture.Name, f);
// enumerating Test Methods
int t = 1;
foreach (var testMethod in fixture.GetMethods().Where(m => m.GetCustomAttributes(typeof(TestAttribute), false).Length > 0))
{
// filtering Actions
var instructions = testMethod.GetInstructions().Where(
i => i.OpCode.Name.Equals("newobj") && ((ConstructorInfo)i.Operand).DeclaringType.IsSubclassOf(typeof(BaseAction))).ToList();
var tempTest = new MintTest(testMethod.Name, t);
// enumerating Actions
for ( int a = 1; a <= instructions.Count; a++ )
{
Instruction action = instructions[a-1];
string actionName = (action.Operand as ConstructorInfo).DeclaringType.Name;
var tempAction = new MintAction(actionName, a);
tempTest.Actions.Add(tempAction);
}
tempFixture.Tests.Add(tempTest);
t++;
}
_fixutres.Add(tempFixture);
f++;
}
}
Fixtures
,类型为List<MintFixture>
,以返回刚刚创建的数据模型(包含测试列表和操作列表的夹具列表)。这将成为我们TreeView
的绑定源。public List<MintFixutre> Fixtures
{
get { return _fixtures; }
}
List<MintFixutre>
类型的DLL的Fixtures
公共属性。我们将从MainWindow的XAML绑定到它。类似于这样(简化版):var _exporter = MySuperDllReaderExporterClass ();
// public property of ViewModel for TreeView, which returns property from #4
public List<MintFixture> Fixtures { get { return _exporter.Fixtures; }}
// Initializing exporter class, ParseDllToEntityModel() is called inside getter
// (from step #3). Cool, we have entity model for binding.
_exporter.PathToDll = @"open file dialog can help";
// Notifying all those how are bound to the Fixtures property, there are work for them, TreeView, are u listening?
// will be faced later in this article, anticipating events
OnPropertyChanged("Fixtures");
<Grid.Resources>
部分,其中包含一组用于我们的TreeViewItem
的模板。对于那些有子项的项目(Fixtures和Tests),我们使用HierarchicalDataTemplate
,对于“叶子”项目(Actions),我们使用DataTemplate
。对于每个模板,我们指定其内容(文本、TreeViewItem图像等)、ItemsSource(如果此项具有子项,例如对于Fixtures它是{Binding Path=Tests}
)和ItemTemplate(仅在此项具有子项的情况下,在这里我们设置模板之间的链接——FixtureTemplate使用TestTemplate作为其子项,TestTemplate使用ActionTemplate作为其子项,Action模板不使用任何内容,它是一个叶子!)。重要提示:不要忘记,为了将“一个”模板“链接”到“另一个”模板,“另一个”模板必须在XAML中定义在“一个”模板之上!(只是列举我自己的错误 :))<Window.Resources><MyViewModel x:Key="DontUseMeForThat" /></Window.Resources>
。如果这样做,那么您将无法通知它属性更改。为什么?静态资源是静态资源,它只初始化一次,之后保持不变。我可能在这里错了,但这是我的一个错误。因此,对于TreeView,请使用ItemsSource="{Binding Fixtures}"
而不是ItemsSource="{StaticResource myStaticViewModel}"
8. ViewModel - ViewModelBase - Property Changed: 几乎全部都完成了。等等!当用户打开应用程序时,TreeView最初当然是空的,因为用户还没有打开任何DLL!我们必须等待用户打开DLL,然后才执行绑定。这是通过OnPropertyChanged
事件完成的。为了让生活更轻松,我所有的ViewModel都继承自ViewModelBase,它确实向所有的ViewModel公开了这个功能。public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, args);
}
}
MVVM
,因此点击通过命令处理。在OpenDllExecuted
处理程序的末尾,执行OnPropertyChanged("Fixtures")
,通知树形结构,绑定到其上的属性已更改,现在是刷新自身的时候了。可以从这里中获取RelayCommand
帮助类的示例。顺便说一句,据我所知,存在一些帮助库和工具包。类似于XAML中发生的情况如下:
ViewModel - 命令
private ICommand _openDllCommand;
//...
public ICommand OpenDllCommand
{
get { return _openDllCommand ?? (_openDllCommand = new RelayCommand(OpenDllExecuted, OpenDllCanExecute)); }
}
//...
// decides, when the <OpenDll> button is enabled or not
private bool OpenDllCanExecute(object obj)
{
return true; // always true for Open DLL button
}
//...
// in fact, handler
private void OpenDllExecuted(object obj)
{
var openDlg = new OpenFileDialog { ... };
_pathToDll = openDlg.FileName;
_exporter.PathToDll = _pathToDll;
// Notifying TreeView via binding that the property <Fixtures> has been changed,
// thereby forcing the tree to refresh itself
OnPropertyChanged("Fixtures");
}