
TDD is a duck. Introduction to Quack-Driven Development. Modern waterfowl methodology for programmers
- Transfer

I suggest you familiarize yourself with a free translation of Jason Hatchers ' article “TDD (1) is' canard. Tell it to the Duck " or literally:" TDD - Duck. Say it to the duck. ” (2)
The author criticizes the “orthodox” TDD approach to development and offers an alternative.
And, yes, there is no typo in the title of the article (3) .
In part, the purpose of life is fun. Usually, the ability to concentrate on the creative side of programming gives programmers joy. Therefore, Ruby was created to make programmers happy.
Yukihiro Matsumoto (also known as "Matz"), 2000.
When explaining your code to someone, you should clearly explain those parts of the code that you previously considered obvious. During these explanations, you may receive new insights into the problem at hand.
Pragmatic Programmer: The Path from Apprentice to Master , 2000.
Is TDD dead?
I followed the discussion on " Is TDD dead? " With great interest, as I struggled with ideas that were summarized as " 3 basic TDD rules from Uncle Bob":
- It is forbidden to write any code other than what is needed to pass failed tests
- It is forbidden to write additional tests if the current set is enough to fail (a compilation check is also a test)
- It is forbidden to write additional code, except for code sufficient to pass the current failed test.
It always seemed to me that these imposed prohibition rules break the programmer’s natural behavior.
Training: bowling game
Uncle Bob demonstrates the three rules of TDD, showing how to implement a scoring mechanism in a bowling game. Let me show you the approach of Uncle Bob, clearly following the three rules of TTD: (4) :
- Check that you can create an instance of a bowling game (failed).
- Implement an empty bowling class (completed).
- Check that the created instance returns 0 points if the ball constantly falls into the gutter (failed).
- Implement a scoring function that always returns 0 (passed).
- Check that the number of points is 20 if 1 point is knocked down at each move (failed).
- Change the function of scoring: return the amount of knocked down pins (completed).
- Check that the score is correctly calculated when throwing a spar (additional throw) (failed).
- … (etc)
For me, only a very strange programmer will really follow the approach mentioned above.
Why test?
Although the use of TDD described above is excessive, there are many good reasons to write tests for your code:
- Documentation. To provide understanding by other programmers. (The documentation is obsolete. Tests do not become obsolete if you make sure they pass. They also describe the code as examples of its use, which are the best documentation for the programmer.)
- Design. Record product requirements and use them as boundaries for development.
- Security. Make it easier for other programmers to do things right.
- Debugging Reproduce errors before correcting their root causes. (It is always better to write a test that reproduces the error than to perform traditional debugging, as this ensures that this type of error does not reappear.)
- Agreement. Agree on function input / output formats.
- Security. Increase product confidence using dynamic, typeless languages.
- Motivation. Mark a small front of the work that you are going to do next. (When you do not know how to approach a new project, where to start, the red icon opposite the test will tell you what can be done here and now (correctly - make sure that the green icon is next to the test) )
- Focus. Prevent the occurrence of a bloated or excessively abstract implementation.
- Guide. Define the finish line so that you know when the work can be considered completed.
- Efficiency. Use the time allotted for manual testing, properly. (Often building and launching an application is several orders of magnitude more difficult than running tests. You don’t want to wait 20 minutes while your game is being compiled for the console and transferred to the SDK to see how it immediately crashes at the start. The test would reveal the error is much faster.)
Most of the reasons described above for writing tests are not explicitly related to the three TDD rules and the refactoring phase of the red-green-refactoring cycle (5) .
It's funny that some of the testing reasons that I give above are also good reasons for writing technical documentation. Actually…
Tests as documentation
... it is often said that tests are the best form of technical documentation.
Keeping documentation separate from code is bad practice because, often, documentation becomes obsolete right after it is written. For this reason, it (documentation) is often written in the form of comments, which can later be extracted and reformatted into documentation .
If tests are the best form of documentation, and if documentation should be embedded in code, then ... maybe tests should also be written next to the code? Of course, it follows if we divide them into two parts: validation and examples .
- Validation is being implemented. It captures the requirements, conventions and intentions of the programmer. This is the embodiment of a programmer’s conversation, where he explains the code to a colleague.
- The examples configure the states, invoke the code under test, and verify that the state has changed as expected. They are stored outside the implementation.
Thus, we kill two birds with one stone. Firstly, the validation, which carries a descriptive load, is right there in the code and is available to all developers in the context in which they are most useful. Secondly, validation can also be performed in a production environment.
Introduction to Quack-Driven Development
Quack-Driven Development, or QDD, allows the programmer to express the requirements, conventions, and intentions within the implementation itself.
This is a modern waterfowl methodology for software development.
The foundation of QDD is the idea of transferring the duckling method to code by replacing comments in the code with test statements. She (the idea) has a simple philosophy that does not limit, but informs us how to do the work:
- Understand the task.Do not confuse the problem and its solution. Always be sure that you are doing really important things. Read “Are Your Lights On” for inspiration.
- Trust your assumptions.It is better to do data validation in the interface. Do not clutter your business logic because you do not trust data.
- Respect the reader.Write code that other programmers and you can read (in a year). Write the code you like to read. Show, not tell.
The following implementation of QDD in Ruby is proof of the QDD concept. It allows programmers to “talk with a duck” in code using a “duck emoticon” (Q <), and demonstrates how I would use the QDD approach to implement an example of Uncle Bob's bowling game. Let's get started.
Lay the foundation of the application
class Game
def initialize
@frames = []
end
Q< "принимает количество сбитых кегель"
def roll(pins)
Q< "вызывается каждый раз, когда игрок бросает шар"
end
Q< "возвращает итоговый счёт игры"
def score
Q< "вызывается только в конце игры"
end
end
- Let’s think carefully before sketching the implementation.
- Four ducks are embedded in the code. They define four requirements in an explicit and verifiable manner.
Let's set the ducks
class Game
Q< :roll do
before "принимает количество сбитых кегель" do
expect(pins).to be_a(FixNum)
expect(1..10).to cover(pins)
end
within "вызывается каждый раз, когда игрок бросает шар" do
expect(@frames).to be_a(Array)
expect(1..10).to cover(@frames.length)
end
end
Q< :score do
within "вызывается только в конце игры" do
expect(@frames.length).to eq(10)
end
after "возвращает итоговый счёт игры" do
expect(retval).to be_a(FixNum)
expect(0..300).to cover(retval)
end
end
end
- Ducks are written in the same class, but can be saved in a separate file.
- They can be defined to run before a method call, inside a method call, or after a method has been executed.
- They are carried out in the scope of the method that is being tested, so local variables defined in the body of the method are available inside the "ducks".
- They use rspec waits to check state.
Add an implementation
class Game
def initialize
@frames = []
end
Q< "принимает количество сбитых кегель"
def roll(pins)
Q< "вызывается каждый раз, когда игрок бросает шар"
@frames << Frame.new if _start_new_frame?
Q< "фрейм, которому необходим бросок должен быть тут"
Q< "если мы на 10-м фрейме,фрйем может быть завершенным"
@frames.each do |frame|
frame.bonus(pins) if frame.needs_bonus_roll?
end
@frames.last.roll(pins) if @frames.last.needs_roll?
end
Q< "возвращает итоговый счёт игры"
def score
Q< "вызывается только в конце игры"
@frames.map(&:score).reduce(:+)
end
private
def _start_new_frame?
@frames.size == 0 ||
@frames.size < 10 && !@frames.last.needs_roll?
end
end
class Frame
def initialize
@rolls = [] ; @bonus_rolls = []
end
Q< "возвращает, ждет ли этот фрейм броска"
def needs_roll?
!_strike? && @rolls.size < 2
end
Q< "принимает количество сбитых кегель"
def roll(pins)
Q< "вызывается только когда фрейм ждет броска"
@rolls << pins
end
Q< "возврвщает ждёт ли фрейм бонусного броска"
def needs_bonus?
_strike? && @bonus_rolls.size < 2 ||
_spare? && @bonus_rolls.size < 1
end
Q< "принимает количество сбитых кегель"
def bonus_roll(pins)
Q< "вызывается только когда фрейм ждет бонусного броска"
@bonus_roll << pins
end
Q< "возвращает итоговый счёт по фрейму"
def score
Q< "вызывается только когда больше нет бросков"
@rolls.reduce(:+) + @bonus_rolls.reduce(0, :+)
end
private
def _strike?
@rolls.first == 10
end
def _spare?
@rolls.reduce(:+) == 10
end
end
- Please note that, in parallel with the refinement and development of the implementation, more ducks have been added. I have not shown their definition for brevity.
- The code was so aesthetic. For example, I would especially note that Game # score is calculated by summing up the points scored in each frame. I did this not because the code looks more universal and reusable, and not because it is easier to check, but because I will explain the essence of this code to a colleague exactly the way it (the code) is already written.
- Thinking about the implementation in the same vein, you may find that the scoring mechanism in real life contains an ugly hack: a player can throw a ball up to three times in the tenth frame. You should not implement this hack in the code, since, obviously, each frame consists of two different types of throws. Throws of the current frame and bonus throws that will be realized in the future when the current frame is completed. This separation was also dictated by aesthetics and serves to better understand the implementation.
Let's write some examples
describe Game do
let(:game) { Game.new }
it “возвратит 0 если шар ушёл в желоб” do
20.times { game.roll(0) }
expect(game.score).to eq(0)
end
it "возвратит 20 если все кегли забиты" do
20.times { game.roll(1) }
expect(game.score).to eq(20)
end
it "учитывает спэры" do
game.roll(5)
game.roll(5)
game.roll(3)
17.times { game.roll(0) }
expect(game.score).to eq(16)
end
it "учитывает броски" do
game.roll(10)
game.roll(3)
game.roll(4)
16.times { game.roll(0) }
expect(game.score).to eq(24)
end
it "возвратит 300 в случае идеальной игры" do
12.times { game.roll(10) }
expect(game.score).to eq(300)
end
it “должна работать с игрой, заданной в примере” do
[1,4,4,5,6,4,5,5,10,0,1,7,3,6,4,10,2,8,6].each do |pins|
game.roll(pins)
end
expect(game.score).to eq(133)
end
it “не должна работать, если игра некорректна” do
expect(game.score).to quack("вызывается только в конце игры")
expect(game.roll(11)).to quack("принимает количество сбитых кегель")
expect(game.roll(5.5)).to quack("принимает количество сбитых кегель")
expect(game.roll(nil)).to quack
expect(30.times { game.roll(0) }).to quack
end
end
- These are just standard rspec tests, but they also test ducks.
- They can be recorded before, after, or mixed with implementation.
- The first five tests are a mirror image of the tests from Uncle Bob's training. Their task is to manage the implementation, not to verify the correctness of the source code.
- We also checked the example that was given in the training requirements - incorrect use is also processed.
Ducks in the work environment
It would be foolish to throw away all this work, which was not easy for us as soon as we launch the product in a working environment. Maybe it’s better to continue to test our code live while users interact with our software product?
Why not! The behavior of ducks can be configured arbitrarily:
- Throw an exception : Throw an exception. The default behavior.
- Log : Log the error and continue.
- Debug : Go to the debugger.
- Ignore : Do nothing at all.
Ducks can also be easily commented out, turning them into dead ducks, as in the following example:
class Game
#< "принимает количество сбитых кегель"
def roll(pins)
#< "вызывается каждый раз, когда игрок бросает шар"
end
#< "возвращает итоговый счёт игры"
def score
#< "вызывается только в конце игры"
end
end
Note that even “dead ducks” serve as comments. Are the comments you write in code today just “dead ducks” waiting in the wings to “come back to life” through QDD?
Summary
I do not think TDD is dead. But I think the idea of writing tests to control development is one of many tools that should be used wisely:
- This tool does not work when I need to develop a prototype iteratively, when the requirements for the final product are not fixed.
- If requirements and a layout exist, it is better to start with the implementation of a prototype that reflects the requirements and layout rather than creating tests.
QDD is a great alternative to TDD. QDD separates the validation tests that are implemented in the implementation and the examples that are used to set the initial state and verify that the implementation correctly changes this state.
Three QDD Rules
- Create an implementation skeleton with “ducks” that capture the requirements, interface conventions, and intentions of the programmer.
- Write tests that test the “ducks” and cover the layout and write an implementation so that these tests pass. It doesn't matter in what order.
- Launch the “ducks” to “live” in the work environment, providing your clients with real examples, not your sophisticated specifications!
Benefits of QDD
QDD has two main advantages over TDD: it combines information about the requirements and intentions of the programmer directly in the implementation, and continues to test your code in a production environment.
And, as a bonus, the duck emoticon is simple, very cute.
I recommend that you add a QDD approach to your “waterfowl” methodology today, as it perfectly complements duck typing , beating a duck (6) and the duckling method !
Q <"Testing will never turn into a duck!"
- TDD - Test Driven Development - a software development technique through testing .
- Puns: “duck” - a false rumor, in the first case, a rubber duck for conversation - in the second.
- Another pun: The cascade model of software development (waterfall) is turning into a “waterfowl methodology” (waterfowl).
- To make it easier to understand what is at stake, check out the presentation by BowlingGameKata.ppt
- More in the section "Development Style" ru.wikipedia.org/wiki/Development_through_testing
- We use the expression monkey patch .