Firstly, NUnit tests both speed up and verify development. They are
written for 2 main reasons (in my opinion).
1) To verify that the code written corresponds to what is expected
2) To catch regressions which may happen due to optimisations
introduced later in the projects lifespan.
As such, they are *very* important. It is much easier to click on a
single button to see if your code works rather than do a lot of painful
checking by hand. It allows you to rip apart a whole algorithm,
reimplement it and then be fairly certain that your changes broke
nothing.
Now, all code that is being submitted to the XNA project should really
come with NUnit tests. Some people are very familiar with writing these
tests, others aren't. I'd classify myself into the second group ;) So
don't take what i write as gospel truth, just as one way of writing the
tests.
As I said, the primary reason for writing the NUnit tests is to make
sure that our code does what is expected. What we expect is that our
code's behaviour will match MS XNA's behaviour. So the *only* way to
write these tests is to be able to load up a copy of the MS libraries
and check the behaviour of their libraries under a set of tests (all
tests should pass under the MS libraries) and then see if the same
tests pass on Mono.XNA. If they don't, we have a bug.
Be warned, you can't assume anything! There are places where passing in
"null" returns a null reference exception instead of an
ArgumentNullException. There are places where cloning an object will
result in "originalObject == clonedObject" returning false and other
cases where it'll return true. So you need to test everything.
What i generally do is go through *every* method available in my chosen
class and check the following (where applicable):
1) What happens if you pass in null.
2) What happens when you pass in a "wrong" type. i.e. passing an "int"
into Equals(object o) when you know it should be a CurveKey.
3) what happens if you pass in invalid values. i.e. if you know your
range of values goes between 1 and 10, what happens if you put in -1 or
1,000,000? Or passing in 0 where you know division will be happening.
4) Check boundary conditions (where possible). For example if you're
trying to see if a BoundingBox contains a BoundingSphere, what is the
output if the edge of the box touches the very edge of the sphere? Does
that count as Intersecting or does that count as Disjoint?
5) Write down the result of each method when run under the MS libraries
and then use that information to write the Tests so that they check
that we give the same result in Mono.XNA.
So, in order to do this, you do the following (assuming you have no
knowledge of NUnit):
1) You have a [Setup] attribute. Before each and every test is run, all
methods marked with [Setup] will be run. For example in CurveKey i
initialise my variables with specific values:
CurveKey c1;
CurveKey c2;
[SetUp]
public void Setup()
{
c1 = new CurveKey(1f, 2f, 3f, -3f, CurveContinuity.Smooth);
c2 = new CurveKey(2f, 4f, 6f, 6.7f, CurveContinuity.Step);
}
This way i know that at the start of every test method, c1 and c2 will
have those exact values. That way if in Test A i change the value of c1
to be null, then when TestB runs, c1 will be reinitialised back to the
value i want it to be.
2) Go through every method available (including Equals and ToString)
with the exception of GetHashCode and create a stub to test that
method. I.e. i'd have stubs like:
[Test]
public void Clone()
{
}
[Test]
public void CompareTo()
{
}
3) Into each of these stubs, you will write a series of assertions to
check the various different ways of using that particular method.
Taking "Equals" as my example, i wrote the following test:
[Test]
public void TestEquals()
{
CurveKey clone1 = new CurveKey(1f, 2f, 3f, -3f,
CurveContinuity.Smooth);
Assert.IsTrue(c1.Equals(c1), "#1");
Assert.IsTrue(c1.Equals(clone1), "#2");
Assert.IsFalse(c1.Equals(null), "#3");
Assert.IsFalse(c1.Equals(c2), "#5");
Assert.IsTrue(c1 == c1, "#6");
Assert.IsTrue(c1 == clone1, "#7");
Assert.IsFalse(c1 != c1, "#8");
Assert.IsFalse(c1 != clone1, "#9");
Assert.IsFalse(c1 == c2, "#10");
Assert.IsTrue(c1 != c2, "#11");
}
I start off by assuming that these Assertions are all correct.
Logically, you'd assume that c1 will always equal c1 (if not, then
there's a bug somewhere). Clone1 is a new instance of a CurveKey with
the exact same values as c1. You'd assume that calling
c1.Equals(clone1) should be true, so i write the test assuming that.
Now, i also need to test that c1 does NOT equal null. The reason for
this is that false should be returned from the call to Equals, but if
the method is implemented incorrectly, an exception will be thrown. It
should also be obvious that c1 shouldn't ever equal 2, so i write a
test for that too. That's the .Equals method tested completely in every
imaginable way.
BUT, i also need to test == and != aswell, just in case there's a bug
in their implementations. So i also wrote the extra tests checking to
see if these work.
Once that's done, run the tests under the MS libraries, and if any of
the tests fail, make sure that you haven't made a stupid mistake
writing the tests. If you haven't, maybe you assumed the wrong thing.
For example where i wrote: Assert.IsTrue(c1.Equals(clone1), "#2"); it
could be that MS XNA is actually doing a reference equals test, so that
should actually be false. In that case, i'd need to change that line to
"Assert.IsFalse(c1.Equals(clone1), "#2");" as i know it shouldn't be
true. That kind of thing you'll only find out by running the test with
the MS libraries.
While you might look at it and say that a lot of the tests are
redundant. You might think that there's no need to call c1.Equals(c1)
as it will always be true. That's wrong. c1 won't equal c1 if there's a
bug in your code! Thats exactly the kind of thing we need to write a
test for.
Please note that at the end of each Assert theres a string containing a
unique identifier for the test. I've given them numbers. This should be
done for each assert in each test so that if a test fails, its easy to
tell which specific Assert failed in the method. That way, when running
the tests, if == fails, then i'll get a message telling me that "#6"
failed, so i know exactly what went wrong.
Any questions or additions or clarifications to this would be great. If
people are happy with it, i'll eventually put it in a doc that goes
into the SVN.