3 Laws Of TDD - egnomerator/misc GitHub Wiki

The 3 Laws of Test Driven Development (video here)

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

Trade-off (Summary)

  • does a reduction of debugging time by some "factor of D" make up for the "silliness" of TDD?
    • bad
      • stupid, tedious, frustrating
    • good (discussed below)
      • thorough built-in documentation
      • reduce debug time
      • better code (decoupled)
      • fun
      • THOROUGH PRODUCTION CODE COVERAGE
        • when the test suite passes, it means the system works

The Discussion

Some initial comments related to TDD:

  • negative
    • This sounds stupid
    • It locks you into a 5 second loop
    • Tedium
    • Analogy of doctors washing their hands 10 strokes per side of finger, each finger.
  • positive
    • what if we always were in a situation where 5 seconds ago, everything worked?!
    • how often, how much time do you spend debugging? maybe that should be way less

Illustration Scenario--Research for Integrating a 3rd Party System:

  • Scenario
    • you need integrate a 3rd party system
    • so you need to look at the docs
    • you skip to the code examples at the back to learn
  • TDD
    • when you follow TDD, the unit tests that you produce are these code examples for the entire system
    • just like the 3rd party system code examples, your unit tests are little snippets of code that explain how the system works
    • in fact, because you are following TDD, there is a snippet of code that explains how EVERY part of the system works

What are unit tests

  • Unit tests don't know about each other; they do not form a system--they are independent little units of code that reach into the production code and exercise a very limited part of it
  • They are little documents--low-level documents--for programmers to understand how to work the system

If you follow TDD you will create a stream of documentation that covers the entire system in a perfect way for developers to interpret.

Writing Unit Tests IS NOT FUN

  • already know the code works, why write tests now? someone requires it...ho hum, fine
    • in this scenario, it sucks to even bother writing the unit tests
    • it doesn't feel like time well spent, maybe even feels like a waste of time
    • most of all, you will come across code that is hard to test--resulting in holes!!
      • it's hard to test, because it wasn't designed to be testable
      • and the effort to make it testable seems to large
      • now there is a hole left in the test suite
    • IF YOU RUN A TEST SUITE FULL OF HOLES, YOU CANNOT TRUST THE TEST SUITE
  • but with TDD ...
    • takes an annoying necessity (writing unit tests for code I already know works), and makes it fun
      • a constant "yes, I'm a programmer!" feeling lol
    • impossible to write a function that's hard to test--you have to design for testing
    • it's easy to test because it's decoupled--a nice side effect of TDD is natural decoupling
    • PRODUCTION CODE COVERAGE

Demo - Prime Factors Kata

Interesting note of a result of the TDD approach he took:

  • he did not plan an algorithm
  • he simply started defining test cases, and making them pass one-by-one
  • eventually with enough test cases providing enough boundaries, the solution that got the test suite to pass was the final algorithm

TDD is a tool/discipline to help incrementally derive solutions to problems

Questions

How would you approach applying TDD to randomized testing or property/model-based testing

  • in functional languages there are test frameworks that allow specifying type structures, then the testing tool invents random values which are constrained to that spec
  • if you have these tools (e.g. fscheck), learn and take advantage of these tools--they are powerful
  • this is a valid thing to do and you should learn how to do it

Legacy Code

  • there is no easy/quick fix with legacy code; it is a long-term discipline to gradually bring legacy code under the level of control that TDD provides
  • Michael Feathers book "Working Effectively with Legacy Code" is a good intro to this discipline

Speed concerns--test-run times and compilation/build times

  • make the feedback loop as fast as possible
  • any test that slows you down needs to be looked at and addressed
  • (brief mention of testing a REST API--these should be tested for proper comm with internals, not business rules)

Mocking

  • mocking is helpful; it is important--especially at the architectural boundaries of the system
  • his preference is to do it himself
  • his preference is property/value-based testing inside modules, and mocking across boundaries
    • btw mocks speed things up enormously

Is 100% test coverage a fools errand?

  • 100% test coverage is a virtually impossible goal to achieve
  • GUI for example is especially challenging
  • a pattern
    • "the humble object pattern"
    • this is the way that we separate the things that are testable from those that are difficult to test
    • typically this is needed at physical boundaries to the system--e.g. screen, device driver
  • for all code that you can test you should
  • you don't have to test all code explicitly--you can test quite a bit indirectly

Regarding a blog post by Bob

  • Q: You stated that a compilation error is a failure (law 2); what do you think about languange features that test such things as null-safety for you
  • A:
    • wrote a blog post about his concern about a trend in modern languages (e.g. Kotlin, Swift, etc.)
      • the trend is going deeper into static type-checking such that we couple ourselves to the compiler
      • can be difficult to create a small set of independently deploy-able modules
        • e.g. when we change a type and have to recompile everything
    • to the note on static type-checking resulting in some tests no longer being necessary
      • when we write unit tests, we don't test those things--e.g. we don't test types
      • what we test is system behavior--which types do not specify
        • types specify a format or structure of data
        • unit tests test the operation
      • so when you write a unit test, you directly test operation, you indirectly test types
      • so idk that adding stronger static typing helps you avoid tests
  • assistant followup
    • Q: not sure why you are making a point that rebuilding a whole solution for a type change is bad--it's inevitable
      • seemed like assistant wasn't getting his point that the compile-time type-checking can become an unwanted limitation to flexibility
    • A: we don't want to go too far on static type-checking
      • a benefit to dynamic is the runtime checking allowing test changes without need to recompile everything every time
      • over the past 40-ish years, languages have crossed the center of the scale from dynamic to static type-checking
        • there are pros and cons on both sides
        • my blog was observing that recently, the swing has been to the static side, and that we will swing back again
      • microservices became popular due to the HTTP (essentially string) communication at boundaries allowing modular deployment
        • but we were just trying to escape type-checking
        • a key aspect to his point is copilation-time VS runtime type checking
      • if we just had a slightly more forgiving language, we could still achieve independent builds and deployments without MSA
⚠️ **GitHub.com Fallback** ⚠️