Test-Driven Development
Fleeting- External reference:
- External reference:
- External reference:
- External reference:
- External reference:
- External reference:
- External reference: https://doc.rust-lang.org/book/ch12-04-testing-the-librarys-functionality.html
- External reference: https://en.wikipedia.org/wiki/Test-driven_development#Test-driven_development_cycle
test first programming, with the development not doing anything more that make the tests pass => The tests actually drive the development.
in a nutshell
To the best of my understanding it is about:
- writing the simplest code,
- that fulfils the promises,
- expressed by the tests,
- while being compatible with our limited working memory,
- and trying to be as intellectually honest as possible,
Here, simplest means using the Occam’s razor: all other things being equal, provide the simplest code, the one that will bring fewest potential bugs with it. The simplest code is no code at all, right? But chances are that we have made some promises about our program and that no code at all won’t fulfil them. Therefore, we need something to tell we, every time we look at it, whether our program fulfil our promises, and this for each promise we have made.
But, focusing on ALL our promises at once is likely to overflow our limited working memory, hence we most likely want to focus on one thing at a time.
Actually, even making sure the code fulfils the promises and making the code “elegant” is likely to be too much cognitive load. So let’s even split this in two.
Also, we could start by writing some code and then try to express the promise once we have a clearer mind. But this would make us primed into thinking the promise in a way that satisfies the code. We definitely don’t want that and want to prime our mind on the problem and afterwards find a solution to that problem. Yet, we won’t have a clear view of the problem until both the problem and the solution will be written, so this work or writing the promise and then writing some code is actually a loop of going back and forth between the two.
Eventually, we get to this procedure:
- focus on ONE promise,
- write something that tells us whether the promise is fulfilled or not,
- starting doing this before writing some code is important to avoid being primed,
- write some code, until the stuff created at step 2 tells this is ok,
- actually, there is a loop between 2 and 3, because when doing the code, it will become clearer that the stuff at step 2 is not that precise and it should be improved,
- we focus here on making the code work, nothing else, so that we keep a clear mind.
- take a step back, look at the code, and make it more elegant.
some older notes
This method is supposed to let the design emerge from the action of thinking the tests. In contrast, BDD makes the specification emerge from the tests.
This method stresses the fact that the implementation should fulfil the tests and only the tests. Hence the name “driven”: the development is only driven by the test, nothing else. This avoids over engineering, produce an helper for refactoring and make the system writer think about what the system is about.
It is important to separate writing the test and writing the code so that we separate the time we make the code work and the time we make the code more elegant.
This video give several hints about how to use it
https://youtu.be/aebv1z80vSM says that we have biases when we wrote the code prior to writing the test in favor of testing what we know of the code. Writing the test prior to the code avoids such bias.
This method is all about making the design emerge from the code. Writing the tests up front is only a side effect due to the fact we would be biased otherwise. Thus, defining this method by « simply write the test before the code » is defining on a single side effect of the method and missing the whole point.
It looks like dogfooding, in the sense it helps the developer think like the end user, when thinking about the test without making hypothesis about how the code works.
The tests should mostly be about the exposed API.
Writing tests in TDD is like explaining the application to someone else. Given this situation, When I do this or that, It should do this and that. By looking at the tests, one should be able to understand what the program is meant to do. This provides a refactoring barrier, in which we can feel confident changing the implementation and have the test tell us when we broke the story.
Also, writing a test first is a good way to go beyond the blank page syndrome. We have a failing test that we are thriving to make pass. It’s a good place to start.
Making the test pass becomes the short time outcome. (red/green development)
Also, with time and the tests suite increasing. Some of the first tests will become redundant, as we gain in abstraction and write more appropriate tests. We should then delete the redundant tests.
Test-driven development (TDD) process. This software development technique follows these steps:
Write a test that fails and run it to make sure it fails for the reason you expect. Write or modify just enough code to make the new test pass. Refactor the code you just added or changed and make sure the tests continue to pass. Repeat from step 1!
— https://doc.rust-lang.org/book/ch12-04-testing-the-librarys-functionality.html
The following sequence is based on the book Test-Driven Development by Example:[2]
Add a test
The adding of a new feature begins by writing a test that passes iff the feature’s specifications are met. The developer can discover these specifications by asking about use cases and user stories. A key benefit of test-driven development is that it makes the developer focus on requirements before writing code. This is in contrast with the usual practice, where unit tests are only written after code.
Run all tests. The new test should fail for expected reasons
This shows that new code is actually needed for the desired feature. It validates that the test harness is working correctly. It rules out the possibility that the new test is flawed and will always pass.
Write the simplest code that passes the new test
Inelegant or hard code is acceptable, as long as it passes the test. The code will be honed anyway in Step 5. No code should be added beyond the tested functionality.
All tests should now pass
If any fail, the new code must be revised until they pass. This ensures the new code meets the test requirements and does not break existing features.
Refactor as needed, using tests after each refactor to ensure that functionality is preserved
Code is refactored for readability and maintainability. In particular, hard-coded test data should be removed. Running the test suite after each refactor helps ensure that no existing functionality is broken.
Examples of refactoring:
- moving code to where it most logically belongs
- removing duplicate code
- making names self-documenting
- splitting methods into smaller pieces
- re-arranging inheritance hierarchies
Repeat
The cycle above is repeated for each new piece of functionality. Tests should be small and incremental, and commits made often. That way, if new code fails some tests, the programmer can simply undo or revert rather than debug excessively. When using external libraries, it is important not to write tests that are so small as to effectively test merely the library itself, unless there is some reason to believe that the library is buggy or not feature-rich enough to serve all the needs of the software under development.
— https://en.wikipedia.org/wiki/Test-driven_development#Test-driven_development_cycle
from –
separate the time we make the code work and the time we make the code more elegant
-
External reference:
Because both require some amount of cognitive load, I suggest letting our brain relax for a few hours in between making the code word and making it more elegant.
Also, using dogfooding helps, because we can make first something that “works”, then try it ourselves and feel how inelegant it is. This gives us the incentive to make it more elegant.
It’s strange to realize how simple the code can be when we make it elegant and how we could miss that simplicity when we were focused on making it work. Somehow, we don’t have the brain power to do both at the same time.
virtuous circle
In the long run, we tend to apply this mantra for the code but also for the test. There is a kind of virtuous circle where a stable test battery supports making the code elegant and a stable code supports making the test battery more elegant.
tragedy of TDD
When we have a lot of transparency in the team. As soon as the test passes, the person in charge of the product owner role starts thinking that the task is done and the team is pushed strongly towards the next piece of code.
Therefore, the team pretending to apply TDD and still always applying exceptions falls into the feeling good bias.
Notes linking here
- “utiliser un marteau piqueur pour enfoncer un clou”
- a TDD way of thinking
- behavior-driven development
- Brain Adapted Development
- braindump et tdd
- builder, user or architect
- definition of done driven development
- exploration and then exploitation development
- is TDD about learning the art of writing “wrong code” right?
- is tdd possible in companies?
- naive interpretation fallacy
- tdd is hard but worth it
- tdd vs bdd
- test first programming
- verified/proved promised contracts based programming
- working code, then elegant code