14

As a newcomer to TDD I'm stuggling with writing unit tests that deal with collections. For example at the moment I'm trying to come up with some test scearios to essentially test the following method

int Find(List<T> list, Predicate<T> predicate);

Where the method should return the index of the first item in the list list that matches the predicate predicate. So far the only test cases that I've been able to come up with have been along the lines of

  • When list contains no items - returns -1
  • When list contains 1 item that matches predicate - returns 0
  • When list contains 1 item that doesn't match predicate - returns -1
  • When list contains 2 items both of which match predicate - return 0
  • When list contains 2 items, the first of which match predicate - return 0
  • etc...

As you can see however these test cases are both numerous and don't satisfactorily test the actual behaviour that I actually want. The mathematician in me wants to do some sort of TDD-by-induction

  • When list contains no items - returns -1
  • When list contains N items call predicate on the first item and then recursively call Find on the remaining N-1 items

However this introduces unneccessary recursion. What sort of test cases should I be looking to write in TDD for the above method?


As an aside the method that I am trying to test really is just Find, simply for a specific collection and predicate (which I can independently write test cases for). Surely there should be a way for me to avoid having to write any of the above test cases and instead simply test that the method calls some other Find implementation (e.g. FindIndex) with the correct arguments?

Note that in any case I'd still like to know how I could unit test Find (or another method like it) even if it turns out that in this case I don't need to.

Justin
  • 84,773
  • 49
  • 224
  • 367

5 Answers5

9

If find() is working, then it should return the index of the first element that matches the predicate, right?

So you'll need a test for the empty list case, and one for the no-matching elements case, and one for a matching element case. I would find that sufficient. In the course of TDDing find() I might write a special first-element-passes case, which I could fake easily. I would probably write:

emptyListReturnsMinusOne()
singlePassingElementReturnsZero()
noPassingElementsReturnsMinusOne()
PassingElementMidlistReturnsItsIndex()

And expect that sequence would drive my correct implementation.

Carl Manaster
  • 39,912
  • 17
  • 102
  • 155
  • 2
    You might want to add a `PassingElementAtEndOfListReturnsItsIndex()` test. – Philipp Jul 05 '12 at 14:07
  • 1
    It's hard for me to envision a realistic solution that would pass the midlist test but fail the endOfList test. – Carl Manaster Jul 05 '12 at 14:08
  • 1
    I could imagine the situation where someone decides a for loop would be faster than a foreach and introduces an off by one error. That person would be an idiot of course, but personally I write my tests for idiots (usually me being the idiot as it turns out :) ) – David Hall Jul 05 '12 at 14:21
  • 1
    Actually after five minutes thought the single instance case would catch this but I still think testing all boundary cases is useful. – David Hall Jul 05 '12 at 14:30
2

Stop testing when fear is replaced by boredom - Kent Beck

In this case, what is the probability that given a passing test for

  • "When list contains 2 items both of which match predicate - return 0"

the following test will fail ?

  • "When list contains 5 items both of which match predicate - return 0"

I'd write the former because I'm afraid that the behavior doesn't work for multiple elements. However once 2 works, writing another one for 5 is just tedium (Unless there is a hardcoded assumption of 2 in the production code.. which should have been refactored away. Even if it is not, I'd just modify the existing test to have 5 instead of 2 and make it work for the general case).

So write tests for significantly different things. In this case, list with (zero, one, many) elements and (contains/does not contain) operand

Gishu
  • 134,492
  • 47
  • 225
  • 308
0

Based on your requirement for Find method, here's what I would test:

  1. list is null - throws ArgumentNullException or returns -1
  2. list contains no items - returns -1
  3. predicate is null - throws ArgumentNullException or returns -1
  4. list contains one item that doesn't match the predicate - returns -1
  5. list contains one item that matches the predicate - returns 0
  6. list contains multiple items, but no item matches the predicate - returns -1
  7. list contains multiple items that match the predicate - returns index of first match

Basically, you would first test for end cases - null arguments, empty list. After that, one item tests. Finally, test match and non-match for multiple items.

For null arguments, you could either throw exception, or return -1, depending on your preference.

Miroslav Popovic
  • 12,100
  • 2
  • 35
  • 47
0

Don't change the list, change the predicates.

Think about how the method will be called. When someone is calling the Find method they will already have a list and need to think of predicates. So think of good examples that demonstrate the behavior of Find:

Example: Using same list 3, 4 for all testcases makes it easy to understand:

  1. Predicate < 5 matches both numbers (returns 1)
  2. Predicate == 3 matches 3 (returns 0)
  3. Predicate == 0 matches none (returns -1)

This is really all you need to specify the behavior and by changing the predicates rather than the list you give good examples of how to use the Find method. A list with zero, one, or two elements isn't really changing the behavior of Find and not really how the method will be used. Follow DRY with your testcases, focus on specifying behavior not proving code is correct or you will end up spending all your time writing tests.

Garrett Hall
  • 29,524
  • 10
  • 61
  • 76
0

To try to answer your aside: I don't have any experience with Rhino mocks, but I believe it should have something similar to FakeItEasy(?):

var finder = A.Fake<IMyFindInterface>();

// ... insert code to call IMyFindInterface.Find(whatever) here

A.CallTo(() => finder.find(A<List>.That.Matches(
                  x => x.someProperty == someValue))).MustHaveHappened();

By putting the implementation of Find() behind an interface, and then passing the method that would uses that interface a fake, you can check that the method is called with certain parameters. (The MustHaveHappended() will cause the test to fail if the expected call is not completed).

Since you know that the real implementation of IMyFindInterface just passes the call on to an implementation you already trust, this should be a good enough test to verify that the code you are testing calls the Find-implementation in the correct way.

This same procedure can be used whenever you just want to ensure that your code (the unit you are testing) calls some component you already trust in a correct way by abstracting away that component itself - exactly what we want when unit-testing.

Kjartan
  • 18,591
  • 15
  • 71
  • 96