views:

226

answers:

5

I am doing my first steps with TDD. The problem is (as probably with everyone starting with TDD), I never know very well what kind of unit tests to do when I start working in my projects.

Let's assume I want to write a Stack class with the following methods(I choose it as it's an easy example):

Stack<T>
 - Push(element : T)
 - Pop() : T
 - Peek() : T
 - Count : int
 - IsEmpty : boolean

How would you approch this? I never understood if the idea is to test a few corner cases for each method of the Stack class or start by doing a few "use cases" with the class, like adding 10 elements and removing them. What is the idea? To make code that uses the Stack as close as possible to what I'll use in my real code? Or just make simple "add one element" unit tests where I test if IsEmpty and Count were changed by adding that element?

How am I supposed to start with this?

EDIT

Here's my rough tests' implementation:

    [TestMethod]
    public void PushTests() {
        StackZ<string> stackz = new StackZ<string>();

        for (int i = 0; i < 5; ++i) {
            int oldSize = stackz.Size;
            stackz.Push(i.ToString());
            int newSize = stackz.Size;
            Assert.AreEqual(oldSize + 1, newSize);
            Assert.IsFalse(stackz.IsEmpty);
        }
    }

    [TestMethod, ExpectedException(typeof(InvalidOperationException))]
    public void PeekTestsWhenEmpty() {
        StackZ<double> stackz = new StackZ<double>();
        stackz.Peek();
    }

    [TestMethod]
    public void PeekTestsWhenNotEmpty() {
        StackZ<int> stackz = new StackZ<int>();
        stackz.Push(5);

        int firstPeekValue = stackz.Peek();

        for (int i = 0; i < 5; ++i) {
            Assert.AreEqual(stackz.Peek(), firstPeekValue);
        }
    }

    [TestMethod, ExpectedException(typeof(InvalidOperationException))]
    public void PopTestsWhenEmpty() {
        StackZ<float> stackz = new StackZ<float>();
        stackz.Pop();
    }

    [TestMethod]
    public void PopTestsWhenNotEmpty() {
        StackZ<int> stackz = new StackZ<int>();

        for (int i = 0; i < 5; ++i) {
            stackz.Push(i);
        }

        for (int i = 4; i >= 0; ++i) {
            int oldSize = stackz.Size;
            int popValue = stackz.Pop();
            Assert.AreEqual(popValue, i);
            int newSize = stackz.Size;
            Assert.AreEqual(oldSize, newSize + 1);
        }

        Assert.IsTrue(stackz.IsEmpty);
    }

Any corrections/ideas about it? Thanks

+1  A: 

I would start like this:

  • create() - IsEmpty() == true -> OK
  • 2x push() - count() == 2 -> OK
  • peek() - T == expected (last pushed) -> OK (peek assumes seek is a typo)
  • 2x pop() - count() == 0 && isEmppty -> OK
stacker
Wowed by the response here I'd like to add that implementation and test code should be in balance, at least if it's not about life or something other very critical.
stacker
+6  A: 

Start by testing the basic principles of your API.

Test on zero elements.

  • Test that it's empty.
  • Count is zero.
  • Pop fails.

Test on one element:

  • Call Push.
  • Test that it's not empty.
  • Test that count is 1.
  • Test that Pop returns the element.
  • Test that it's now empty.
  • Test that count is now 0.

Test on >1 elements:

  • Now Push 2 and test count is two.
  • Pop 2 and make sure they come in LIFO order.
  • Check the emptiness and counts.

Each of these would be at least one test case.

For example (roughly outlined in Google's unit test framework for c++):

TEST(StackTest, TestEmpty) {
  Stack s;
  EXPECT_TRUE(s.empty());
  s.push(1);
  EXPECT_FALSE(s.empty());
  s.pop();
  EXPECT_TRUE(s.empty());
}

TEST(StackTest, TestCount) {
  Stack s;
  EXPECT_EQ(0, s.count());
  s.push(1);
  EXPECT_EQ(1, s.count());
  s.push(2);
  EXPECT_EQ(2, s.count());
  s.pop();
  EXPECT_EQ(1, s.count());
  s.pop();
  EXPECT_EQ(0, s.count());
}

TEST(StackTest, TestOneElement) {
  Stack s;
  s.push(1);
  EXPECT_EQ(1, s.pop());
}

TEST(StackTest, TestTwoElementsAreLifo) {
  Stack s;
  s.push(1);
  s.push(2);
  EXPECT_EQ(2, s.pop());
  EXPECT_EQ(1, s.pop());
}

TEST(StackTest, TestEmptyPop) {
  Stack s;
  EXPECT_EQ(NULL, s.pop());
}


TEST(StackTest, TestEmptyOnEmptyPop) {
 Stack s;
  EXPECT_TRUE(s.empty());
  s.pop();
  EXPECT_TRUE(s.empty());
}

TEST(StackTest, TestCountOnEmptyPop) {
  Stack s;
  EXPECT_EQ(0, s.count());
  s.pop();
  EXPECT_EQ(0, s.count());
}
Stephen
Should I make 3 tests or should I make one test by each one of the bullet items?
devoured elysium
devoured: when a bug is introduced, would you prefer a message like "test against one element failed" or "after pushing once, count is not 1"? If the former, use three tests, if the latter, make one test for each bullet point. (I know what I'd pick :)
David Winslow
This was just for making sure :P btw, Would you put each one of the 3 cases in a different test class?
devoured elysium
When you say "Test that count is 1" (for example) - is that a different test or just an assert?
Amir Rachum
@devoured, amir: Updated with an example. It's up to you how to structure it, but i'd make it part of the same test - it's just testing a constant property.
Stephen
With this type of test, it's going to be difficult to know that you really have covered all the bases. If a new requirement comes along, which test do you change to ensure the new feature is tested? If all your tests are just use cases, then it becomes hard to know whether you've really tested everything. You may be testing some things many times, and others not at all. For a Stack, this is manageable, for more complex Classes, it will become unmanageable. As well as usecase tests, you also tests that test individual requirements.
mdma
@all, after thinking it over awhile... I changed my mind a little bit. I think it's better off to test the functionality separately.
Stephen
+4  A: 

If you write out the requirements for each method in a little more detail, that will give you more hints as to the unit tests you need. You can then code up these tests. If you have an autocomplete IDE, like IDEA, then doing TDD is simple, because it underlines all the bits that you haven't implemented yet.

For example, if the requirement is "pop() on an empty stack throws a NoSuchElementException" then you would start with

@Test(exception=NoSuchElementException.class)
void popOnEmptyStackThrowsException()
{
   Stack s = new Stack();
   s.pop();
}

The IDE will then prompt you what to do about the missing Stack class. One of the options is "create class", so you create the class. Then it asks about the pop method, which you also choose to create. Now, you can implement your pop method, putting in what you need to implement the contract. i.e.

T pop() {
   if (size==0) throw new NoSuchElementException();
}

You continue, iteratively in this fashion until you have implemented tests for all the Stack requirements. As before, the IDE will complain that there is no "size" variable. I'd leave this until you create the test case "a newly created stack is empty", where you can then create the variable, since it's initialization is verified in that test.

Once your method requirements are handled, you can then add some more complex use cases. (Ideally these use cases would be specified as class-level requirements.)

mdma
You have made an excellent point about being more concise about the requirements/contracts of the methods. Maybe that is one of my problems, as I tend to never think of them before coding.
devoured elysium
One more thing. In your pop implementation example, will you just make "if (size == 0) throw blablabla" or do you start coding already the rest of pop() method logic? Should I only implement things in the Stack class after I've made a unit test for it?
devoured elysium
Well, the doctrine of TDD says thall shalt write test before code, and it's probably best to at least start out very strict, so you get into the habit. Personally, I do tend to smooth things over, e.g. for the pop, I might at least sketch out an implementation, and at the same time stub corresponding tests. The important thing is to end up with tests that fullfill all the stated requirements.
mdma
Novice question: TDD seems to advocate for writing the minimum needed to pass the test. As such, for this first test, is the "if (size==0)" actually necessary? Just "throw new NoSuchElementException()" would be enough to pass the test. Then, theoretically, you'd add another test later that popped after pushing and this test would fail and you'd go add the "if (size==0)". Is that taking TDD too far?
SCFrench
+1  A: 

Ideally the tests have to cover all functionalities of the class. They should check whether each operation behaves according to its contract. Theoretically speaking, I see the contract as a mapping between <prev state , params> to <new state , returned value>. Therefore, before designing the tests you should define well the contracts of all operations.

Here are some sample tests for the stack API above:

1) Push should increase the value returned by Count() by 1

2) Pop on an empty stack should throw an exception

3) Pop should reduce the value returned by Count() by 1

4) Pushing x1,x2,...,xn and then popping them must return them in reverse order xn,...,x1

5) Adding elements, validating isEmpty()==false and then popping all and validating isEmpty ()==true

6) Seek() must not change the value returned by Count()

7) Consecutive calls to Seek() must return the same value etc...

Eyal Schneider
+1  A: 

If you read the book about the Test-Driven development by Kent Beck, you might have noticed an idea that sounds frequently in the book: you should write tests to what you are curently missing. As long as you do not need something, do not write tests and do not implement it.

While your implementation of the Stack class fits your needs, you do not need to thoroughly implement it. Under the hood, it can even return constants to you or do nothing.

Testing should not become overhead to your development, it should speed up your development instead, supporting you when you do not want to keep everything in your head.

The main advantage of the TDD that it makes you write code that is testable in a small lines of code, because usually you do not want to write 50 lines of code to test a method. You become more concerned with interfaces and ditribution of the functionality among classes, because, once again, you do not want to write 50 lines of code to test a method.

Having said that, I can tell you that is not interesting and probably useful to learn TDD by implementing unit tests to superutil interfaces that are gained through suffering of several generation of developers. You will just not feel anynthing exciting. Just take any class from an application written by you and try to write tests to it. Refactoring them will give you much pleasure.

newtover
I've been reading the first example of the book you suggest. I don't get the idea of first returning 5 in a multiplication method instead of using the correct since the beggining. What is the point?
devoured elysium
@devoured elysium: The point is to return what the calling code expects. When the expected result differs, your code has an error. It is not always clear what is correct from the beginning. And it is not always required to implement absolute correctness, even if you can. The example of multiplication is just an example.
newtover