练习示例测试C#代码

时间:2009-03-05 20:08:50

标签: c# unit-testing

我读过有关单元测试的内容,并听到其他人宣传它的实用性,并希望看到它的实际应用。因此,我从我创建的一个简单应用程序中选择了这个基本类。我不知道测试会如何帮助我,我希望你们中的一个能够通过指出可以测试这些代码的哪些部分以及这些测试的外观来帮助我看到它的好处。那么,我将如何编写以下代码的单元测试?

public class Hole : INotifyPropertyChanged
{
    #region Field Definitions
    private double _AbsX;
    private double _AbsY;
    private double _CanvasX { get; set; }
    private double _CanvasY { get; set; }
    private bool _Visible;
    private double _HoleDia = 20;
    private HoleTypes _HoleType;
    private int _HoleNumber;
    private double _StrokeThickness = 1;
    private Brush _StrokeColor = new SolidColorBrush(Colors.Black);
    private HolePattern _ParentPattern;
    #endregion

    public enum HoleTypes { Drilled, Tapped, CounterBored, CounterSunk };
    public Ellipse HoleEntity = new Ellipse();
    public Ellipse HoleDecorator = new Ellipse();
    public TextBlock HoleLabel = new TextBlock();

    private static DoubleCollection HiddenLinePattern = 
               new DoubleCollection(new double[] { 5, 5 });

    public int HoleNumber
    {
        get
         {
            return _HoleNumber;
         }
        set
        {
            _HoleNumber = value;
            HoleLabel.Text = value.ToString();
            NotifyPropertyChanged("HoleNumber");
        }
    }
    public double HoleLabelX { get; set; }
    public double HoleLabelY { get; set; }
    public string AbsXDisplay { get; set; }
    public string AbsYDisplay { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;
    //public event MouseEventHandler MouseActivity;

    // Constructor
    public Hole()
    {
        //_HoleDia = 20.0;
        _Visible = true;
        //this.ParentPattern = WhoIsTheParent;
        HoleEntity.Tag = this;
        HoleEntity.Width = _HoleDia;
        HoleEntity.Height = _HoleDia;

        HoleDecorator.Tag = this;
        HoleDecorator.Width = 0;
        HoleDecorator.Height = 0;


        //HoleLabel.Text = x.ToString();
        HoleLabel.TextAlignment = TextAlignment.Center;
        HoleLabel.Foreground = new SolidColorBrush(Colors.White);
        HoleLabel.FontSize = 12;

        this.StrokeThickness = _StrokeThickness;
        this.StrokeColor = _StrokeColor;
        //HoleEntity.Stroke = Brushes.Black;
        //HoleDecorator.Stroke = HoleEntity.Stroke;
        //HoleDecorator.StrokeThickness = HoleEntity.StrokeThickness;
        //HiddenLinePattern=DoubleCollection(new double[]{5, 5});
    }

    public void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, 
                       new PropertyChangedEventArgs(info));
        }
    }

    #region Properties
    public HolePattern ParentPattern
    {
        get
        {
            return _ParentPattern;
        }
        set
        {
            _ParentPattern = value;
        }
    }

    public bool Visible
    {
        get { return _Visible; }
        set
        {
            _Visible = value;
            HoleEntity.Visibility = value ? 
             Visibility.Visible : 
             Visibility.Collapsed;
            HoleDecorator.Visibility = HoleEntity.Visibility;
            SetCoordDisplayValues();
            NotifyPropertyChanged("Visible");
        }
    }

    public double AbsX
    {
        get { return _AbsX; }
        set
        {
            _AbsX = value;
            SetCoordDisplayValues();
            NotifyPropertyChanged("AbsX");
        }
    }

    public double AbsY
    {
        get { return _AbsY; }
        set
        {
            _AbsY = value;
            SetCoordDisplayValues();
            NotifyPropertyChanged("AbsY");
        }
    }

    private void SetCoordDisplayValues()
    {
        AbsXDisplay = HoleEntity.Visibility == 
        Visibility.Visible ? String.Format("{0:f4}", _AbsX) : "";
        AbsYDisplay = HoleEntity.Visibility == 
        Visibility.Visible ? String.Format("{0:f4}", _AbsY) : "";
        NotifyPropertyChanged("AbsXDisplay");
        NotifyPropertyChanged("AbsYDisplay");
    }

    public double CanvasX
    {
        get { return _CanvasX; }
        set
        {
            if (value == _CanvasX) { return; }
            _CanvasX = value;
            UpdateEntities();
            NotifyPropertyChanged("CanvasX");
        }
    }

    public double CanvasY
    {
        get { return _CanvasY; }
        set
        {
            if (value == _CanvasY) { return; }
            _CanvasY = value;
            UpdateEntities();
            NotifyPropertyChanged("CanvasY");
        }
    }

    public HoleTypes HoleType
    {
        get { return _HoleType; }
        set
        {
            if (value != _HoleType)
            {
                _HoleType = value;
                UpdateHoleType();
                NotifyPropertyChanged("HoleType");
            }
        }
    }

    public double HoleDia
    {
        get { return _HoleDia; }
        set
        {
            if (value != _HoleDia)
            {
                _HoleDia = value;
                HoleEntity.Width = value;
                HoleEntity.Height = value;
                UpdateHoleType(); 
                NotifyPropertyChanged("HoleDia");
            }
        }
    }

    public double StrokeThickness
    {
        get { return _StrokeThickness; }
        //Setting this StrokeThickness will also set Decorator
        set
        {
            _StrokeThickness = value;
            this.HoleEntity.StrokeThickness = value;
            this.HoleDecorator.StrokeThickness = value;
            NotifyPropertyChanged("StrokeThickness");
        }
    }

    public Brush StrokeColor
    {
        get { return _StrokeColor; }
        //Setting this StrokeThickness will also set Decorator
        set
        {
            _StrokeColor = value;
            this.HoleEntity.Stroke = value;
            this.HoleDecorator.Stroke = value;
            NotifyPropertyChanged("StrokeColor");
        }
    }

    #endregion

    #region Methods

    private void UpdateEntities()
    {
        //-- Update Margins for graph positioning
        HoleEntity.Margin = new Thickness
        (CanvasX - HoleDia / 2, CanvasY - HoleDia / 2, 0, 0);
        HoleDecorator.Margin = new Thickness
        (CanvasX - HoleDecorator.Width / 2, 
         CanvasY - HoleDecorator.Width / 2, 0, 0);
        HoleLabel.Margin = new Thickness
        ((CanvasX * 1.0) - HoleLabel.FontSize * .3, 
         (CanvasY * 1.0) - HoleLabel.FontSize * .6, 0, 0);
    }

    private void UpdateHoleType()
    {
        switch (this.HoleType)
        {
            case HoleTypes.Drilled: //Drilled only
                HoleDecorator.Visibility = Visibility.Collapsed;
                break;
            case HoleTypes.Tapped: // Drilled & Tapped
                HoleDecorator.Visibility = (this.Visible == true) ? 
                Visibility.Visible : Visibility.Collapsed;
                HoleDecorator.Width = HoleEntity.Width * 1.2;
                HoleDecorator.Height = HoleDecorator.Width;
                HoleDecorator.StrokeDashArray = 
                LinePatterns.HiddenLinePattern(1);
                break;
            case HoleTypes.CounterBored: // Drilled & CounterBored
                HoleDecorator.Visibility = (this.Visible == true) ? 
                Visibility.Visible : Visibility.Collapsed;
                HoleDecorator.Width = HoleEntity.Width * 1.5;
                HoleDecorator.Height = HoleDecorator.Width;
                HoleDecorator.StrokeDashArray = null;
                break;
            case HoleTypes.CounterSunk: // Drilled & CounterSunk
                HoleDecorator.Visibility = (this.Visible == true) ? 
                Visibility.Visible : Visibility.Collapsed;
                HoleDecorator.Width = HoleEntity.Width * 1.8;
                HoleDecorator.Height = HoleDecorator.Width;
                HoleDecorator.StrokeDashArray = null;
                break;
        }
        UpdateEntities();
    }

    #endregion

}

13 个答案:

答案 0 :(得分:6)

单元测试示例:

  • 验证是否使用正确的事件args触发了PropertyChanged事件。 在测试中使用反射来迭代设置值并检查事件的所有属性。

通常,这是通过NUnit等测试框架完成的。

(有趣的是,您会注意到ParentPattern属性不会触发事件。)

答案 1 :(得分:4)

除非同时给出规范,否则无法正确测试此代码。 “测试”通常意味着确保软件按设计工作。

编辑:这真的不是“警察出局”的答案。我以前做过测试员,我可以告诉你,我写的几乎所有测试用例都是直接来自软件规范。

答案 2 :(得分:4)

我会告诉你测试的神秘之处。

编写测试时,您正在编写检查其他软件的软件。它会检查您的假设是否正确。你的假设只是陈述。这是一个愚蠢的简单测试,加法有效。

if( 1 + 1 == 2 ) {
    print "ok - 1 plus 1 equals 2\n";
}
else {
    print "not ok\n";
}

那些声明,那些断言,必须是真的,否则就会丢失一个错误或功能。在用户看到之前,这些斑点会在它们变成毛茸茸的系统性错误之前更快地发生错误。失败指向必须解决的问题。理想情况下,它还为您提供足够的信息来诊断问题。重点测试和诊断使调试更快。

您正在编写此软件以便为您完成工作。要做得比你好。您可以手动测试软件,注视输出,但一旦编写的测试不会消失。他们构建,构建和构建,直到有大量的测试新功能,旧功能,新bug和旧bug。手动测试新代码的任务,以及确保你没有重新引入一些旧的bug,很快就变得势不可挡。一个人只会停止测试旧的错误。它们将被重新引入,浪费时间。只需按一下按钮,测试程序就可以为您完成所有这些工作。这是一项无聊的死记硬背的任务。人类嘲笑他们,这就是我们发明计算机的原因。通过编写软件来测试您的软件,您正在使用计算机实现预期目的:节省时间。

我用简单的术语来说,因为刚接触测试的人经常不知所措。他们认为有一些魔力。他们必须使用一些特殊的框架。他们甚至经常忘记测试仍然是程序,突然想不到使用循环或编写子程序。还有很多东西需要学习,但希望这会给你一个核心来解决这个“测试”的东西。

答案 3 :(得分:2)

测试不只是工程 - 它是一门艺术。需要你阅读的东西。我不太确定我们能够通过这个单一的问题教你一切你想要/需要/应该/应该/必须知道的事情。首先,这里有一些你可以测试的东西。

  • 单位(界面按预期工作)
  • 集成(组件之间的行为)
  • 可用性(客户满意)
  • 功能(功能完整)

针对每个(指标)定义一组标准并开始测试。

答案 4 :(得分:2)

在单元测试中,您只需测试“可见”方法/属性,而不是私有方法/属性。

因此,例如在您的代码中,您可以添加以下测试:

hole.Visible = false;

Debug.Assert( "".Equals( hole.AbsXDisplay ) );
Debug.Assert( "".Equals( hole.AbsYDisplay ) );

您可能会认为“那么显而易见!”但几周后,你可能会忘记它。如果代码的某些部分依赖于 AbsXDisplay (这是一个公共属性)的值,并且在将属性设置为false后出于某种原因,它不再是“”而是“空”或“NotSet”然后此测试将失败,您将立即收到通知。

您应该为每个公共方法或属性执行此操作,这些方法或属性中包含一些业务规则或影响类的其他部分。

有些人发现首先更容易测试(并使测试失败)然后编码以满足测试,这样您只编码测试的内容,并且只测试您关心的内容(搜索TDD)

这只是测试代码的一个简单示例。

我希望它可以帮助您更好地了解测试的全部内容。

正如其他人所建议的那样,寻找一个测试框架。

答案 5 :(得分:1)

除此之外,如果课程以不同的方式编写,似乎大多数课程都不需要进行测试(Gord's answer除外)。例如,您正在将模型信息(holetype等)与视图信息(厚度)混合。另外,我认为你忽略了WPF和数据绑定/触发器的重点。 UpdateHoleType()应该在.xaml文件中表示为一组DataTriggers,与UpdateEntities()和大多数其他属性相同。

答案 6 :(得分:1)

这是一个例子。请记住,您的示例代码缺少许多依赖项的定义:

[TestFixture()]
public class TestHole 
{

    private Hole _unitUnderTest;

    [SetUp()]
    public void SetUp() 
    {
        _unitUnderTest = new Hole();
    }

    [TearDown()]
    public void TearDown() 
    {
        _unitUnderTest = null;
    }

    [Test]
    public void TestConstructorHole()
    {
        Hole testHole = new Hole();
        Assert.IsNotNull(testHole, "Constructor of type, Hole failed to create instance.");
    }

    [Test]
    public void TestNotifyPropertyChanged()
    {
        string info = null;
        _unitUnderTest.NotifyPropertyChanged(info);
    }
}

你可以看到它正在测试构造函数是否正在生成一个有效的对象(通常不需要使用完整的测试夹具,通常可以很好地构造构造),并且它还测试了类中唯一的公共方法。在这种情况下,您需要一个事件处理程序委托和一个Assert来检查info参数的内容。

目标是编写练习课程每个方法的测试。通常这包括上限和下限以及失败条件。

答案 7 :(得分:1)

通过测试,我认为你的意思是测试驱动设计。测试驱动设计主要关注单元测试和有时集成测试。单元测试测试最小代码可测试代码元素和集成测试测试组件与应用程序之间的交互。

还有更多形式的测试,但这些是开发人员通常自己做的。其他测试主要关注来自without的应用程序,并测试用户界面是否具有正确性,性能和可伸缩性等各种质量。

单元测试涉及测试您的方法,看看它们是否符合您的要求。这些通常是如此简单的测试,你几乎认为它们是微不足道的。你想要测试的是你班级的逻辑。你提供的课程并没有那么多逻辑。

只有private void UpdateHoleType(){...}包含任何逻辑,它似乎是视觉导向的逻辑,总是最难测试。编写测试非常简单。以下是钻孔类型的示例。

[Test]
public void testDrilledHole()
{
  Hole hole = new Hole();
  hole.HoleType = HoleTypes.Drilled;
  Assert.AreEqual(Visibility.Collapsed, hole.HoleDecorator.Visibility);
}

如果你看一下,你几乎不会认为值得。这项测试非常简单明了。 [Test]属性将方法声明为测试,如果提供的值不相等,则Assert.AreEquals()方法会抛出异常。实际构造可能会根据使用的测试框架而有所不同,但它们都同样简单。

这里的技巧是为类中的所有方法编写这些方法,执行业务逻辑并测试许多值。 null总是值得尝试。

单元测试的力量是所有这些测试的结合。现在,如果您在课程中更改了某些内容,则会检查您是否进行了更改,从而中断了您在测试中定义的行为之一。这使您可以使用更大的项目,更改和实现新功能,同时测试保留您已编码的功能。

答案 8 :(得分:1)

如果您需要进行更改,测试会有所帮助。

根据Feathers(Feathers,Working Effectively with Legacy Code,第3页),有四个原因需要改变:

  • 添加功能
  • 修复错误
  • 改善设计
  • 优化资源使用

当需要改变时,你要确信你不会破坏任何东西。更确切地说:你不想破坏任何行为(Hunt,Thomas,Pragmatic Unit Testing in C# with NUnit,第31页。)

通过单元测试,您可以更自信地进行更改,因为他们会(如果编程正确)捕获行为的变化。这是单元测试的好处。

对于您提供的类作为示例进行单元测试很难,因为单元测试还需要测试代码的某种结构。我看到的一个原因是课程做得太多了。您将在该课程中应用的任何单元测试都会非常脆弱。轻微的更改可能会使您的单元测试爆炸,最终会浪费很多时间来修复测试代码中的问题,而不是生产代码。

要获得单元测试的好处,需要更改生产代码。只是应用单元测试原则,而不考虑这将不会给你积极的单元测试经验。

如何获得积极的单元测试体验? 为此开放并学习。

我建议您Working Effectively with Legacy Code获取现有代码(如上面提供的那段代码)。要轻松启动单元测试,请尝试Pragmatic Unit Testing in C# with NUnit。真正令我大开眼界的是xUnit Test Patterns: Refactoring Test Code

祝你好运!

答案 9 :(得分:0)

一个例子,

用于

public HoleTypes HoleType

在集合

中测试/检查null

答案 10 :(得分:0)

  

我们必须测试一下,对吧?

测试验证代码是否按预期工作。现在为这个类编写测试不会给你带来任何实际好处(除非你在编写测试时发现错误)。真正的好处是你必须返回并修改这个类。您可能在应用程序的几个不同位置使用此类。如果没有测试,班级的变化可能会有不可预见的代表。通过测试,您可以更改课程,并确信如果所有测试都通过,您不会破坏其他内容。当然,测试需要写得很好并涵盖了所有类的功能。

  

那么,如何测试呢?

在课程级别,您需要编写单元测试。有几个单元测试框架。我更喜欢NUnit

  

我在测试什么?

您正在测试所有内容的行为与您期望的行为相同。如果给出方法X,那么您希望返回Y.在Gord's回答中,他建议测试您的活动实际上是否会被触发。这将是一个很好的测试。

鲍勃叔叔的书Agile Principles, Patterns, and Practices in C#确实帮助我理解了测试的内容和方法。

答案 11 :(得分:0)

就Notify事件触发而言,您当然应该确保您的班级是否按照规范运作,即:

  • 无论值集合
  • ,父母都不会开火
  • StrokeColour和StrokeThickness总是触发事件,即使设置了相同的值
  • CanvasX / Y,HoleType / Dia仅在设置了与前一个值不同的值时触发

然后,您要检查设置属性的两个副作用。 在那之后你可以考虑重构这个东西,因为, dang,这不是一个漂亮的课程!

答案 12 :(得分:-1)

嗯,故事从理论开始。

这就是我所做的。

首先,如果你用OO语言编程学习设计模式。如果你组建一个学习小组并与一些朋友和同事一起学习,这是最简单的。

花几个月时间轻松消化所有模式。

然后,转向重构技术,您将学习如何将任何代码转换为将使用以前学过的块的代码,例如:设计模式。

在准备之后,测试将像任何其他编程技术一样简单。