views:

1098

answers:

13

I've read about unit testing and heard a lot of hullabaloo by others touting its usefulness, and would like to see it in action. As such, I've selected this basic class from a simple application that I created. I have no idea how testing would help me, and am hoping one of you will be able to help me see the benefit of it by pointing out what parts of this code can be tested, and what those tests might look like. So, how would I write unit tests for the following code?

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

}
+4  A: 

You can not properly test this code unless the specification is also given. "Testing" generally means making sure software works as designed.

EDIT: This is really not a "cop out" answer. I've worked as a tester before and I can tell you that almost all of the test cases I wrote were derived straight from the software spec.

BoltBait
Beats one place I worked at, where tests were created by the developer according to the developer's idea of the specification. I always felt like I was cheating by making my own tests.
David Thornley
Making your own tests is not cheating. They say that you understood the specification and were able to break it down to the components that in their entirety are what the consumer of the software wants.
flq
A: 

one example,

for the

public HoleTypes HoleType

test / check for null in the set

Fredou
+6  A: 

Unit Test Example:

  • Verify that PropertyChanged Event is fired with the correct event args. Use reflection in the test to iterate all of the properties setting the values and checking for the event.

Usually this is done with a testing framework such as NUnit.

(Kind of funny cause you will notice that the ParentPattern property doesn't fire the event.)

Gord
Tests using reflection are not so good. For me they hint to some design issues. Beside this, such tests are very likely to break and are quite unclear.
Theo Lenndorff
I'm not a testing pro, but I still can't find a good way to automate via unit tests whether or not an event fires.
SnOrfus
Reflection allows you to forget about this test once you code it. In this example we were testing that an event is fired when a property changes on this entity. Would a new dev think to go back to the test when a new property is added? Is the test still green if it doesn't test all properties?
Gord
http://haacked.com/archive/2006/06/23/UsingRhinoMocksToUnitTestEventsOnInterfaces.aspx is a good start for automating events.Really all you need is to sink on the event with a mock object that will hold onto the event args for later checking
Gord
+3  A: 

Testing is not just engineering -- it's an art. Something that requires you to read. I am not so sure it will be possible for us to teach you through this single question everything you want/need/ought/should/must know. To start off, here's are a few things you can test.

  • Unit (interfaces work as expected)
  • Integration (components behave among themselves)
  • Usability (clients are satisfied)
  • Functional (feature complete)

Define a set of criteria against each (metrics) and start testing.

dirkgently
It is not an art. It may be artistic, but not an art.
OscarRyz
I look at engineering as a fat-free form of art.
dirkgently
Well that may be the reason. In my case I think the art has no other purpose than the "re-creation" the human being. If your code ( or anything else ) main's purpose is other than that, then it is not art ( for me ). For instance testing first objective is validate software. :) :) Peace.
OscarRyz
+1  A: 

Kind of an aside, it seems like most of this class shouldn't need to be tested (other than Gord's answer), if the class was written in a different manner. For example, you are intermixing model information (holetype, etc) with view information (thickness). Also, I think you are missing the point of WPF, and databinding/triggers. UpdateHoleType() Should be expressed in the .xaml file as a set of DataTriggers, and the same with UpdateEntities(), and most of the other properties you have.

FryGuy
+1 for pointing out the merging of "model" and "view" information. (I thought of this too, but not in a MVC fashion.)
strager
+2  A: 

In unit testing, you just test your "visible" methods/properties and not the private ones.

So for instance in your code you can add the following test:

hole.Visible = false;

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

You might think "well thats obvious!" but after a few weeks, you might forget about it. And if some part of your code depends on the value of AbsXDisplay ( which is a public attribute ) and for some reason after you set the property to false, it is no longer "" but "empty" or "NotSet" then this test will fail and you will be notified immediately.

You are supposed to do this for every public method or attribute that have some business rule in it or that affect some other part of your class.

Some people find easier to test first ( and make the test fail ) and then code to satisfy the test, that way you code only what you test, and you test only what you care ( search for TDD )

That was only a simple example of what you can do to test your code.

I hope it helps you to give you a good idea of what the test is all about.

As other have suggested, look for a testing framework.

OscarRyz
There is value in testing private methods. "Glassbox" vs "blackbox". Often you can write a narrower, sharper, simpler, more revealing test by testing the private rather than the public. The glassbox tests must be changed when the private methods change, but that's part of normal refactoring.
Schwern
A: 

Well, the story begins from the theory.

This is what I have done.

First, if you program in OO language learn design patterns. It is easiest if you form a study group and learn together with some friends and colleagues.

Spend several month to easy digest all the patterns.

Then, move to refactoring techniques where you'll learn how to transform any code to code which will use previously learned blocks, e.g. design patterns.

After this preparation, testing will be as easy as any other programming technique.

Boris Pavlović
All but the simplest refactorings REQUIRE testing! And only if you learn patterns as rote exercises would they be restricted to OO languages. That order is horrible. Testing, refactoring and patterns should be learned in parallel with emphasis on testing. OO is in a completely different set.
Schwern
@Schwern You are absolutely right. I was talking strictly about the way I had learned how to test. The problem is with bootstrapping. How to begin? What to learn at first? You have to start from something, don't you
Boris Pavlović
+2  A: 

Here's an example. Keep in mind your sample code lacked definitions for a number of dependencies:

[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);
 }
}

You can see that it is testing that the constructor is producing a valid object (usually not necessary with a full test fixture in place, construction is usually well exercised) and it is also testing the only public method in the class. In this case you would need an event handler delegate and an Assert to check the info parameter's contents.

The goal is to write tests that exercise each method of your class. Usually this includes upper and lower bounds as well as failure conditions.

codeelegance
+4  A: 

I will tell you the great mystery of testing.

When you write a test, you are writing software that checks other software. It checks that your assumptions are true. Your assumptions are simply statements. Here is a dumb simple test that addition works.

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

Those statements, those assertions, must be true or else there is a bug or a feature is missing. This spots bugs faster, before they become hairy, systematic mistakes, before the user sees them. The failure points to a problem that must be solved. Ideally, it also gives you enough information to diagnose the problem. The focused test and the diagnostics makes debugging much faster.

You are writing this software to do your work for you. To do it better than you can. You could test the software by hand, eyeballing the output, but tests once written don't go away. They build and build and build until there is a great mass of them testing for new features, old features, new bugs and old bugs. The task of testing your new code by hand, as well as making sure you haven't reintroduced some old bug, rapidly becomes overwhelming. A human will simply stop testing for the old bugs. They will be reintroduced and time will be wasted. A test program can do all this for you at the push of a button. It is a boring, rote task. Humans suck at them, this is why we invented computers. By writing software to test your software you are using the computer for what it was intended: saving time.

I put it in such simplistic terms because people who are new to testing are often overwhelmed. They think there's some magic. Some special framework they have to use. They often even forget that tests are still programs and suddenly cannot think to use a loop or write a subroutine. There is much, much more to learn, but hopefully this will give you a kernel around which to figure out what this "test" thing is.

Schwern
A bit too much rambling, but still a good answer.
strager
A: 

We must test it, right?

Tests are validation that the code works as you expect it to work. Writing tests for this class right now will not yield you any real benefit (unless you uncover a bug while writing the tests). The real benefit is when you will have to go back and modify this class. You may be using this class in several different places in your application. Without tests, changes to the class may have unforseen reprecussions. With tests, you can change the class and be confident that you aren't breaking something else if all of your tests pass. Of course, the tests need to be well written and cover all of the class's functionality.

So, how to test it?

At the class level, you will need to write unit tests. There are several unit testing frameworks. I prefer NUnit.

What am I testing for?

You are testing that everything behaves as you expect it to behave. If you give a method X, then you expect Y to be returned. In Gord's answer, he suggested testing that your event actually fires off. This would be a good test.

The book, Agile Principles, Patterns, and Practices in C# by Uncle Bob has really helped me understand what and how to test.

Aaron Daniels
+1  A: 

By testing I assume you mean test driven design. Test driven design mostly concerns itself with unit tests and sometimes integration tests. Unit tests test the smallest code testable code elements and integrations tests test the interaction between components with your application.

There are many more forms of testing, but these are the ones developers usually do themselves. The other tests mostly look at the the application from without and test user interfaces exposed for various qualities as correctness, performance and scalability.

Unit testing involves testing your methods to see if they do what you want them to. These are usually test so simple you would almost think them trivial. The thing you are looking to test is the logic of your class. The class you provides does not really have all that much logic.

Only the private void UpdateHoleType(){...} contains any logic it seem to be visually oriented logic, always the hardest to test. Writing a tests is very simple. Below is an example for the drilled holetype.

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

If you look at it, you would almost not consider it worth it. The test is trivial and obvious. The [Test] attribute declare the method a test and the Assert.AreEquals() method throws an exception if the provided values are not equal. The actual construct may vary depending on the test framework used but they are all equally simple.

The trick here is you write these methods for all methods in your class performing business logic and test a number of values. null is always a good value to try.

The power of unit testing is in the combination of all those tests. Now if you change something in the class there is a number of tests checking if a change you made, breaks one of the behaviors you have defined in your test. This allows you to work with a larger project, changing and implementing new features while the tests preserve the functionality you have already coded.

DefLog
+1  A: 

Tests will help, if you need to make changes.

According to Feathers (Feathers, Working Effectively with Legacy Code, p. 3) there are four reasons for changes:

  • Adding a feature
  • Fixing a bug
  • Improving design
  • Optimizing resource usage

When there is the need for change, you want to be confident that you don't break anything. To be more precise: You don't want to break any behavior (Hunt, Thomas, Pragmatic Unit Testing in C# with NUnit, p. 31).

With unit testing in place you can do changes with much more confidence, because they would (provided they are programmed properly) capture changes in behavior. That's the benefit of unit tests.

It would be difficult to make unit tests for the class you gave as an example, because unit tests also requires a certain structure of the code under test. One reason I see is that the class is doing too much. Any unit tests you will apply on that class will be quite brittle. Minor change may make your unit tests blow up and you will end up wasting much time with fixing problems in your test code instead of your production code.

To reap the benefits of unit tests requires to change the production code. Just applying unit tests principles, without considering this will not give you the positive unit testing experience.

How to get the positive unit testing experience? Be openminded for it and learn.

I would recommend you Working Effectively with Legacy Code for an existing code basis (as that piece of code you gave above). For an easy kick start into unit testing try Pragmatic Unit Testing in C# with NUnit. The real eye opener for me was xUnit Test Patterns: Refactoring Test Code.

Good luck in you journey!

Theo Lenndorff
A: 

In terms of the Notify event firing you should certainly ensure whether your class works according to spec, namely that:

  • Parent will never fire regardless of the value set
  • StrokeColour and StrokeThickness always fire the event, even though the same value is set
  • CanvasX/Y, HoleType/Dia only fire when a value different than the previous one is set

Then you want to check a couple of side effects that setting your properties cause. After that you could think about refactoring the thing because, dang, this ain't a pretty class!

flq