Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
In the previous post Iâve covered the basic ideas behind property-based testing. Here, Iâm going to TDD the diamond kata using that technique.
The post is heavily inspired (i.e. blatantly copied). So be sure to go say hi to Nat Pryce and Mark Seemann doing the same exercise[1][2] (links at the bottom in the references). Luckily Iâm going to use JavaScript and JSVerify. That way I can hide myself behind the âbut Iâm using a different stackâ excuse.
Also, Iâm gonna keep code snippets to a minimum. Should you be interested into more details, feel free to check the repo.
The diamond kata
As well described by Seb Rose, the problem statement is as follows:
Given a letter, print a diamond starting with âAâ with the supplied letter at the widest point.
A few examples are
Input: AOutput: AInput: BOutput: A B B AInput: COutput: A B B C C B B A
Ready, set, rock and roll
In the init commit I want to check the wirings. Thatâs why I use a generator that always returns 5 to check the isFive property.
// index.test.js
const jsc = require('jsverify')const mocha = require('mocha')const isFive = require('./index')
describe('TODO', () => { jsc.property('TODO', jsc.constant(5), isFive)})
// index.js
const isFive = number => number === 5module.exports = isFive
which of course is green
$ mocha index.test.js
TODO â TODO
1 passing (12ms)
âš Done in 0.52s.
The generator
Everything works, thus I can create the generator for the diamond kata. In particular, I need to generate characters in the A..ZÂ range.
Since Iâm not sure what to use, I decide to check what the jsc.asciichar generator returns
const debug = x => { console.log(x) return true}describe('diamond', () => { jsc.property('TODO', jsc.asciichar, debug)})
Notice the return true. That way the âpropertyâ debug never fails and I can check all the generated asciichar. Since by default JSVerify checks the property 100 times by generating 100 inputs out of the generator, IÂ see
$ mocha index.test.js
diamondTK.E
B<// ... up to 100 asciichars
â TODO
1 passing (16ms)
âš Done in 0.52s.
Not quite right, in fact, I need to generate characters in the A..Z range only. Unfortunately, JSVerify doesnât provide any generators out of the box that satisfy that constraint. Therefore, I create a custom one
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')const char = jsc.suchthat(jsc.asciichar, c => alphabet.includes(c))describe('diamond', () => { jsc.property('TODO', char, debug)})
This time we get the proper values
$ mocha index.test.js
diamondBLXBQVXBJSCPI// ... up to 100 chars in A..Z
â TODO
1 passing (19ms)
âš Done in 0.52s.
Notice that I could have moved the check inside the property
const property = c => { if (!alphabet.includes(c)) return true // ... test the property}
describe('diamond', () => { jsc.property('TODO', jsc.asciichar, property)})
but I would have made a mistake. In fact, in this case, JSVerify would call property 100 times with random jsc.asciichars. Therefore, only a subset of the generated input would get past the if. In other words, I would lose test coverage.
Property: diamond is not empty
The property that kicks off the exercise just checks the diamond has length different than 0 for any char.
jsc.property('is not empty', char, c => make(c).length !== 0)
Which I make green with
const make = char => 'whatever'
From the REPL
make(c) // for any c// => 'whatever'
Property: first row contains A
jsc.property( 'first row contains A', char, c => firstRow(make(c)).trim() === 'A')
Which I make green with
const make = char => ' A ' // padding is asymmetric
From the REPL
make(c) // for any c// => ' A '
Property: last row contains A
jsc.property( 'last row contains A', char, c => lastRow(make(c)).trim() === 'A')
Which is already green.
Property: first row has symmetrical contour
const firstRowHasSymmetricalContour = diamond => { const leadingElements = leading('A', firstRow(diamond)).length const trailingElements = trailing('A', firstRow(diamond)).length return leadingElements === trailingElements}
jsc.property( âfirst row has symmetrical contourâ, char, c => firstRowHasSymmetricalContour(make(c)))
Which I make green with
const make = char => ' A ' // padding is symmetric
From the REPL
make(c) // for any c// => ' A '
Property: rows have symmetrical contour
Well, not only the first row has a symmetrical contour. Letâs modify the property so that all of the rows are checked
const rowsHaveSymmetricalContour = diamond => diamond .split('\n') .map(rowHasSymmetricalContour) .reduce((acc, x) => acc && x) // [].every would be better here
jsc.property( 'rows have symmetrical contour', char, c => rowsHaveSymmetricalContour(make(c)))
Which is already green.
Property: rows contains the correct letters
const rowsContainsCorrectLetters = (char, diamond) => { const pre = alphabetUntilBefore(char) const post = pre.slice().reverse() const expected = pre.concat([char]).concat(post) const actual = diamond.split('\n').map(row => row.trim()) return expected.join() === actual.join()}
jsc.property( ârows contains the correct lettersâ, char, c => rowsContainsCorrectLetters(c, make(c)))
Which I make green with
const make = char => { const pre = alphabetUntilBefore(char) const post = pre.slice().reverse() const chars = pre.concat([char]).concat(post) return chars.join('\n')}
The duplication between test and production code is a bad smell. But I decide to leave it there.
From the REPL
make('C')// => 'A\nB\nC\nB\nA'
Property: rows are as wide as high
const rowsAreAsWideAsHigh = diamond => { const height = rows(diamond).length return all(rows(diamond).map(hasLength(height)))}
jsc.property( 'rows are as wide as high', char, c => rowsAreAsWideAsHigh(make(c)))
which I make green with
const makeRow = width => char => { if (char === 'A') { const padding = ' '.repeat(width / 2) return `${padding}A${padding}` } else { return char.repeat(width) }}
const make = char => { const pre = alphabetUntilBefore(char) const post = pre.slice().reverse() const chars = pre.concat([char]).concat(post) return chars.map(makeRow(chars.length)).join('\n')}
and from the REPL
make('C')// => ' A \nBBBBB\nCCCCC\nBBBBB\n A '
Property: rows except top and bottom have two identical letters
jsc.property( 'rows except top and bottom have two identical letters', char, c => internalRowsHaveTwoIdenticalLetters(make(c)))
which I make green with
const makeRow = width => char => { if (char === 'A') { const padding = ' '.repeat(width / 2) return `${padding}A${padding}` } else { const padding = ' '.repeat(width - 2) return `${char}${padding}${char}` }}
const make = char => { const pre = alphabetUntilBefore(char) const post = pre.slice().reverse() const chars = pre.concat([char]).concat(post) return chars.map(makeRow(chars.length)).join('\n')}
and from the REPL
make('C')// => ' A \nB B\nC C\nB B\n A '
Property: rows have the correct amount of internal spaces
jsc.property( 'rows have the correct amount of internal spaces', char, c => rowsHaveCorrectAmountOfInternalSpaces(make(c)))
which I make green with
const internalPaddingFor = char => { const index = alphabet.indexOf(char) return Math.max((index * 2) - 1, 0)}
const makeRow = width => char => { if (char === 'A') { const padding = ' '.repeat(width / 2) return `${padding}A${padding}` } else { const internalSpaces = internalPaddingFor(char) const internalPadding = ' '.repeat(internalSpaces) const externalSpaces = width - 2 - internalSpaces const externalPadding = ' '.repeat(externalSpaces / 2) return `${externalPadding}${char}${internalPadding}${char}${externalPadding}` }}
const make = char => { const pre = alphabetUntilBefore(char) const post = pre.slice().reverse() const chars = pre.concat([char]).concat(post) return chars.map(makeRow(chars.length)).join('\n')}
and from the REPL
make('C')' A \n B B \nC C\n B B \n A '
Unfortunately,rowsHaveCorrectAmountOfInternalSpaces in the test uses the following
const index = alphabet.indexOf(char)return Math.max((index * 2) - 1, 0)
I donât like this duplication. Therefore, I decide to test the external space (and not the internal one).
Property: rows have the correct amount of external spaces
jsc.property( 'rows have the correct amount of external spaces', char, c => rowsHaveCorrectAmountOfExternalSpaces(make(c)))
This time rowsHaveCorrectAmountOfExternalSpaces internally uses a different calculation:
const index = alphabet.indexOf(char)return ((width - 1) / 2 - index) * 2
which means Iâve removed the duplication. Plus, the tests are already green since the production code for the internal spaces takes care of the external too.
And.. We are done
As shown above, the last REPL test gave us
make('C')// => ' A \n B B \nC C\n B B \n A '
which means
A B BC C B B A
And these are all the properties I have discovered:
- is not empty
- first row contains A
- last row contains A
- rows have symmetrical contour
- rows contain the correct letters
- rows are as wide as high
- rows except top and bottom have two identical letters
- rows have the correct amount of external spaces
Outro
The first thing Iâve noticed is how hard property-based TDD makes you think. In fact, itâs really easy to come up with examples for this kata. But the same cannot be said for invariants.
At the same time, knowing what properties your problem space has, means having a deep understanding of it. And with property-based TDD, itâs necessary to discover them before writing the actual production code.
Not only that, I found myself writing a property that conflicted with previous ones. In fact, the code that made it green, also turned red some of the existing. The diamond kata is a simple exercise but this happens frequently in the specs we are given on everyday work.
Also, Iâve built my way up from generic properties first and then specialised (i.e. diamond is not empty to rows have the correct amount of external spaces). Which is the opposite of what happens in example-based TDD: from specific to generic[3].
Unfortunately, I cannot compare much with example-based TDD since I havenât tried the kata that way. Should you be interested into that, please check out the references.
References
- Diamond kata with FsCheck by Mark Seemann
- Diamond KataâââTDD with only Property-Based Tests by Nat Pryce
- Diamond KataâââThoughts on Incremental Development by Nat Pryce
More Pointers
- Property Based TDD at SPA 2013 by Nat Pryce
If you liked the post and want to help spread the word, please consider tweeting, clapping or sharing this. But only if you really liked it. Otherwise, please feel free to comment or tweet me with any suggestions or feedback.
Diamond kata via property-based TDD in JavaScript was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.