2

When doing TDD "by the book" we should only -- as far as I understand -- write failing tests. This means tests for unimplemented functionality.

I often find myself wanting to add tests for edge cases that expect to work with the current implementation. This means the test does not drive the implementation, it is merely there to confirm what I suspect and document the intent.

In "TDD proper", is writing these types of tests part of the "refactor" step, or are they not part of test-driving at all, and supposed to be added at a later pass altogether?

Thank you for your feedback on the question. I agree it was unclear. To clarify:

Note that I'm explicitly looking for references to the originators/popularisers of TDD. I can invent sensible ways to deal with this situation myself, so it's not common sense I'm asking for help with. I want to try to follow the original idea dogmatically for a little bit at first.

Also note that this is not because I've written too much code in response to a previously failing test. Kent Beck explicitly recognises in TDD By Example that if you can right the obvious, correct implementation right away, by all means do it.

It's then I want to verify that it is the correct implementation, by adding the other tests I would have added to drive the implementation had it not been obvious to me right away.

These are also not strictly redundant test cases, because different implementations might handle edge cases differently, but these are the choices I believe are correct, and I want to verify and document that.

Edit: Upon close inspection, Kent Beck actually brings this up in passing at the end of TDD By Example. If two tests exercise the same code path but with different inputs, the base rule is to delete one. However, both should be kept if one of the following conditions are true.

  1. Both tests increase your confidence in the solution for whatever reason, or
  2. They communicate different scenarios to the reader, that happen to be implemented with the same code path.
7
  • Also edge cases are part of the functionality. In my experience, your problem is maybe rather a design problem. “Expect to work” => expected behaviour => requirement. Considering your edge cases as normal required behaviour will make you think more thoroughly why you feel like “wanting to add tests”. This will either make the “edge case” behaviour transparent or superfluous. In the first case there’s nothing odd about adding tests, in the latter case you will probably find out how to refactor your code and remove the edge case behaviour. Aug 29, 2021 at 7:34
  • It looks like most of the answers to the existing question -- while good -- do not make much reference to any of the "originators" of TDD. In other words, they make sense, but I would like an answer true to the intent of Kent Beck et al. when they popularised TDD.
    – kqr
    Aug 29, 2021 at 9:53
  • (Why? Because when trying someone else's method for the first time, I don't want to start by improvising alternatives. Want to follow it religiously for a few iterations to really understand what ideas are interlocking and then improvise improvements when I understand better. So like Chesterton's fence.)
    – kqr
    Aug 29, 2021 at 9:57
  • @kqr: I don't know if Kent Beck had written something about adding non-failing test, but I actually cannot imagine he really meant to forbid adding such tests - the situation is way-too-common, it does not only appear for edge cases, but also for certain requirements. But this site is not for asking the community to do a literature research for you, we share our first-hand experience here. Hence I think that other question's top answer is the best answer one can give to this question: regardless of what Kent Beck wrote (or forgot to write), adding non-failing tests is ok in TDD.
    – Doc Brown
    Aug 29, 2021 at 12:35
  • Fair enough! Thank you for the attention and assistance this far.
    – kqr
    Aug 29, 2021 at 12:41

2 Answers 2

1

You write the test. The test passes without you having written any production. This means one of two things:

  1. At some point in your past, you wrote more code than needed to pass the test(s).
  2. The test is redundant. It is already covered by one of your other tests, or a combination of them.

#1 is simple: keep the test, you're done. In situation #2, you have a choice to make:

  • Keep the test as documentation of the edge case.
  • Add a comment to the existing test(s) that already covers the edge case.
6
  • 1
    This answer ignores the way more common situation where at some point, one chose to implement a feature in a way which does not require more code to make the new tests pass - but the test is nevertheless not redundant, since a different kind of implementation might not provide such a guarantee.
    – Doc Brown
    Aug 29, 2021 at 12:07
  • For example, when my first requirement is to write a function which splits strings at a certain separator, and I use the inbuilt "Split" function of the given language/library to implement this feature, I haven't written more code than necessary to make even a simple test pass which split a string with one separator contained. However, chances are high my function will cover a lot of standard edge cases correctly (like "no separator contained in the input", "string starts or ends with the separator" etc). Adding extra tests for these edge case will not be redundant.
    – Doc Brown
    Aug 29, 2021 at 12:44
  • @DocBrown, if you try to follow TDD dogmatically, you have implemented too much functionality by using that inbuilt Split function. For a practical application of TDD, what you did is entirely correct. Note that the OP is trying to learn/understand TDD by following it dogmatically initially. Aug 30, 2021 at 6:08
  • @BartvanIngenSchenau: sorry, but I don't buy that. I agree that if there is just one test for SplitByComma("A,B") which checks if it returns A and B, then we could start with an implementation return new[]{"A","B"}. But if we add more and more of those tests - still not any of those edge cases - there will be a point where using the inbuilt Split function is the most simplest solution one can think of. And then new tests for those edge cases will not fail. My point is: I don't believe Kent Beck's "dogma" was "you are only allowed to write a new test when it fails first" ...
    – Doc Brown
    Aug 30, 2021 at 6:22
  • 1
    @DocBrown, fair enough. My feeling has always been that the dogma starts to break down once you progress beyond the trivial examples. Aug 30, 2021 at 6:26
0

In "TDD proper", is writing these types of tests part of the "refactor" step

No. Refactoring is about changing production code without changing it's behavior. It's not about changing or adding tests.

are they not part of test-driving at all, and supposed to be added at a later pass altogether?

There's no reason a failing test can't be a failing edge case. And no reason you can't wait until you have one before writing code that makes it pass.

You can see that clearly illustrated in Uncle Bob's bowling kata. It treats every feature as an edge case and creates tests for them before writing code that makes them pass.

But if you insist on adding an edge case test to working code I can give you formal steps for that. I documented them here. They support the principle that you should see every test both pass and fail before you trust the test.

2
  • 1
    If the current implementation already supports an edge case, you suggest not to write a test to confirm and document that intent?
    – Rik D
    Aug 29, 2021 at 7:10
  • @Rik D that is indeed my question!
    – kqr
    Aug 29, 2021 at 7:14

Not the answer you're looking for? Browse other questions tagged or ask your own question.