Hohenheim
Hohenheim

Reputation: 407

TDD Unit Test sub-methods

I'm in a dilemma of whether I'll write tests for methods that are a product of refactoring another method.

First question, consider this plot.

class Puzzle(
    val foo: List<Pieces>,
    val bar: List<Pieces>
) {
    init {
       // code to validate foo
       // code to validate bar
    }
}

Here I'm validating parameters in constructing an object. This code is the result of TDD. But with TDD we write fail_test -> pass test -> refactor, when refactoring I transferred validator methods to a helper class PuzzleHelper.

object PuzzleHelper {

    fun validateFoo() {
         ...
    }

    fun validateBar() {
         ...
    }
}

Do I still need to test validateFoo and validateBar in this case?

Second question

class Puzzle(
    val foo: List<Pieces>,
    val bar: List<Pieces>
) {
    ...

    fun getPiece(inPosition: Position) {
        validatePosition()
        // return piece at position
    }

    fun removePiece(inPosition: Position) {
        validatePosition()
        // remove piece at position
    }
}

object PuzzleHelper {

    ...

    fun validatePosition() {
         ...
    }
}

Do I still need to write test for getPiece and removePiece that involve position validation?

I really want to be fluent in using TDD, but don't know how to start. Now I finally dive-in and don't care whats ahead, all I want is product quality. Hope to hear from your enlightenment soon.

Upvotes: 0

Views: 288

Answers (2)

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57397

Do I still need to test validateFoo and validateBar in this case?

It depends.

Part of the point of TDD is that we should be able to iterate on the internal design; aka refactoring. That's the magic that allows us to start from a small investment in up front design and work out the rest as we go -- the fact that we can change things, and the tests evaluate the change without getting in the way.

That works really well when the required behaviors of your system are stable.

When the required behaviors of the system are not stable, when we have a lot of decisions that are in flux, when we know the required behaviors are going to change but we don't know which... having a single test that spans many unstable behaviors tends to make the test "brittle".

This was the bane of automated UI testing for a long time -- because testing a UI spans pretty much every decision at every layer of the system, tests were constantly in maintenance to eliminate cascades of false positives that arose in the face of otherwise insignificant behavior changes.

In that situation, you may want to start looking into ways introduce bulkheads that prevent excessive damage when a requirement changes. We start writing tests that validate that the test subject behaves in the same way that some simpler oracle behaves, along with a test that the simpler oracle does the right thing.

This, too, is part of the feedback loop of TDD -- because tests that span many unstable behaviors is hard, we refactor towards designs that support testing behaviors at an isolated grain, and larger compositions in terms of their simpler elements.

Upvotes: 1

Kraylog
Kraylog

Reputation: 7563

When we get to the refactoring stage of the Red -> Green -> Refactor cycle, we're not supposed to add any new behavior. This means that all the code is already tested, so no new tests are required. You can easily validate you've done this by changing the refactored code and watch it fail a test. If it doesn't, you added something you weren't supposed to.

In some cases, if the extracted code is reused in other places, it might make sense to transfer the tests to a test suite for the refactored code.

As for the second question, that depends on your design, as well as some things that aren't in your code. For example, I don't know what you'd like to do if validation fails. You'll have to add different tests for those cases in case validation fails for each method.

The one thing I would like to point out, is that placing methods in a static object (class functions, global functions, however you want to call it) makes it harder to test code. If you'd like to test your class methods when ignoring validation (stubbing it to always pass) you won't be able to do that.
I prefer to make a collaborator that gets passed to the class as a constructor argument. So your class now gets a validator: Validator and you can pass anything you want to it in the test. A stub, the real thing, a mock, etc.

Upvotes: 1

Related Questions