Unit testing is quite possibly the single best practice for ensuring that your code is
bug free (or very nearly bug free!). The basic idea is very simple - as you write application code, you
write other test code which exercises the application code to ensure that it is operating correctly.
Sounds simple, and for the most part it is - things get tricky when dealing with interfaces and databinding
but most "standard" objects can be tested very easily - if you use good tools!
Testing Zen
Regardless of the framework you use, the concept is the same. You create a "test" class,
which exposes a set of public methods, which execute the code under test, and report success or failure. The
idea is to build up a suite of tests for all the functions in your application, and then re-run them every time
you do a build. This way you catch any regression very quickly. When you do find a bug in your code, the first
thing you do is create a test which replicates the bug, and then apply the fix. By adding the fix to your test suite
you can be sure that it will always be checked.
Unit Testing Tools
Since unit testing is so widely used, there are a number of tools and frameworks which can help you manage
your testing.
Nunit
The most widely known .NET testing framework is NUnit. This is a port of the popular
Java unit testing tool - Junit. The examples on this page use NUnit, and the book "Test Driven Development in Microsoft .NET"
also uses NUnit.
TestDriven.Net
TestDriven.Net is another toolkit & unit testing framework. While it still operates in the
same way as NUnit, it includes a number of Visual Studio tools, and some extensions which allow for declarative
data driven testing. It costs $95 for the "Professional" version, and $135 for the "Enterprise" version. Although I've never
used it, it is a very higly regarded product,
Visual Studio / MSTest
Visual Studio 2005 now has integrated testing tools. While not exactly compatible with formal "Test Driven Development",
they are quite nice. We are now using this for all our on-going development.
Enough Talk - Examples
The example I'm going to walk through can be downloaded here. This is a pair of Visual Studio 2003 projects, which have references to ArcGIS 9.1
The sample is pretty simple in some respects - I've created a utility assembly for working with geometries. The GeometryOperations class
has a Shared (static) method called SplitPolygon. The code for the method is shown below.
Click to View Code
Public Shared Function SplitPolygon(ByVal polygon As IPolygon, ByVal splitline As IPolyline) As SplitPolygon
If polygon Is Nothing Then
Throw New ArgumentException("Can not split nothing. No polygon passed in.")
End If
If splitline Is Nothing Then
Throw New ArgumentException("No splitline passed in.")
End If
'-----------------------------
'validate the geometries
'-----------------------------
'See if the first and last points of the polyline are outside the polygon
Dim pointCol As IPointCollection
pointCol = splitline
Dim point As IPoint = New point
pointCol.QueryPoint(0, point)
'Test the first point
If geometryRelations.PointIsInPolygon(point, polygon) Then
Throw New ArgumentException("The beginning of the split line is inside the polygon.")
End If
'Test the last point
pointCol.QueryPoint(pointCol.PointCount - 1, point)
If geometryRelations.PointIsInPolygon(point, polygon) Then
Throw New ArgumentException("The end of the split line is inside the polygon.")
End If
'check that the line crosses the polygon
If Not geometryRelations.LineCrossesPolygon(polygon, splitline) Then
Throw New ArgumentException("Split lines does not cross the polygon")
End If
Try
'now we split.
'Create Geometries for the new features
Dim leftPolygon As IGeometry
Dim rightPolygon As IGeometry
'Create topological operator.
Dim pTopo As ITopologicalOperator2 = CType(polygon, ITopologicalOperator2)
'Do the cut.
pTopo.Cut(splitline, leftPolygon, rightPolygon)
'Simplify the geometries
pTopo = leftPolygon
pTopo.Simplify()
pTopo = rightPolygon
pTopo.Simplify()
pTopo = Nothing
Return New SplitPolygon(leftPolygon, rightPolygon)
Catch ex As Exception
Throw ex
End Try
End Function
I'm not going to get into the details of what this code does - suffice to say that
you pass in a polygon and a line, and the polgon is cut into two pieces by the line.
In order to test this code, we must think about all the good iputs, as well as all the bad inputs. We want to be
sure to test not only "ideal" conditions, but also error conditions.
As a bonus this sameple code also contains methods for serializing and de-serializing IGeometry objects to Xml.
Very handy for testing, and for other uses. Have fun with it!
Creating Tests
I tend to put tests in a separate test assembly (ArcDeveloper.Utilities.Tests in this case), but you can put
them where ever you like - some people like to
keep the tests in the same file as the class under test. It really does not matter.
The key to creating tests lies in the use of Attribures. Attributes are used to "decorate" a class/property/method with
extended information that the .NET framework can use. Unit testing frameworks use these attributes when they execute
the test methods.
The code below shows the bare-bones of a test class. Note the <TextFixture()> attribute on the class, and
the <Test()> attribute on the SplitPolygonTest method.
<TestFixture()> _
Public Class PolySplitTest
<Test()> _
Public Sub SplitPolygonTest()
End Sub
End Class
From here, we simply add in some code to actually test our SplitPolygon method on the GeometryOperations class.
Click to View Code
<Test()> _
Public Sub SplitPolygonTest()
Dim cutter As IGeometry = _
GeometryStorage.RetreiveFromXml(GetXML("cutter-geom.xml"))
Dim original As IGeometry = _
GeometryStorage.RetreiveFromXml(GetXML("original-geom.xml"))
Dim goodleft As IGeometry = _
GeometryStorage.RetreiveFromXml(GetXML("good-left-geom.xml"))
Dim goodright As IGeometry = _
GeometryStorage.RetreiveFromXml(GetXML("good-right-geom.xml"))
'Call the function
Dim output As SplitPolygon = _
geometryOperations.SplitPolygon(original, cutter)
'Compare the outputs
Assert.IsTrue(geometryRelations.AreGeometriesSame(output.LeftGeometry, goodleft), _
"Left Geometry was incorrect")
Assert.IsTrue(geometryRelations.AreGeometriesSame(output.RightGeometry, goodright), _
"Right Geometry was incorrect")
End Sub
This first test passes in good data, so the function goes ahead and splits the polygon using the line. but we also
want to test the case where bad data is fed to the function. The "bad data" scenarios are:
- Line does not intersect polygon
- Line entirely inside polygon
- Line ends inside polygon
- Line starts inside polygon
- Line is null
- Polygon is null
Thus, we want to ensure that we have tests which cover each scenario. In the SplitPolygon function, the code
checks for each of these conditions and throws an ArgumentException containing a message explaining the problem.
The code below shows a test written to exercide one of these conditions. Note the Attribute now specifies the
type of exception that is expected.
Click to View Code
<Test(), ExpectedException(GetType(ArgumentException))> _
Public Sub SplitPolygon_LineInsidePoly()
Dim cutter As IGeometry = GeometryStorage.RetreiveFromXml _
(GetXML("line-inside-poly-geom.xml"))
Dim original As IGeometry = GeometryStorage.RetreiveFromXml _
(GetXML("original-geom.xml"))
'Call the function
Dim output As SplitPolygon = _
geometryOperations.SplitPolygon(original, cutter)
End Sub
Running Tests
The NUnit GUI is used to actually execute the tests. This application looks at the assembly that contains
the tests, reads the attributes (via reflection) and presents a list of the tests in the assembly. The user then
selects the test to execute, and the application reports success or failure as Green or Red "lights".
Tests run in NUnit (click to enlarge)
Summary
In my experience, writing tests may take more time during coding, but it saves much more time during debugging.
I highly recommend learning a unit testing framework, and using it as much as possible. This article is a very
high-level introduction to the concepts of unit testing, and while it can get you started, there are a myriad of
other related topics - code coverage, mock objects, continuious integration and "test-driven development" to name a few.
There are also challenges involved in testing interfaces, and data binding - maybe I'll add another article on
these topics if people request it!
Resources: