接上篇,我留到这里才介绍怎样测试,因为不会做的话也不会测做得对不对。说是单元测试的话,其实应该设计好 Model 后,定好大概 VM 内要干什么之后,马上可以动手写测试代码。
很多公司没有规定如何测试,更加没有单元测试,也没考虑 TDD,或许是有他们的原因的,不一定是因为水平问题的,也不一定是同事能力所限。还真的有很多人,很多老开发,不愿意写测试。这测试只是一种开发方法,是其中一个而已。没有所谓绝对的最佳办法。
我这里说的,是一些情况,你必须要这么测,才有比较好的效率,才能确保代码正确无误。
想一想,如果你负责写模块,同时,另一位负责写 Shell,(比如用 PRISM 的 Modularity),你在没有 Shell 的情况下,点击 Debug Run 帮不了你。你的 View 全是 UserControl,你的入口,是个 IModule 的东西而且它只负责注册视图。你真要运行来看效果来「人肉」测试的话,你只能自己写一个 Shell 出来(?!),在 IModule 注册菜单后,再由 Shell 打开视图。
一个只为测试而做的 Shell ,可能很快能写出来。同样地,一个测试项目也一样很快能写完。它们之间的分别,是测试项目能自动重复运行。
如果 TDD 就更加不用说了。
废话讲完。入正题。
测试对象:ICommand 的 CanExecute
假设,用例之一,是「在没有选择明细行前,删除行按钮禁用」。在上几篇实现了的一个功能。
以下省略了测试描述,只含代码:
有没有选中明细行,这是视图中 DataGrid 对它 SelectedItem 属性(绑定 VM 的 CurrentRow)赋值的一个操作,可以像上面代码一样模拟它。注意,如果你 VM 构造函数,有检查参数是否为 null 然后是 null 就抛异常的话,上面代码就当然不行,要写个模拟,如下部分的做法。
上面代码,除了 new 什么出来测以外,真正的代码只有两行,一个是为 CurrentRow 赋值,另一个是运行 CanExecute 委托让它返回 true/false。额,当然还有 assertion 啦。
这测试,其实只是检查 { return CurrentRow != null; } 这一句委托而已,作用不大,但想象一下,如果这是 VM 包含了数据验证用了 IDataErrorInfo,测试目标是保存按钮能不能点击,那委托,就不是那么简单了。
测试对象:ICommand 的 Execute
假设,用例中,要求明细行内,单行中的物料栏,要能打开另一个选择窗口来选择物料。
这里做了几件事,这几件事是从用例而来,是操作的顺序。target.ItemCodeSelectionCommand 没有必要 Execute,运行了也只是有个空白窗口,标题是 Mock Empty Window 的而已,写这样纯粹为示范模拟类用到依赖注入上。
这测试能通过,但或许会有错误信息,不过你看看就知道是 BackgroundWorker 的事,我不作解释了,它跟这里无关。
其他
额,其他我不说了。测属性没什么好说,特别是在这里没数据验证的情况下。
总结
我觉得测试中,MVVM 跟其他的,不一样的地方就是这个 ICommand。上面写了它两方面的测试运作。我就单单写这部分算了。
测试中,没有调用采购订单的视图,连弹出窗口也是假的,这测试,要测的是 ViewModel 本身。MVVM 在单独测试有优势,就是 ViewModel 它自己能做到完全与 View 隔开,单独运行。好处是:
- 外壳没写就已经开始写和测试 ViewModel
- 弹出窗口没写也可以写和测试 ViewModel
- 减少了依赖,发生错误时,查找的范围只限于 ViewModel 本身
- 每次改动后,重复测试,能确保 ViewModel 按照原定的行为跑
至于 Model 呢,Model 本身只是个放值的容器,除 get /set 外没有任何东西,如果 EF 等等的 ORM 它更是自动生成,我认为没必要分离出来,它错的机会低,而且分出来也有相当大的工作量。
再说一篇这只是其中一个方法,再说这做法与外面、书本上说的不一样,有点歪曲了的做法。对我管用。实际用不用,请自行考虑。
采购订单示例,到此结束。往后的是,MVVM 应用时的各种情况,下一篇将会是树结构在 MVVM 的绑定。
树结构很有趣,很多算法都跟它有关,它也随处可见,比如菜单,比如产品结构 BOM 、组织架构等等。虽然有趣,但用起来痛苦,过往你试过写代码历遍 node 找一个元素,然后一堆 boxing / unboxing,你懂的。用 MVVM 会是一个解脱,一部分解脱。下回继续。