3 Laws Of TDD - egnomerator/misc GitHub Wiki
The 3 Laws of Test Driven Development (video here)
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
- 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
- bad
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
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
- takes an annoying necessity (writing unit tests for code I already know works), and makes it fun
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
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
- wrote a blog post about his concern about a trend in modern languages (e.g. Kotlin, Swift, etc.)
- 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
- Q: not sure why you are making a point that rebuilding a whole solution for a type change is bad--it's inevitable