There are already a few different styles of TDD out there. Still, though, my feeling is something is missing in the realm of test-first or test-driven development. I tried to explain that in my posting introducing the „Hamburg style TDD“. And then I showed what that meant in practice in a second article applied to the Diamond kata.
But knowing how hard it is to get the nuances (or even the obvious aspects) of a method across in writing (or with a video), I think I need to provide more examples of the application of Hamburg style TDD.
As a second example I’m choosing the kata Macro Emrich used to compare the established TDD styles: the Bank kata. If you like, check out Marco’s code samples in this presentation he did at the DWX 2019 conference.
Following I’ll share my version of the kata like I did with the Diamond kata. It differs from the Diamond kata in quite some aspects. Problem solving requires some breadth of approaches. Red-green-refactor + KISS is too simplistic.
This is what I’ve distilled from descriptions of the Bank kata I found on the web. It’s a bit more specific than usual, I guess. But I like it that way. Without specific requirements software development is bound to become even more complex.
An excerpt from my in-project
.md documentation of my „stream of consciousness“ while working on the problem:
A user should be able to deposit/withdraw funds from a bank account (object). At any time a statement can be "printed" (requested) listing the timestamped transactions and the updated account balance. Example: ` Date Amount Balance 1.7.2019 +1000 1000 2.7.2019 -200 800 3.7.2019 +500 1300 ` Integer amounts are sufficient as are transaction timestamps with dates only. No transaction is rejected, even if it leads to a negative account balance.
With the problem description in hand I sit down and analyze it. I log my thoughts in a
.md file. (The more I do that, the more I like it. It makes my thinking open for scrutiny.)
## Analysis Essentially the problem is about an event stream for which a report is generated. There are two commands: * deposit * withdraw And a single query: * print statement For testing purposes and also as to not dilute the responsibility of the account the statement should not really be printed (output to a device), but just generated as a data structure. For the timestamp the current time is used, ie. a dependency to the system is needed. Transactions will not be persisted. An account's state will be limited to the lifetime of it's object. ### 1. Functions #### Deposit command ` void Deposit(int amount) ` #### Withdraw command ` void Withdraw(int amount) ` #### Print statement query ` Statement PrintStatement() ` #### Classes The request handling functions can be aggregated by a single class `Account`. More classes are needed for the statement if access to the statement details is needed: ` class Statement Transaction* Transactions class Transaction DateTime Date // set by the Account class int Amount int Balance ` ### 2. Test Cases ` Input | State || State' Amount | Date | Balance || Balance' +100 1.1.19 0 100 +200 1.1.19 100 300 -50 2.1.19 300 250 -300 3.1.19 250 -50 ` This development of an account will be expressed in a growing statement (output) after each transaction: ` v1 1.1.19,+100,100 v2 1.1.19,+100,100 1.1.19,+200,300 v3 1.1.19,+100,100 1.1.19,+200,300 2.1.19,-50,250 v4 1.1.19,+100,100 1.1.19,+200,300 2.1.19,-50,250 3.1.19,-300,-50 `
This time I came up with an acceptance test case of my own. It’s exercising not just one function, though, but all functions I derived from the requirements. The acceptance test thus is more like a whole use case.
Also note that this problem leads to several classes. In the Diamond kata classes were of no concern. But here they are needed to aggregate functions and data.
But the modularization is straightforward. It’s quite obvious, I’d say, because it is directly tied to the user’s needs. That’s why I would not say the classes are a product of design. I’m still in the analysis phase.
Maybe noteworthy is that I decided to not deliver a formatted textual account statement. Why should an account (object) care about report layout? It delivers a statement as a data structure some other part of a program can turn into a neatly layouted report.
Finally I encode my understanding:
There’s only one thing strange about this, I guess: why that kind of constructors on the
During analysis I identified a dependency of my system under test (SUT, or system under development): It depends on the current date since the statement contains a date in each transaction, but requesting the transactions (
Withdraw) does not pass in a date.
How could an automated test running on an arbitrary date have an expectation regarding transaction dates? Without making „date acquisition“ configurable that would be hard. Hence the public ctor for production, and the internal for testing. The public one sees to that the current date is used; but the acceptance test uses the internal one to pass in a substitute function to deliver a date from a fixed set for each transaction.
That makes the test case a kind of whitebox test because it encodes an assumption about how many dates are required. But in this case that’s not severe, I’d say. Four transactions, four dates: that’s pretty obvious.
Starting from the analysis I do some design. That’s not much, though. The problem is very simple. It’s just that I stop for a moment to gather my thoughts…
## 2. Design The `Account` class pretty much is an event stream with an aggregator attached. * The commands add events to the stream. No checking is needed, all amounts are ok. But the date is added to the tx. * The statement is an aggregation of the events in the stream where the balance is constantly updated. The main functionality is statement generation. And within that it's updating the balance. That suggests a function (or even class of its own). The event stream is a simple append-only list for (internal) transcactions. // Open question: What should happen if a negative amount is passed to // `Deposit()` or `Withdraw()`?
After that I feel comfortable to start the implementation.
This is an article about some form of test-first development. So what I’m doing here when I start the implementation of the
Account class might seem horrible to you: I write logic without a test. I start with production code right away.😱
But after my design recording transactions (functions
Withdraw) seems so simple that I don’t see much of a danger therein.
There really is not much to test anyway. And if I wanted to test the effect of each function I would need to look under the hood of
Account; it would only work with more whiteboxing.
So I reason that in the end the correctness of transaction recording will show up immediately during statement production: If statement production is working, but delivers the wrong statements, then something must be wrong with recording transactions. In that case I would closer at the transaction methods.
One word about the
Transaction class inside of
Account: It looks similar to the same named class inside of
Statement. Why two classes for transactions? Because their „nature“ (or purpose) is different.
Statement.Transaction is visible from the outside. It’s a class used for interaction with the system under development (a DTO). It’s subject to the user’s whim at any moment. I expect it to change due to changing requirements quite often.
Account.Transaction on the other hand is an event object. It’s for recording what’s happening to the state of an
Account object. (Never mind that it’s not phrased in the past tense.) This class is purely internal, an implementation detail. It’s not under direct stress from user requirements. I expect changing requirements not to lead to changes there, rather more events will be needed.
Two classes with the same name is simply a matter of decoupling. That’s a good architectural style. It’s clean code in my view.
Or let me say it the other way round more bluntly: Clean code does not mean the least amount of code possible to implement some runtime behavior! Just because you see more code than you think is technically needed, does not mean it’s smelling. The question always is: Why? Why more core than fore the bare functional necessity? And the answer has to show that future readers are kept in mind.
Yes, future readers over future requirements. It’s not that I don’t value future requirements on the right, but I find concern for future readers more important.
Printing the statement
Printing the statement is what the whole kata is about. It’s the crucial part and I want to get it right. Also I want to be able to work on it straightforwardly and independently.
If I wrote a test for just the
PrintStatement function, though, I would need to open the
Account class in order to gain control over its state;
PrintStatement is relying on that.
To avoid this hassle I decide to switch to TDD as if you meant it (TDDaiymi) from Keith Braithwaite. That means I use tests as my sole development environment; I won’t touch the production code until I’ve finished the functionality.
Also I’ll be moving forward with incremental tests since I’m not really sure how to solve this problem.
Here’s my first iteration: What should the statement look like if there are no transactions yet? It should be empty.
I know this code looks pretty stupid. But it „simulates“ the situation in production: there are no transaction events (
new Account.Transaction). And the statement’s number of transactions mirrors that.
As simple as lines 13 and 14 might look, they are crucial and I expect them to stay the same in next test cases. They are like a seed to grow the solution from. There is a small insight encoded in them.
The next incremental test is about just one transaction event. I set that up with fixed data in line 23:
Line 24 is the same as 13 in the first test. But it’s followed by a manual mapping of the transaction event to the statement transaction.
Line 28 is the only noteworthy line here: It could be reduced to
accountTransactions.Amount, but by including
0 + I make clear right away that I know the transaction’s
Balance is an accumulating value.
When doing TDDaiymi I actually often create new test cases by copying previous test cases and only change the expectation first. That way it’s immediately red and I’m forced to solve the problem: adjust the test input data, then adapt the logic. This is what I’m doing for iteration 3, too.
Now it’s two transaction events to „print“. The expectations (58ff) and input data (40ff) are set up accordingly. Line 45 and 56 still match 13 and 14 of the first iteration as expected.
And in between I copy the mapping from the previous test and adapt the second copy by changing the indexes.
Plus I need to think about how to carry over the balance from the previous statement transaction (line 54).
My impression is I’ve solved the statement printing problem. I know how it’s gonna work. Now I just need to generalize it. Currently the number of transaction events is hardcoded. This is along the lines of the Transformation Priority Premise, I guess: (constant->scalar) and (statement->recursion).
I refactor the hardcoded solution to a variable solution:
The basic mapping stays the same (lines 50 to 54), but I move the balance update up because I want to use a variable for it (line 49).
The loop around the mapping is trivial: for each transaction event a statement transaction is created. The same as before, just automatically done.
With the refactored/generalized logic still getting the test to green I extract it to the production code. I’m confident it will work there, too:
The only adjustment I need to make is changing the source of the transaction events to the
Account class’ global state
PrintStatement filled in I check the acceptance test. It should be green now - if the implementation of the transaction functions was correct.
And indeed it is!🎉
What’s left is to restrict the accessibility of functions and classes to
private where necessary. The surface of the production code should be minimal.
That’s only affecting the
Account.Transaction class which I set to
internal while going through the TDDaiymi iterations.
Now that it’s hidden, however, the TDDaiymi test becomes flagged. The incremental tests cannot be compiled anymore. Their true nature is revealed: they were scaffolding tests all along. That means they now have to be deleted.
The acceptance test remains as the only test. That makes me free to refactor the solution below the surface at any time without breaking tests targeting details.
This problem required a somewhat different treatment compared to the Diamond kata: not writing tests for functions, doing TDDaiymi… That might feel new or uncomfortable to you. But I stand by my opinion: it’s legitimate, it’s even easier than sticking to one of the other schools of TDD.
In the end it’s all about the result. How straightforwardly can you move towards functional and clean code? If ADC and TBC can help, then you should employ them.
But Hamburg style TDD actually is not in opposition of Chicago style etc. Rather it’s embracing them all - and adding a twist. This twist is the recommendation to do explicit analysis and explicit design and to avoid functional dependencies. And that in turn leads to tests with no (or less) need for substitutes, i.e. less complexity.