I'm putting here some of the things Sandro said in his 3-part session on Outside-In TDD (see the github repo here), and highlights some parts that are significant to me.
Sandro Mancuso on Outside-In TDD
Part One: https://www.youtube.com/watch?v=XHnuMjah6ps
TDD does not lead to a good design if you don't know what a good design looks like.
The way I code in this video is the way I normally code but not the way I normally teach.
Some of you will notice that I skip the traditional refactoring steps a few times and I do quite a lot of design upfront (or "just-in-time design" as I prefer to call it) without much feedback from my code.
This is normally the case for many experienced TDD practitioners who use outside-in TDD.
If you are starting with TDD don't follow the advice in this video blindly; you should probably use the classicist approach: design less upfront and rely more on the feedback from your code in order to evolve your design.
In order to demonstrate how I normally code, I chose the bank kata: so my task is to create a bank application where I have an
Account class which I can deposit some money, withdraw some money and call the print statement, so I can print my bank statement to the console.
The acceptance criteria for that is: after depositing and withdrawing a few times I should print my statement; the statement should have three columns: the date of the transaction, the amount of the transaction and the running balance.
The transaction should be displayed in my statement in reverse chronological order, like you see in your normal bank statement online.
To make things a little bit more interesting I have a few constraints and I have a starting point: my starting point is an
Account class has three methods:
deposit, that receives an amount
withdraw, that also receives an amount
printStatementmethod, which prints the account statements
Note that all the three methods are commands: they they don't return anything.
Also, I cannot add any other public method to this class and I cannot change the signature, which means that I cannot have a query method, so when I'm testing this class I need to call the command methods but I need to test the behavior: I cannot query the state of the account and test for it.
Also, in order to keep this exercise simple and short I'll be using strings as dates and integers as amounts, so I don't need to deal with date formatting and big decimal or anything like that, and I don't need to worry too much about the formatting of the thing that is being printed to the console.
Here I have my readme file and has the description of the problem and so this is what I need to achieve: https://github.com/sandromancuso/bank-kata-outsidein-screencast/
I will start with an acceptance test first.
So I will create a package called
feature and inside this
feature package I'll create a
In order to write an acceptance test we need to identify the side effect: what are we testing?
In this case, what we want to do is to print all the transactions to the console, so this is what we want to test.
I'll treat the console as an external system the same way that I would treat the database, so normally if I have an external system I would have an interface to isolate my application from the external world.
I already decided that console will be some sort of an interface or will be a class as we represent the console, and, whatever I do, my acceptance tests will need to verify that the method
printLine in the
Console class was called a few times with all the information that I need.
I configured my IDE to always throw an
UnsupportedOperationException for new methods, because then every time that I run my tests I always know what's not implemented or what is left to be implemented.
Before I move on I would like to see my acceptance test failing for the right reason.
So, as my acceptance test is failing for the right reason, it's time to park the acceptance test and now this is the double loop of TDD: so first we start with an acceptance test, once the acceptance test is failing for the right reason then you go into the inner loop of TDD, that is the unit test.
So now I'm gonna start unit testing my code and then hopefully once I finish unit testing all the classes that I may have then these acceptance tests should go green.
Clearly I'm not injecting this mock anywhere so I should inject my
Console into my
Account, however I'm not quite sure if the
Account will be the one calling the console. I'm not quite sure how many abstractions I will have, I don't know what I will have between that
Account and the
Console, I don't know if I'll have more classes and that's the reason that I'm not gonna change this test right now, I'm not gonna inject this console.
I'm gonna go down to the unit level to start unit testing the
Account and then I will see if the
Console must be injected into the
Account or somewhere else.
So let's start with a unit test...
...I would like to start with the simplest test that I can possibly find.
The interesting thing about this exercise is that all the methods in the
Account class are commands, and according to the initial constraint I cannot change this interface, which means that I cannot change the signature of the three methods and I cannot add any other public method.
A common approach in the "classicistic" approach would be to have a query method, let's say
getBalance(): there are many problems with this approach.
First, you are exposing a query method just for the purpose of testing.
Also, why do you need a
getBalance at all when my requirements did not require a total balance? What I have in my statement is a running balance, that is very different from total balance.
Another thing: if I call deposit of 100, expose a method, say
getBalance to return 100, my test will go green but my implementation will do what? It will store that deposit in memory inside the same
So, when we are writing tests we need to be realistic, right?
One thing is to make the test goes green, another thing is to make the code useful, because there's no point in me providing a
getBalance somewhere in my
Account class: I would store my deposit in memory; if I bounce my application I lose all my data, that doesn't make sense.
Instead, I need to focus on: "What is the side effect of a deposit?", "What do I want to happen when a deposit is made?"
Another thing: I don't want to test my deposit through my
printStatement, because if I test my deposit at a unit level through my print statement, my unit test will be exactly the same as the acceptance test; so we need to think about these operations as separate operations.
A statements should have transactions, so transaction is a domain term, so basically what I'm expecting is to store deposit transactions, so that's what I should be testing for.
I need to do something that is not so common to many seasoned TDD practitioner. They would just start typing on the keyboard and of course they would try to get a query method there somehow, but Outside-In TDD is slightly different: we do design while we write the tests.
Those are all design decisions that we need to make.
Design is all about trade-offs.
It's not that we are trying to foresee the future, but we need to at least ask some basic questions, and these basic questions may give us some directions on design.
E.g if running balance is something that I can calculate on the fly this is already a hint to me that everything that can be calculated on the fly I would never store in an object. I will only do that if at some point I have some performance issues and then I need to keep that already calculated so I can easily print my statements but if I don't need to do that I probably won't store it, so I will calculate on the fly.
I'm wondering if I can defer some of these things... what if I just focus on delegating that and just say store a deposit to me.
I will defer the logic of creating transactions, I will just defer andn push the details of this operation a little bit further down into my system.
Part Two: https://www.youtube.com/watch?v=gs0rqDdz3ko
Should the Account know how to print its statements?
Let's look back at the requirements: the statement has a header, it has a current date and a running balance. We need to format the date, we need to know that there are three columns, we also need to formats numbers with two decimal, we need to decide to print a negative sign in front of our negative numbers, and we need to know how to print all of those data in the given layout (with pipes to define the rows).
So the first question that we should ask is: should the
Account know about all of that? Should the Account know how my statement is formatted? Is it its responsibility, so that we should change the account every time we want for example to add a new column or if we want to format dates in a different way?
Those are the questions that we should ask ourselves when we are doing outside in TDD.
Is the responsibility of the class under test to do all of that or is the responsibility of someone else?
In this case I don't think that the
Account should do all of that: account is a very high level class and should not know about the details of a statement, so who should know about that?
I believe that what we could do is to create a class that knows everything about statement and that in how to print statement: a
So at this point we made lot of design decisions:
- the first one was that a transaction repository will have an
allTransaction()method to have all transaction
- we created a
Transactionclass, which is the type that "binds" together a date and an amount
- we also decided that the
StatementPrinterwill handle statement details
I think that all the
Account methods are now at the same level of abstraction in terms of naming and implementation as well so there's not many details being done by any of the public methods which means that they are well balanced in terms of abstraction.
Acceptance Tests guides... (where's the next
When we do Outside-In, the Acceptance Tests play an important role: it's not only about making sure that the whole feature is done, but also it guide us in our development.
So now, in order to know what I need to do, I'll run my acceptance test again: when I run it, it fails and it has an exception
UnsupportedOperationException, which means that somewhere my collaborators have not been implemented yet.
So what I need to do next as part of the outside-in process is to write a unit test for my
In real life I would create an integration test to test the repository against a real DB, so
TransactionRepository would be an interface and I would implement a concrete (e.g)
For the sake of this exercise and to keep it short we'll have an in-memory repository.
Every time I store a deposit transaction, I need to record alongside the amount deposited also the current date.
One thing that is very important in test-driven development is that you cannot test what you can't control: this is extremely important, so system date is a thing that you cannot control.
That's another design decision in here: who should be responsible for retrieving the system date? I probably would like to have a class called
Clock...but before I do that, let's see what I want to assert on
Back to the system test (broken due to the wrong call to the
...okay in my so when I run my system test is now failing because it doesn't have a clock
In this case as I treat
clock as an external thing, something that I don't control, and I need to control, I will make it a mock.
Part Three: https://www.youtube.com/watch?v=R9OAt9AOrzI
Let's test the
Starts with the header.
"I don't like strings hard-coded in the middle of my code"
Then, test for the print of the statements
Now test fails for the right reason: https://www.youtube.com/watch?v=R9OAt9AOrzI?t=9m
It takes many minutes to go green (8 minutes), and, what worsts to me, in these 8 minutes many little things are done... so it sounds really a big step to me, not a tiny one.
Issue: the statements should be printed in reverse chronological order, while they are just printed in reverse order.
Implements the real Console
His version of the "Acceptance Test" is a little bit strage to me... two external deps are mocked out... but I guess the AT for him is not an end-to-end test but something that can be executed in a single process, in memory maybe.