![](https://i-blog.csdnimg.cn/blog_migrate/50b21ad92dacc21a9b2cb7644094e4fa.png)
在上一篇文章中,我介绍了基于属性的测试背后的基本思想。 在这里,我将使用该技术来TDD钻石kata。
这篇文章受到很大的启发(即公然复制)。 因此,请务必对Nat Pryce和Mark Seemann做同样的练习[1] [2](参考文献底部的链接)打个招呼。 幸运的是,我将使用JavaScript和JSVerify 。 这样,我可以将自己隐藏在“但我使用其他堆栈”的背后。
另外,我将代码片段降至最低。 如果您有兴趣了解更多详细信息,请随时检查回购 。
钻石方
给定字母,打印以'A'开头的菱形,并在最宽处打印所提供的字母。
一些例子是
Input: A
Output: A
Input: B
Output: A
BB
A
Input: C
Output: A
BB
CC
BB
A
准备,设定,摇滚
在init
提交中,我要检查接线。 这就是为什么我使用始终返回5
的生成器来检查isFive
属性的原因。
// 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 === 5
module.exports = isFive
当然是绿色的
$ mocha index.test.js
TODO
✓ TODO
1 passing (12ms)
✨ Done in 0.52s.
发电机
一切正常,因此我可以为菱形kata创建发生器。 特别是,我需要生成A..Z
范围内的字符。
由于我不确定使用什么,因此决定检查jsc.asciichar
生成器返回的内容
const debug = x => {
console.log(x)
return true
}
describe('diamond', () => {
jsc.property('TODO', jsc.asciichar, debug)
})
注意return true
。 这样,“属性” debug
永远不会失败,我可以检查所有生成的asciichar。 由于默认情况下JSVerify通过从生成器中生成100个输入来检查属性100次,因此我看到
$ mocha index.test.js
diamond
T
K
.
E
B
<
// ... up to 100 asciichars
✓ TODO
1 passing (16ms)
✨ Done in 0.52s.
不太正确,实际上,我只需要生成A..Z
范围内的字符。 不幸的是,JSVerify并没有提供任何符合该约束的生成器。 因此,我创建了一个自定义的
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
const char = jsc.suchthat(jsc.asciichar, c => alphabet.includes(c))
describe('diamond', () => {
jsc.property('TODO', char, debug)
})
这次我们得到适当的值
$ mocha index.test.js
diamond
B
L
X
B
Q
V
X
B
J
S
C
P
I
// ... up to 100 chars in A..Z
✓ TODO
1 passing (19ms)
✨ Done in 0.52s.
注意,我可以将支票移到属性内
const property = c => {
if (!alphabet.includes(c)) return true
// ... test the property
}
describe('diamond', () => {
jsc.property('TODO', jsc.asciichar, property)
})
但我会犯一个错误。 实际上,在这种情况下,JSVerify将使用随机的jsc.asciichar
调用property
100次。 因此,仅生成的输入的一部分会超过if
。 换句话说,我会失去测试范围。
属性:钻石不为空
开始练习的属性只是检查钻石的长度是否大于0(对于任何字符)。
jsc.property('is not empty', char, c => make(c).length !== 0)
我用它使绿色
const make = char => 'whatever'
来自REPL
make(c) // for any c
// => 'whatever'
属性:第一行包含A
jsc.property(
'first row contains A',
char,
c => firstRow(make(c)).trim() === 'A'
)
我用它使绿色
const make = char => ' A ' // padding is asymmetric
来自REPL
make(c) // for any c
// => ' A '
属性:最后一行包含A
jsc.property(
'last row contains A',
char,
c => lastRow(make(c)).trim() === 'A'
)
这已经是绿色的。
属性:第一排具有对称轮廓
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))
)
我用它使绿色
const make = char => ' A ' // padding is symmetric
来自REPL
make(c) // for any c
// => ' A '
属性:行具有对称轮廓
好吧,不仅第一行具有对称轮廓。 让我们修改属性,以便检查所有行
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))
)
这已经是绿色的。
属性:行包含正确的字母
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))
)
我用它使绿色
const make = char => {
const pre = alphabetUntilBefore(char)
const post = pre.slice().reverse()
const chars = pre.concat([char]).concat(post)
return chars.join('\n')
}
测试代码和生产代码之间的重复是难闻的气味。 但是我决定把它留在那里。
来自REPL
make('C')
// => 'A\nB\nC\nB\nA'
属性:行的宽度和高度一样高
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))
)
我用它使绿色
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')
}
和REPL
make('C')
// => ' A \nBBBBB\nCCCCC\nBBBBB\n A '
属性:除顶部和底部之外的行具有两个相同的字母
jsc.property(
'rows except top and bottom have two identical letters',
char,
c => internalRowsHaveTwoIdenticalLetters(make(c))
)
我用它使绿色
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')
}
和REPL
make('C')
// => ' A \nB B\nC C\nB B\n A '
属性:行具有正确的内部空间量
jsc.property(
'rows have the correct amount of internal spaces',
char,
c => rowsHaveCorrectAmountOfInternalSpaces(make(c))
)
我用它使绿色
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')
}
和REPL
make('C')
' A \n B B \nC C\n B B \n A '
不幸的是, rowsHaveCorrectAmountOfInternalSpaces
中的rowsHaveCorrectAmountOfInternalSpaces
使用以下内容
const index = alphabet.indexOf(char)
return Math.max((index * 2) - 1, 0)
我不喜欢这种重复。 因此,我决定测试外部空间(而不是内部空间)。
属性:行具有正确的外部空间量
jsc.property(
'rows have the correct amount of external spaces',
char,
c => rowsHaveCorrectAmountOfExternalSpaces(make(c))
)
这次, rowsHaveCorrectAmountOfExternalSpaces
内部使用了不同的计算方法:
const index = alphabet.indexOf(char)
return ((width - 1) / 2 - index) * 2
这意味着我已经删除了重复项。 另外,由于内部空间的生产代码也照顾外部环境,因此测试已经是绿色的。
并且..我们完成了
如上所示,最后一次REPL测试给了我们
make('C')
// => ' A \n B B \nC C\n B B \n A '
意思是
A
B B
C C
B B
A
这些是我发现的所有属性:
-
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
奥托罗
我注意到的第一件事是基于属性的TDD如何使您思考。 实际上,为这个kata举个例子真的很容易。 但是对于不变式不能说相同。
同时,了解您的问题空间的性质意味着对它有深入的了解。 对于基于属性的TDD,有必要在编写实际的生产代码之前发现它们。
不仅如此,我发现自己编写的属性与以前的属性冲突。 实际上,使它变成绿色的代码也使现有的一些变成红色。 菱形卡塔是一个简单的练习,但是在我们日常工作中经常会出现这种情况。
另外,我先从通用属性开始构建,然后再从专用属性构建(即diamond is not empty
,而rows have the correct amount of external spaces
)。 这与基于示例的TDD中发生的情况相反:从特定到泛型[3]。
不幸的是,因为我还没有尝试过这种方法,所以我无法与基于示例的TDD进行比较。 如果您对此感兴趣,请查阅参考资料。
参考文献
- 马克· 西曼 (Mark Seemann)的FsCheck钻石方盒
- 钻石卡塔(Diamond Kata)— TDD仅包含基于性能的测试 , 作者是Nat Pryce
- 钻石卡塔-纳特·普莱斯(Nat Pryce) 关于增量发展的思考
更多指针
- Nat Pryce 在SPA 2013上基于属性的TDD
如果你喜欢的职位,并希望帮助流传着一句话,请考虑啁啾 ,拍手或分享这些。 但前提是您真的很喜欢它。 否则,请随时发表评论或向我发送任何建议或反馈。
From: https://hackernoon.com/diamond-kata-via-property-based-tdd-in-javascript-5fa99acd3e62