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.
Best Answer
Because you're running too fast. When reading specifications you should stop as soon as you can write some code (after you understood the domain). I usually read the domain model specifications few times until I think I understood them and then I read them again using an highlighter to mark important bits to start coding.
First of all write down the complete specifications for this game. Everything, even if you think you have it clear in your mind. It doesn't even need to be strictly formal.
Let's go through this word by word, the first keyword to highlight is game. You will, probably, then need a
Game
class you want to create. Let's write a test method (syntax is for C# but the important part is the concept):So far so good, we just want to be sure we can create a
Game
object and it doesn't throw any exception. If we will introduce a factory (for example to join an on-line pending game) then this is the test we need to rewrite. Let's write code to make it pass:Now continue scanning your specifications, you have players. You will then need to create them, same as above. You see then ...in a game.... It means they must be stored in the
Game
class and they can be from 2 to 5, you need also a collection.Let's declare the class and its collection
PlayerCollection
then add it to the game class:Our tests are green, we can now move to the next important part (the number of players). When we need at least two players and no more than 5? When we start a game:
Make it compile, add
Start()
toGame
:Just continue like this until your application is completed. Now you can move that check into a separate function
EnsureThereIsRightNumberOfPlayers()
and you already have all the tests to do it safely. I'd suggest to also add assertions to help you with tests (countless times an assertion failed when running my tests and I found I had to add more tests...) but it's probably outside the scope of your question.Writing tests before code will also help to have a better code coverage and to keep your code testable even when you just started using unit testing.
What I shown above is a literal TDD approach, I am often little bit more pragmatic and I write bigger code blocks and/or multiple tests in one shot. For example I will NEVER write
int Sum(int a, int b) => 2;
because I need to pass the testAssert.AreEqual(2, Sum(1, 1,))
(to fix it in the next test when I need to verifySum(-1, -1)
). I'd write, whenever possible, enough code to pass the test but using what may be a true implementation (which I may refine and refactor later because I already have tests in-place).With some experience with TDD you will find what's the best compromise for you.
TDD as a tool to study the domain
Note: TDD won't replace design phase, it's not its role. It will, however, help you to refine your design from an abstract domain point of view to a practical implementation. Also note that the type of tests you write in this case is pretty different from the type of tests you write on a normal TDD approach.
Sometimes the domain isn't perfectly clear and you need to write some code to outline overall architecture. This case, IMO, is when you have greater benefits with TDD. Didn't ever happen to write a great model which is a pain to consume? If you start consuming your model first then this won't ever happen.
In this case I find convenient to write a slightly complex test method to understand who the actors are, for example a very naive first approach to the problem may be:
As you can see you immediately see what your objects have to do, you need to keep track of the current player, its cards and stop playing when...cards are finished. You also immediately see that more users may share the same screen and so on...
Of course you will need to write a lot of code before this test can pass (you may temporarily
[Ignore]
it) and it MUST change/evolve over time but it will be your GPS to write all the other smaller tests you need. You may even find that some of these "high level abstraction" tests should be moved to integration tests, for example. Even better. Note that you should not throw away these tests, they're great self-documenting examples and the first thing that someone else should see when studying your code.Writing this kind of rough code will greatly help you to better understand the Big Picture before writing one single line in your model (and some IDEs let you generate stubs from this code...). After you wrote above code you may then think "Shouldn't everything be inside
Start()
?". You will then drop thatwhile
and you will next think "How should I get inputs?" and add, for example, anInput
property of typeStream
to theGame
class. You will do the same forOutput
and you will then ask yourself "What if I want to make it graphical?" and replace it with with aUserInteraction
class. Keep changing, refining and increasing your understanding. You won't write a model (and its tests) you need to throw away because it's a pain to use (which may also cause the calling code to be a mess because you don't want to throw away such beautiful already written model...)Note that this consumer to implementation approach will also help you to refine some of your architectural decisions, for example:
You probably have already noted that DDD, TDD and - when dealing with UI - lean UX design aren't separate steps or different approaches (!!!) but they go pretty well all together.