Monday, August 15, 2016

Kata: Bank Account in Clojure using outside-in TDD with Component and Midje

I did the Printing Account Statement subset of the Bank Account kata in Clojure using Component and Midje.

I started the kata in a Barcelona Clojure Developers event.

The truth is that, since I was learning how to use the Component library, I didn't use TDD.

Instead I worked on the REPL to get everything in place and make it work.

Then I wrote the tests I would have liked to write if I had used outside-in TDD with Midje.

I find that, when I'm learning something new, it works better for me what Brian Marick describes in these tweets: Now I'll show you the tests I actually wrote afterwards, in the order I would have written them doing outside-in TDD.

This is the acceptance test I would have started with:

(ns bank-account.acceptance-test
(:require
[midje.sweet :refer :all]
[bank-account.test-helpers :refer [output-lines make-dates]]
[com.stuartsierra.component :as component]
[bank-account.factories :as factories]
[bank-account.account :as account]))
(unfinished date-fn)
(facts
"printing an account statement"
(let [config {:format {:date-format "dd/MM/yyyy"
:separator "||"
:num-decimals 2
:header "date || credit || debit || balance"}}
dates (partial make-dates "dd/MM/yyyy")
account-system (assoc (factories/make-system config)
:transactions
(factories/in-memory-transactions #(date-fn)))
account (-> account-system component/start :account)]
(do
(account/deposit! account 1000)
(account/deposit! account 2000)
(account/withdraw! account 500)
(output-lines
account/print-statement account))
=> ["date || credit || debit || balance"
"14/01/2012 || || 500.00 || 2500.00"
"13/01/2012 || 2000.00 || || 3000.00"
"10/01/2012 || 1000.00 || || 1000.00"]
(provided
(date-fn) =streams=> (dates ["10/01/2012" "13/01/2012" "14/01/2012"]))))
Then I would have written this unit test for Account:

(ns bank-account.account-test
(:require
[midje.sweet :refer :all]
[midje.open-protocols :refer [defrecord-openly]]
[com.stuartsierra.component :as component]
[bank-account.account :as account]
[bank-account.factories :as factories]
[bank-account.transactions-operations.transactions-operations :as transactions-operations]
[bank-account.statement-printing.statement-printer :as statement-printer]))
(unfinished register!)
(unfinished balanced-transactions)
(unfinished print-statement)
(defrecord-openly FakeTransactions []
transactions-operations/TransactionsOperations
(register! [this amount])
(balanced-transactions [this]))
(defrecord-openly FakePrinter []
statement-printer/StatementPrinter
(print-statement [this balanced-transactions]))
(defn new-account [transactions printer]
(-> (factories/account)
(merge {:transactions transactions
:printer printer})
component/start))
(facts
"about account operations"
(fact
"it registers deposit transactions"
(let [fake-transactions (->FakeTransactions)
an-account (new-account fake-transactions :not-used)]
(account/deposit! an-account 50) => irrelevant
(provided
(register! fake-transactions 50) => irrelevant :times 1)))
(fact
"it registers withdrawals transactions"
(let [fake-transactions (->FakeTransactions)
an-account (new-account fake-transactions :not-used)]
(account/withdraw! an-account 100) => irrelevant
(provided
(register! fake-transactions -100) => irrelevant :times 1)))
(fact
"it prints the transactions in the statement"
(let [fake-transactions (->FakeTransactions)
fake-printer (->FakePrinter)
an-account (new-account fake-transactions fake-printer)]
(account/print-statement an-account) => irrelevant
(provided
(balanced-transactions
fake-transactions) => ...some-balanced-transactions... :times 1
(print-statement
fake-printer
...some-balanced-transactions...) => irrelevant :times 1))))
And these are the ones for InMemoryTransactions, ConsoleStatementPrinter and NiceReverseStatementFormat:

(ns bank-account.in-memory-transactions-test
(:require
[midje.sweet :refer :all]
[midje.open-protocols :refer [defrecord-openly]]
[com.stuartsierra.component :as component]
[bank-account.factories :as factories]
[bank-account.transactions-operations.transactions-operations :as transactions]
[bank-account.test-helpers :refer [make-date]]))
(unfinished date-fn)
(defn new-in-memory-transactions [date-fn]
(component/start (factories/in-memory-transactions date-fn)))
(fact
"about transactions"
(facts
"in memory"
(fact
"returns balanced transactions lines for all registered transactions"
(let [date (partial make-date "dd/MM/yyyy")
first-transaction {:amount 1000 :date (date "10/02/2016")}
second-transaction {:amount 1500 :date (date "13/05/2016")}
third-transaction {:amount -500 :date (date "14/08/2016")}
in-memory-transactions (new-in-memory-transactions #(date-fn))]
(do
(transactions/register! in-memory-transactions (:amount first-transaction))
(transactions/register! in-memory-transactions (:amount second-transaction))
(transactions/register! in-memory-transactions (:amount third-transaction))
(transactions/balanced-transactions in-memory-transactions))
=> [(assoc first-transaction :balance 1000)
(assoc second-transaction :balance 2500)
(assoc third-transaction :balance 2000)]
(provided (date-fn) =streams=> [(:date first-transaction)
(:date second-transaction)
(:date third-transaction)])))))
(ns bank-account.console-statement-printer-test
(:require
[midje.sweet :refer :all]
[midje.open-protocols :refer [defrecord-openly]]
[com.stuartsierra.component :as component]
[bank-account.factories :as factories]
[bank-account.statement-formatting.statement-format :refer [StatementFormat]]
[bank-account.statement-printing.statement-printer :as printer]
[bank-account.test-helpers :refer [output-lines]]))
(unfinished format-statement-lines)
(unfinished header)
(defrecord-openly FakeFormat []
StatementFormat
(header [this])
(format-statement-lines [this statement-lines]))
(defn new-console-printer
([format print-fn]
(component/start (merge (factories/console-printer)
{:format format
:print-fn print-fn})))
([format]
(new-console-printer format identity)))
(fact
"about printing statements"
(fact
"it asks the format for the header"
(let [fake-format (->FakeFormat)
a-printer (new-console-printer fake-format)]
(printer/print-statement
a-printer :not-used-in-this-test) => irrelevant
(provided
(header fake-format) => irrelevant :times 1)))
(fact
"it asks the format to format all balanced transactions"
(let [fake-format (->FakeFormat)
a-printer (new-console-printer fake-format)]
(printer/print-statement
a-printer
...some-balanced-transactions...) => irrelevant
(provided
(format-statement-lines
fake-format
...some-balanced-transactions...)
=> ...some-formatted-statement-lines... :times 1)))
(fact
"it prints the header and formatted lines"
(let [some-header "some-header"
some-formatted-statement-lines ["statement-line-1" "statement-line-2"]
expected-output-lines (cons some-header some-formatted-statement-lines)
fake-format (->FakeFormat)
a-printer (new-console-printer fake-format println)]
(output-lines
printer/print-statement
a-printer
...some-balanced-transactions...) => expected-output-lines
(provided
(header fake-format) => some-header
(format-statement-lines
fake-format
...some-balanced-transactions...) => some-formatted-statement-lines))))
(ns bank-account.nice-reverse-statement-format-test
(:require
[midje.sweet :refer :all]
[midje.open-protocols :refer [defrecord-openly]]
[com.stuartsierra.component :as component]
[bank-account.factories :as factories]
[bank-account.statement-formatting.statement-format :as statement-format]
[bank-account.test-helpers :refer [make-date]]))
(defn- new-nice-reverse-format [config]
(component/start (factories/nice-reverse-statement-format config)))
(fact
"about formatting statements"
(facts
"using NiceReverseStatementFormat"
(let [config {:date-format "dd/MM/yyyy"
:separator "||"
:num-decimals 2
:header "date || credit || debit || balance"}
date (partial make-date (:date-format config))
nice-reverse-format (new-nice-reverse-format config)]
(fact
"it returns the configured header"
(statement-format/header nice-reverse-format) => (:header config))
(fact
"it formats the statement lines"
(let [balanced-transactions [{:balance 1000 :amount 1000 :date (date "10/02/2016")}
{:balance 2500 :amount 1500 :date (date "13/05/2016")}
{:balance 2000 :amount -500 :date (date "14/08/2016")}]]
(statement-format/format-statement-lines
nice-reverse-format
balanced-transactions)
=> ["14/08/2016 || || 500.00 || 2000.00"
"13/05/2016 || 1500.00 || || 2500.00"
"10/02/2016 || 1000.00 || || 1000.00"])))))
You can check the rest of the tests and code in this GitHub repository.

Doing this kata I learned and practiced how to use Component.

I also learned how to use Midje's defrecord-openly and provided macros to mock protocols which helped me correct something I did wrong in another kata.

No comments:

Post a Comment