前言
最近在读《匠艺整洁之道 - 程序员的职业修养》这本书,作者鲍勃大叔开篇就用了大量的示例来讨论与演示为什么需要和如何操作测试驱动开发。我是之前写过测试,但从不知道测试如何驱动开发,大部分情况下也只是先写生产代码,写好后再测试,看看是否能调通,再修改代码中隐藏的问题。
而测试驱动开发会扭转这种思维方式,是先写测试,再写对应的生产代码。这一开始让人会觉得很奇怪,感觉并且很麻烦,但当你尝试一下,就会发现这事儿挺有趣,且神奇。
温馨提示:Clean Craftsmanship: Disciplines, Standards, and Ethics (Companion Videos),这个是该书示例操作的视频说明,非常非常非常有用!!! 第一篇是讲述如何通过测试驱动开发来写一个栈结构。我严重怀疑大叔是不是一边写代码一边喝啤酒。整个过程伴随大叔魔性的笑声和宛如魔法般的操作,就好像他在不停调戏编译器。你会通过这个视频感受到如何从零开始,进行测试驱动开发。 下面将讲讲一些基础知识。
上面的纪律需要下面展示的法则作为基础,如果没有一些技巧和知识就很难遵守这些法则。
第一法则:在编写因为缺少生产代码而必然会失败的测试之前,绝不编写生产代码 第二法则:只写刚好导致失败或者通不过编译的测试,编写生产代码来解决失败的问题 第三法则: 只写刚好能解决当前测试失败问题的生产代码。测试通过后,立即写其他测试代码 整个过程看起来好像是:
如此循环,贯穿始终
遵守上述法则后,有以下好处
这里建议先看我上面提供的链接,作者是用 Java 写的,我这里用 Ruby(核心思想是一致的)
require 'minitest/autorun'
class StackTest < Minitest::Test
def test_nothing
end
end
这里我们一开始写一个什么都不做的测试,然后保证这个测试通过,这至少能说明你的环境没问题
规则 1:先编写测试,逼着自己写将要写的代码
我们知道我们需要一个栈
# stack_test.rb
require 'minitest/autorun'
class StackTest < Minitest::Test
# ...
def test_canCreateStack
stack = Stack.new
end
end
这里的测试一定会失败,因为现在根本没有Stack
这个类结构,所以我们写两行生产代码来通过测试
# stack.rb
class Stack
end
这次我们在stack_test.rb
引入一下,就可以通过测试了。
规则 2:让测试失败,让测试通过,清理代码
这时候我们发现我们还没有断言行为呢,比如当刚创建一个栈时,这个栈应该是空的。
# stack_test.rb
require 'minitest/autorun'
require_relative 'stack'
class StackTest < Minitest::Test
def test_nothing
end
def test_canCreateStack
stack = Stack.new
assert(stack.isEmpty?)
end
end
# => 报错:NoMethodError: undefined method `isEmpty?'
我们迅速补上生产代码,来解决这个问题
# stack.rb
class Stack
def isEmpty?
false
end
end
这样改后,方法可以找到了,但断言失败了,这里我们是故意的,为什么这么做呢?第一法则:测试必须失败,为啥呢?因为当测试应该失败时,我们就能看到它失败,我们测试了自己的测试。当我们将上面的方法返回 true 时,就能测试另一半了。
# stack.rb
class Stack
def isEmpty?
true
end
end
这时候执行测试,测试通过,万事大吉,你一定会骂这不是作弊吗,但等等,至少我们只用几秒就能测出该通过时通过,该失败时失败。
下一个要测的是,栈需要能 push 吧
# stack_test.rb
require 'minitest/autorun'
require_relative 'stack'
class StackTest < Minitest::Test
# ...
def test_canPush
stack = Stack.new
stack.push(0)
end
end
这里会报错,因为找不到方法,我们就跟着改改生产代码
# stack.rb
class Stack
def isEmpty?
true
end
def push(ele)
end
end
执行测试,测试通过,但这里我们没有断言啊,那既然 push 了,栈就不应为空吧
require 'minitest/autorun'
require_relative 'stack'
class StackTest < Minitest::Test
# ...
def test_canPush
stack = Stack.new
stack.push(0)
refute(stack.isEmpty?)
end
end
执行测试,断言失败,因为我们返回的一直是 true,那改改代码吧
class Stack
attr_accessor :empty
def initialize
@empty = true
end
def isEmpty?
empty
end
def push(ele)
@empty = false
end
end
我们抽离出一个实例变量来保存是否为空,并在 push 后直接暴力设置为 false。这时候测试又能通过了
我们发现没写一个测试方法,都要创建一个栈,太麻烦了,于是我们重构一下,使用setup方法
require 'minitest/autorun'
require_relative 'stack'
class StackTest < Minitest::Test
def setup
@stack = Stack.new
end
def test_nothing
end
def test_canCreateStack
assert(@stack.isEmpty?)
end
def test_canPush
@stack.push(0)
refute(@stack.isEmpty?)
end
end
测试依然能通过,不过 canPush 这个测试名不太好,我们改改(test_操作_结果)
def test_afterOnePush_isEmpty
@stack.push(0)
refute(@stack.isEmpty?)
end
当然测试还是能通过
这时候我们测试,栈 push 一次,也能 pop 一次吧,并且这时候栈应该为空
def test_afterOnePushAndOnePop_isEmpty
@stack.push(0)
@stack.pop
assert(@stack.isEmpty?)
end
测试失败,因为没有这个方法
class Stack
# ...
def pop
@empty = true
end
end
现在测试又通过了,现在我们测试两次 push 之后,栈的尺寸应该是 2
def test_afterTwoPushs_sizeIsTwo
@stack.push(0)
@stack.push(0)
assert_equal(2,@stack.size)
end
测试失败,因为没有这个方法,我们修改生产代码
class Stack
attr_accessor :empty,:size
def initialize
@empty = true
@size = 0
end
def isEmpty?
empty
end
def push(ele)
@size += 1
@empty = false
end
def pop
@empty = true
end
end
我们定义 size 实例变量来保存状态,并在每次 push 时,size+1,这样断言通过。为了测试完整,我们再加一个测试
def test_afterOnePush_isNotEmpty
@stack.push(0)
refute(@stack.isEmpty?)
assert_equal(1,@stack.size)
end
测试通过。回到第一法则,如果对空栈执行 pop 操作,应该会有个异常吧
def test_poppingEmptyStack_raisesUnderflow
assert_raises do
@stack.pop
end
end
测试失败,我们还没定义这个异常类
class Underflow < StandardError
end
断言失败,我们再修改生产代码
require_relative 'underflow'
class Stack
# ...
def pop
raise Underflow if isEmpty?
@empty = true
end
end
测试通过,再测试:当栈 push 一个数据时,也应该 pop 出相同的数据吧
def test_afterPushingX_willPopX
@stack.push(1)
assert_equal(1,@stack.pop)
end
测试失败,调整生产代码,我们加一个实例变量保存信息
require_relative 'underflow'
class Stack
attr_accessor :empty,:size,:element
def initialize
@empty = true
@size = 0
end
def isEmpty?
empty
end
def push(ele)
@size += 1
@empty = false
@element = ele
end
def pop
raise Underflow if isEmpty?
@empty = true
@element
end
end
测试通过,已经写了这么多代码,你看的都崩溃了,觉得直接写一个栈不就完了嘛
规则 3:别挖金子
在最开始尝试 TDD 时,你会急于解决较难或有趣的问题,你可能先写 FILO 行为,这个就是挖金子,我们有意避免测试与栈行为有关的东西,专注与周边行为。例如栈是否为空或栈大小。 为什么避免挖金子,因为如果过早挖金子,可能忽略周边所有细节。现在我们根据第一法子,编写 FILO 测试
def test_afterPushingXAndY_willPopYThenX
@stack.push(1)
@stack.push(2)
assert_equal(2,@stack.pop)
assert_equal(1,@stack.pop)
end
测试失败,我们发现抛出了Underflow
,我们修改一下isEmpty?
方法
require_relative 'underflow'
class Stack
# ...
def isEmpty?
size == 0
end
end
测试失败,test_afterOnePushAndOnePop_isEmpty
这个测试中报错,好吧,我们在 pop 时没有 size-1
require_relative 'underflow'
class Stack
# ...
def pop
raise Underflow if isEmpty?
@size -= 1
@empty = true
@element
end
end
就剩下最后一个断言失败了,FILO 行为,我们开始修改
require_relative 'underflow'
class Stack
attr_accessor :empty,:size,:elements
def initialize
@empty = true
@size = 0
@elements = []
end
def isEmpty?
size == 0
end
def push(ele)
@size += 1
@empty = false
@elements << ele
end
def pop
raise Underflow if isEmpty?
ele = @elements[size-1]
@size -= 1
@empty = true
ele
end
end
测试全部通过,我们已经完成了一个栈的基础行为啦,你会发现一个栈在我们不断测试中渐渐成型,虽然现在并不完善,但我们已经能感受到测试驱动开发的整个过程了。
也许你可以花点儿时间,利用测试驱动开发自己写一个队列结构。