Justin
Justin

Reputation: 86729

TDD - writing tests for a method that iterates / works with collections

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

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

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.

Upvotes: 14

Views: 634

Answers (5)

Kjartan
Kjartan

Reputation: 19111

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.

Upvotes: 0

Gishu
Gishu

Reputation: 136613

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

Upvotes: 2

Garrett Hall
Garrett Hall

Reputation: 30022

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.

Upvotes: 0

Miroslav Popovic
Miroslav Popovic

Reputation: 12128

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.

Upvotes: 0

Carl Manaster
Carl Manaster

Reputation: 40336

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.

Upvotes: 9

Related Questions