DEV Community

sour_grape
sour_grape

Posted on

My Take on OOP (Part 4): TDD Isn't a Cult, It's a Contract

Why I Hate TDD

You've probably heard the TDD mantra:

  1. RED: First, write a failing test.

  2. GREEN: Write the actual code to make the test pass.

  3. BLUE: Refactor to remove duplication, generalize, etc.

This is why I hate TDD.

But I also love TDD.

Sounds like nonsense, right? Let me explain. I don't think the essence of TDD is this rigid formula.

If you've read the first three parts of this series, you know how critical it is to honor your requirements and contracts. We use interfaces in our software to represent these contracts. But how do we know if the contract is actually being fulfilled? Is conforming to an interface enough to satisfy our requirements?

It’s not enough.

We test to see if our implementation honors the requirements and contracts. So, isn't the ultimate goal to uphold the contract? Then doesn't it make sense to first create a test that verifies the contract?

TDD is the verification of a contract. TDD is a true test that checks if the implementation satisfies the agreement. If the tests don't pass, the implementation is a failure. Why? Because it broke the contract.

Do you see why I hate the way TDD is described? TDD is a contract verification. It's a living guarantee, a document that evolves. But the dogma tells you to intentionally write failing code?

The 'RED' step isn't about aiming for failure. It's a natural consequence. You've simply defined the 'contract to be fulfilled' (the test) first, and the implementation that satisfies it doesn't exist yet. The 'failure' isn't the goal; it's just the expected state before the contract is fulfilled. Describing this as "deliberately failing" distorts the purpose and puts the cart before the horse.

Why I Love TDD

If you've read the rest of this series, you'll understand how much I value requirements and contracts.

Whether you write the test first or after, I love TDD from the perspective that it verifies the contract. This is how I use TDD.

To make it easy to understand, here's a unit test file for a Value Object (VO) class I wrote.

class TestActorValueObject(unittest.TestCase):

    def test_create_valid_actor_all_fields(self):
        # Contract: Must be able to successfully create an ActorVO with all valid fields.
        vo = ActorVO(name="Tom Hanks", role_name="Forrest Gump", external_id="tmdb_actor_123")
        self.assertEqual(vo.name, "Tom Hanks")
        self.assertEqual(vo.role_name, "Forrest Gump")
        self.assertEqual(vo.external_id, "tmdb_actor_123")

    def test_create_valid_actor_name_only(self):
        # Contract: Must be able to successfully create an ActorVO with only the required field, name.
        vo = ActorVO(name="Song Kang-ho")
        self.assertEqual(vo.name, "Song Kang-ho")
        self.assertIsNone(vo.role_name)
        self.assertIsNone(vo.external_id)

    # ... (and so on for all other test cases) ...

    def test_create_with_empty_name_raises_value_error(self):
        # Contract: Attempting to create with an empty name must raise a ValueError.
        with self.assertRaisesRegex(ValueError, "Actor name cannot be empty."):
            ActorVO(name="")

    def test_actor_equality(self):
        # Contract: Two ActorVO objects must be equal if all their attributes are the same.
        vo1 = ActorVO(name="Same Actor", role_name="Same Role", external_id="ext1")
        vo2 = ActorVO(name="Same Actor", role_name="Same Role", external_id="ext1")
        self.assertEqual(vo1, vo2)

    def test_actor_string_representation(self):
        # Contract: The string representation of an ActorVO object must be displayed appropriately.
        vo_with_role = ActorVO(name="Ma Dong-seok", role_name="Maseokdo")
        self.assertEqual(str(vo_with_role), "Ma Dong-seok (Role: Maseokdo)")
Enter fullscreen mode Exit fullscreen mode

As you can see, I explicitly state the contracts and constraints that arise from the design as comments. No matter how I implement or change ActorVO, it must honor these contracts. This test suite is the living document that guarantees it.

What if the requirements themselves change and the contract is updated? I just modify the test. And of course, I update the contract in the comment.

My Thoughts on assert

When I first started writing tests, the thing I understood the least was assert.

You create a result and then check if it matches what you expect. What's the point of that? Seems like a waste of time, right?

But as we've seen in this series, requirements and contracts are what matter.

The word assert means "to state a fact or belief confidently and forcefully." Why do we need to 'assert' something in a test? Because we have to verify that the contract is being met.

assert(my_implementation_result, what_the_contract_requires)

This assertion is, in essence: my_implementation_result == contract_satisfaction_value.

The assert statement is the most intuitive expression of the purpose of testing: verifying the contract.

Conclusion

TDD is a helper for OOP. It's the living document that verifies our requirements and contracts are being met.

Top comments (0)