views:

155

answers:

4

Being new to test based development, this question has been bugging me. How much is too much? What should be tested, how should it be tested, and why should it be tested? The examples given are in C# with NUnit, but I assume the question itself is language agnostic.

Here are two current examples of my own, tests on a generic list object (being tested with strings, the initialisation function adds three items {"Foo", "Bar", "Baz"}):

[Test]
public void CountChanging()
{
    Assert.That(_list.Count, Is.EqualTo(3));
    _list.Add("Qux");
    Assert.That(_list.Count, Is.EqualTo(4));
    _list[7] = "Quuuux";
    Assert.That(_list.Count, Is.EqualTo(8));
    _list.Remove("Quuuux");
    Assert.That(_list.Count, Is.EqualTo(7));
}

[Test]
public void ContainsItem()
{
    Assert.That(_list.Contains("Qux"), Is.EqualTo(false));
    _list.Add("Qux");
    Assert.That(_list.Contains("Qux"), Is.EqualTo(true));
    _list.Remove("Qux");
    Assert.That(_list.Contains("Qux"), Is.EqualTo(false));
}

The code is fairly self-commenting, so I won't go into what's happening, but is this sort of thing taking it too far? Add() and Remove() are tested seperately of course, so what level should I go to with these sorts of tests? Should I even have these sorts of tests?

+1  A: 

Is _list an instance of a class you wrote? If so, I'd say testing it is reasonable. Though in that case, why are you building a custom List class?

If it's not code you wrote, don't test it unless you suspect it's in some way buggy.


I try to test code that's independent and modular. If there's some sort of God-function in code I have to maintain, I strip out as much of it as possible into sub-functions and test them independantly. Then the God function can be written to be "obviously correct" -- no branches, no logic, just passing results from one well-tested subfunction to another.

John Millikin
It is my own code, and in writing the tests have actually exposed bugs even though I didn't suspect it to be buggy. As to why I'm reinventing the wheel so to speak, I needed a List<T> type class that supported events when items were added/removed etc.
Matthew Scharley
Since it's your code, then yes, do write tests for it. Even if writing them is boring or feels wasteful, every test for a non-trivial class can prevent hours of frustration down the road. The only tests I don't write in C# are for things like {get;set;} properties.
John Millikin
Why not just write a decorator for the default generic list that fires those events on add/remove, passing the actual work on to the superclass?
jdmichal
+6  A: 

Think of your tests as a specification. If your system can break (or have material bugs) without your tests failing, then you don't have enough test coverage. If one single point of failure causes many tests to break, you probably have too much (or are too tightly coupled).

This is really hard to define in an objective way. I suppose I'd say err on the side of testing too much. Then when tests start to annoy you, those are the particular tests to refactor/repurpose (because they are too brittle, or test the wrong thing, and their failures aren't useful).

James Baker
+2  A: 

A few tips:

  1. Each testcase should only test one thing. That means that the structure of the testcase should be "setup", "execute", "assert". In your examples, you mix these phases. Try splitting your test-methods up. That makes it easier to see exactly what you are testing.

  2. Try giving your test-methods a name that describes what it is testing. I.e. the three testcases contained in your ContainsItem() becomes: containsReportsFalseIfTheItemHasNotBeenAdded(), containsReportsTrueIfTheItemHasBeenAdded(), containsReportsFalseIfTheItemHasBeenAddedThenRemoved(). I find that forcing myself to come up with a descriptive name like that helps me conceptualize what I have to test before I code the actual test.

  3. If you do TDD, you should write your test firsts and only add code to your implementation when you have a failing test. Even if you don't actually do this, it will give you an idea of how many tests are enough. Alternatively use a coverage tool. For a simple class like a container, you should aim for 100% coverage.

Rasmus Faber
Prior to right now, I havn't done TDD, this is my first attempt at it, and I'm converting a small library (6-8 classes or so) from another project and writing test cases for it as an introduction. I understand the advantages of writing test first, but in this case, I just didn't know any better.
Matthew Scharley
"containsReportsFalseIfTheItemHasBeenAddedThenRemoved" hoooooooly shit! When writing tests, there's nothing wrong with "TestContains1", "TestContains2", etc.
John Millikin
Nothing except that when the test fails, you have no idea what actually failed without looking at the test code. All to save typing a few characters once and never looking at it again?
jdmichal
-1 Awful naming scheme
Tomas
+7  A: 

I would say that what you're actually testing are equivalence classes. In my view, there is no difference between a adding to a list that has 3 items or 7 items. However, there is a difference between 0 items, 1 item and >1 items. I would probably have 3 tests each for Add/Remove methods for these cases initially.

Once bugs start coming in from QA/users, I would add each such bug report as a test case; see the bug reproduce by getting a red bar; fix the bug by getting a green bar. Each such 'bug-detecting' test is there to stay - it is my safety net (read: regression test) that even if I make this mistake again, I will have instant feedback.

Yuval
The reason I prepopulated it (using .Add()) was so that the majority of methods, which need to work with test data, have something to work with.
Matthew Scharley
I understand that. However, your list can also be empty which is a valid state (i.e., Count should return 0, etc.) This is fundamentally 'different' from adding to a list which already has items - hence the term 'equivalence classes'.
Yuval
I have a test for .Clear() and a test for adding after a .Clear(), as well as a test for creating and adding to a list that's created with no initial capacity. I think I'm still covered, unless you can think of something else?
Matthew Scharley
When I say "no initial capacity", I don't mean a default initial capacity, I mean a capacity that is zero.
Matthew Scharley
I think you're covered - you covered all the different 'states' your list can be in.
Yuval