Skip to content

Instantly share code, notes, and snippets.

@xpepper
Last active March 19, 2024 20:36
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save xpepper/66ced102032ad479b8170d9205754519 to your computer and use it in GitHub Desktop.
Save xpepper/66ced102032ad479b8170d9205754519 to your computer and use it in GitHub Desktop.
Sandro Mancuso on Outside-In TDD

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

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 up-front (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.

The Account class has three methods:

  • the deposit, that receives an amount
  • withdraw, that also receives an amount
  • a printStatement method, 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 PrintStatementFeature.

https://youtu.be/XHnuMjah6ps?t=4m

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.

https://youtu.be/XHnuMjah6ps?t=12m

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.

https://youtu.be/XHnuMjah6ps?t=13m29s

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.

https://youtu.be/XHnuMjah6ps?t=15m15s

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...

https://youtu.be/XHnuMjah6ps?t=16m06s

...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 Account class?

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?"

https://youtu.be/XHnuMjah6ps?t=19m40s

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 printStatement, my unit test will be exactly the same as the acceptance test; so we need to think about these operations as separate operations.

A statement should have transactions, so transaction is a domain term: so basically what I'm expecting is to store deposit transactions, 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.

https://youtu.be/XHnuMjah6ps?t=22m08s

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.

https://youtu.be/XHnuMjah6ps?t=25m30s

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.

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?

https://youtu.be/gs0rqDdz3ko?t=2m18s

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 StatementPrinter

https://youtu.be/gs0rqDdz3ko?t=9m00s

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 Transaction class, which is the type that "binds" together a date and an amount
  • we also decided that the StatementPrinter will 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 UnsupportedOperationException?)

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 guides 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.

https://youtu.be/gs0rqDdz3ko?t=12m00s

So what I need to do next as part of the outside-in process is to write a unit test for my TransactionRepository.

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) OracleTransactionRepository.

For the sake of this exercise and to keep it short we'll have an in-memory repository.

https://youtu.be/gs0rqDdz3ko?t=14m40s

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.

https://youtu.be/gs0rqDdz3ko?t=16m00s

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 TransactionRepositoryShould test.

Back to the system test (broken due to the wrong call to the TransactionRepository constructor)

https://youtu.be/gs0rqDdz3ko?t=25m30s

...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.

Let's test the StatementPrinter

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

https://www.youtube.com/watch?v=R9OAt9AOrzI?t=5m

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.

https://www.youtube.com/watch?v=R9OAt9AOrzI?t=17m

Issue: the statements should be printed in reverse chronological order, while they are just printed in reverse order.

Implements the real Console

https://www.youtube.com/watch?v=R9OAt9AOrzI?t=17m

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment