Unit Testing – How to TDD a Yahtzee Game

tddunit testing

Let's say you're writing a Yahtzee game TDD style. You want to test the part of the code that determines whether or not a set of five die rolls is a full house. As far as I know, when doing TDD, you follow these principles:

  • Write tests first
  • Write the simplest thing possible that works
  • Refine and refactor

So an initial test might look something like this:

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(1, 1, 1, 2, 2);

    Assert.IsTrue(actual);
}

When following the "Write the simplest thing possible that works", you should now write the IsFullHouse method like this:

public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
    if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
    {
        return true;
    }

    return false;
}

This results in a green test but the implementation is incomplete.

Should you unit test every possible valid combination (both of values and positions) for a full house? That looks like the only way to be absolutely sure that your IsFullHouse code is completely tested and correct, but it also sounds quite insane to do that.

How would you unit test something like this?

Update

Erik and Kilian point out that using literals in the initial implementation to get a green test might not be the best idea. I'd like to explain why I did that and that explanation does not fit in a comment.

My practical experience with unit testing (especially using a TDD approach) is very limited. I remember watching a recording of Roy Osherove's TDD Masterclass on Tekpub. In one of the episodes he builds a String Calculator TDD style. The full specification of the String Calculator can be found here: http://osherove.com/tdd-kata-1/

He starts with a test like this:

public void Add_with_empty_string_should_return_zero()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("");

    Assert.AreEqual(0, result);
}

This results in this first implementation of the Add method:

public int Add(string input)
{
    return 0;
}

Then this test is added:

public void Add_with_one_number_string_should_return_number()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("1");

    Assert.AreEqual(1, result);
}

And the Add method is refactored:

public int Add(string input)
{
    if (input.Length == 0)
    {
        return 0;
    }

    return 1;
}

After each step Roy says "Write the simplest thing that will work".

So I thought I would give this approach a try when trying to do a TDD-style Yahtzee game.

Best Answer

There are already lots of good answers to this question, and I've commented and upvoted several of them. Still, I'd like to add some thoughts.

Flexibility isn't for novices

The OP clearly states that he's not experienced with TDD, and I think a good answer must take that into account. In the terminology of the Dreyfus model of skill acquisition, he's probably a Novice. There's nothing wrong with being a novice - we are all novices when we start learning something new. However, what the Dreyfus model explains is that novices are characterized by

  • rigid adherence to taught rules or plans
  • no exercise of discretionary judgement

That's not a description of a personality deficiency, so there's no reason to be ashamed of that - it's a stage we all need to go through in order to learn something new.

This is also true for TDD.

While I agree with many of the other answers here that TDD doesn't have to be dogmatic, and that it can sometimes be more beneficial to work in an alternative way, that doesn't help anyone just starting out. How can you exercise discretionary judgement when you have no experience?

If a novice accepts the advice that sometimes it's OK not to do TDD, how can he or she determine when it's OK to skip doing TDD?

With no experience or guidance, the only thing a novice can do is to skip out of TDD every time it becomes too difficult. That's human nature, but not a good way to learn.

Listen to the tests

Skipping out of TDD any time it becomes hard is to miss out of one of the most important benefits of TDD. Tests provide early feedback about the API of the SUT. If the test is hard to write, it's an important sign that the SUT is hard to use.

This is the reason why one of the most important messages of GOOS is: listen to your tests!

In the case of this question, my first reaction when seeing the proposed API of the Yahtzee game, and the discussion about combinatorics that can be found on this page, was that this is important feedback about the API.

Does the API have to represent dice rolls as an ordered sequence of integers? To me, that smell of Primitive Obsession. That's why I was happy to see the answer from tallseth suggesting the introduction of a Roll class. I think that's an excellent suggestion.

However, I think that some of the comments to that answer get it wrong. What TDD then suggests is that once you get the idea that a Roll class would be a good idea, you suspend work on the original SUT and start working on TDD'ing the Roll class.

While I agree that TDD is more aimed at the 'happy path' than it's aimed at comprehensive testing, it still helps to break the system down into manageable units. A Roll class sounds like something you could TDD to completion much more easily.

Then, once the Roll class is sufficiently evolved, would you go back to the original SUT and flesh it out in terms of Roll inputs.

The suggestion of a Test Helper doesn't necessarily imply randomness - it's just a way to make the test more readable.

Another way to approach and model input in terms of Roll instances would be to introduce a Test Data Builder.

Red/Green/Refactor is a three-stage process

While I agree with the general sentiment that (if you are sufficiently experienced in TDD), you don't need to stick to TDD rigorously, I think it's pretty poor advice in the case of a Yahtzee exercise. Although I don't know the details of the Yahtzee rules, I see no convincing argument here that you can't stick rigorously with the Red/Green/Refactor process and still arrive at a proper result.

What most people here seem to forget is the third stage of the Red/Green/Refactor process. First you write the test. Then you write the simplest implementation that passes all tests. Then you refactor.

It's here, in this third state, that you can bring all your professional skills to bear. This is where you are allowed to reflect on the code.

However, I think it's a cop-out to state that you should only "Write the simplest thing possible that isn't completely braindead and obviously incorrect that works". If you (think you) know enough about the implementation on beforehand, then everything short of the complete solution is going to be obviously incorrect. As far as advice goes, then, this is pretty useless to a novice.

What really should happen is that if you can make all tests pass with an obviously incorrect implementation, that's feedback that you should write another test.

It's surprising how often doing that leads you towards an entirely different implementation than the one you had in mind first. Sometimes, the alternative that grows like that may turn out to be better than your original plan.

Rigour is a learning tool

It makes a lot of sense to stick with rigorous processes like Red/Green/Refactor as long as one is learning. It forces the learner to gain experience with TDD not just when it's easy, but also when it's hard.

Only when you have mastered all the hard parts are you in a position to make an informed decision on when to deviate from the 'true' path. That's when you start forming your own path.

Related Topic