如何对Windows Forms应用程序进行单元测试建议

34
我编写了一个Windows Forms应用程序,现在我想为它编写一些单元测试(虽然不完全是TDD,但总比没有好!)。 我的问题是,对于这样的应用程序,如何编写单元测试,因为几乎所有的方法和事件都是私有的?我听说过NUnit Forms,但是我听到的好坏不一,而且该项目已经停滞了一段时间,看起来已经被放弃。此外,如果我仅为用户点击/按下按钮触发的所有事件编写了单元测试用例,那么是否可以认为该项目有足够的单元测试?还是我必须编写单元测试用例来测试所有方法,并找出测试私有方法的方法?
编辑:我的业务逻辑与显示逻辑分开,我的业务逻辑有1或2个公共方法可供窗体访问,但是关于业务逻辑中的所有私有方法呢?
6个回答

43

单元测试图形应用程序的关键是确保所有业务逻辑都在一个单独的类中,而不是在代码后面。

设计模式,如模型视图展示器模型视图控制器,在设计这样的系统时可以提供帮助。

举个例子:

public partial class Form1 : Form, IMyView
{
    MyPresenter Presenter;
    public Form1()
    {
        InitializeComponent();
        Presenter = new MyPresenter(this);
    }

    public string SomeData
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            MyTextBox.Text = value;
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Presenter.ChangeData();
    }
}

public interface IMyView
{
    string SomeData { get; set; }
}

public class MyPresenter
{
    private IMyView View { get; set; }
    public MyPresenter(IMyView view)
    {
        View = view;
        View.SomeData = "test string";
    }

    public void ChangeData()
    {
        View.SomeData = "Some changed data";
    }
}

如你所见,这个表单只有一些基础设施代码来将所有东西联系在一起。你的所有逻辑都在你的Presenter类中,它只知道一个View接口。
如果你想对此进行单元测试,你可以使用一个模拟工具,比如Rhino Mocks来模拟View接口,并将其传递给你的Presenter。
[TestMethod]
public void TestChangeData()
{
    IMyView view = MockRepository.DynamickMock<IMyView>();
    view.Stub(v => v.SomeData).PropertyBehavior();

    MyPresenter presenter = new MyPresenter(view);

    presenter.ChangeData();

    Assert.AreEqual("Some changed data", view.SomeData);
}

24
首先,我会确保您的业务逻辑与表单有适当的分离,基本上使用MVC模式。然后,您可以轻松地在表单之外测试所有内容,就好像表单根本不存在一样。
现在,这可能仍然会留下一些未经测试的特定于表单的功能。例如,表单是否正确连接到服务?对于此问题,您仍然可以考虑使用NUnit Forms或其他替代方案。

好的,我明白了。但是如果我的业务逻辑中有1或2个公共方法供我的表单访问,并且这些方法又访问业务逻辑中的私有方法,那么仅测试业务逻辑中与表单公共接口相关的部分就足够了吗?而不需要编写直接测试任何私有方法的单元测试吗? - DukeOfMarmalade
@Jim - 是的,许多人认为这是可以接受的,因为测试公共方法就是在执行私有方法中的代码。这与删除私有方法并将其代码移动到公共方法的内部块中并没有太大区别。 - ziesemer
单独使用MVC不能确保其想要测试的内容得到测试。他可以实现MVC模式,但如果控制器中的业务逻辑在Form项目内部,则仍然是内部的。如果将其移动到外部项目中,则原本希望保护的内容现在会变成公共的,因此不再是“受保护”的,即使没有完全保密的内容。 - Lord of Scripts

6

将所有业务逻辑拆分为单独的项目,并对其进行单元测试。或者至少将所有表单中的逻辑移动到单独的类中。


6
你有几个选择。
1. 使用像Coded UI这样的工具通过用户界面进行测试。这不是一个很好的选择,因为它比单元测试慢,而且测试往往更加脆弱。
2. 将业务逻辑与演示逻辑分离。如果您在UI中有许多执行业务逻辑的私有方法,那么您已经将业务逻辑紧密耦合到演示中。开始识别这些并将它们移动到具有公共接口的独立类中以进行测试。阅读SOLID原则,它可以帮助您保持代码松散耦合和可测试性。

关于 #1:它不仅因为速度慢而不是一个好选择,而且它甚至不是单元测试。UI测试就是那样,它更接近于集成测试。关于 #2:不仅如果你有“很多”私有方法,而是如果你有任何执行业务逻辑的私有方法,你已经使你的代码耦合了。耦合就是耦合,它不应该有一个尺度。除此之外,#2 完全正确。 - Suamere

2

使用approvaltests(www.approvaltests.com或nuget)对View进行单元测试非常简单。这里有一个视频:http://www.youtube.com/watch?v=hKeKBjoSfJ8

然而,似乎您也担心将函数设置为默认或公共以便能够测试功能。

这些通常被称为接缝;用于测试代码的方式。它们很好。有时人们会混淆私有/公共和安全性,并害怕将私有函数公开,但是反射将调用任何一个,因此它并不真正安全。其他时候,人们担心类的API接口。但是,只有当您拥有公共API时才会有影响,如果您有一个winform应用程序,则可能意味着它是顶层(没有其他消费者在调用它)。

您是程序员,因此可以设计易于测试的代码。这通常意味着仅需将几个方法更改为公共方法并创建一些方便的方法即可传递依赖项。

例如:

buttonclick += (o,e)=> {/*somecode*/};

很难测试。

private void button1_Click(object sender, EventArgs e) {/*somecode*/}

仍然很难测试

public void button1_Click(object sender, EventArgs e) {/*somecode*/}

更容易测试
private void button1_Click(object sender, EventArgs e) { DoSave();}
public void DoSave(){/*somecode*/}

非常容易测试!

如果您需要从事件中获取一些信息,这将变得更加容易。例如:

public void ZoomInto(int x, int y)

相比于对应的鼠标点击事件,测试它要容易得多,而且透传调用仍然可以是一个可以忽略的单行代码。


1
有些人会混淆忽略不良原因的原因和做正确事情的原因。封装不仅仅是为了安全。一旦程序员认为公开本应封装的成员变量是可以接受的,他们就失去了关注点分离和稳健开发实践的时间。现在你有一个可能没有失去安全性,但难以维护,并且采用测试工作区而不是正确执行测试的应用程序。 - Suamere
1
当私有变为公有时,事情就更难维护了...我经常想知道这对于那些没有public/private的语言如Ruby/Javascript等是否适用... - llewellyn falco
2
选择编程语言时,你需要考虑到它能在哪些硬件上运行、代码维护者的实力以及语言本身的特性。C# 的主要优势包括类型安全、垃圾回收、泛型、友好的本地结构等等。仅因为另一种语言(具有其自身的优势)与 C# 之间存在差异不意味着该评论无关紧要。JavaScript 的主要优点是它不是类型安全的,这是解决问题的最佳方式。此外,你可以很好地在 JavaScript 中拥有私有成员。 - Suamere
1
重点是:如果你正在使用C#,请使用C#面向对象的技术进行编程。是的,你可以使用过程化或非封装方法来廉价地解决问题。或者,你可以找出使用C#的方法来解决它。不要犯认为所有语言都是相同的错误。 - Suamere
3
与上面的评论一样,这是一种不好的做法,会很快导致难以维护的代码。仅为了测试目的而将某个项公开永远都不是一个好主意。更好的替代方案是使用内部访问修饰符和InternalsVisibleTo属性,将这些项暴露给你的测试。 - Christopher Berman
以上只是教条主义。对于同一程序集中的代码,internal 和 public 一样好用,而且既然我们正在谈论一个只有单元测试会引用的顶级程序集,除了“纯洁性”之外,没有真正的优势来使它们成为 internal。如果这是一个类库,使用 internal 和 InternalsVisibleTo 将是值得使用的。 - Jessica

1

人们可以使用MVVM(Model-View-ViewModel)模式与Reactive.UI来编写可测试的WinForms代码,以获得真正需要的关注点分离。请参见:Reactive.UI https://reactiveui.net/ 使用Winforms/MVVM/Reactive.UI的主要缺点是没有太多的使用示例(用于WinForms)。好处在于它适用于几乎所有桌面框架和语言。您可以为一种语言学习它,但其原则适用于所有语言。当你有很多私有方法时,这没关系。个人建议:尝试使用公共方法开始要测试的业务过程。您可以使用“告知,不要询问”:https://martinfowler.com/bliki/TellDontAsk.html,同时保持所有这些方法为私有。

一种测试代码的方法是通过驱动UI进行测试,但这并不是非常推荐的,因为结果测试非常脆弱、难以实现,并且不能像纯代码测试那样精细地编写;最后,如果您使用数据库,您需要考虑用测试数据填充它,并且由于每次测试前必须将数据库置于干净、定义明确的状态,所以您的测试可能比您想象的运行得更慢,因为您需要重新初始化每个测试的数据。
总结:使用良好的SoC(例如应用MVVM)编写代码,然后您的代码将具有更好的可测试性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接