如何使用 ZenTest 与 Ruby
重构和单元测试是一对强大的工具,应该成为每位程序员工作台的一部分。遗憾的是,并非每位程序员都知道如何使用这些工具。我第一次接触它们是在我开始使用 Ruby 时。重构和单元测试是 Ruby 社区的重要组成部分。
不久前,我将 Martin Fowler 的优秀著作《重构》第一章中的重构示例从 Java 翻译成 Ruby。 我觉得这将是学习更多关于重构知识并在使用 Ruby 时复习的好方法。 最近,我决定更新 Ruby 1.8.X 的翻译。 我需要做的事情之一是将旧的单元测试转换为使用 Test::Unit,这是 Ruby 的新单元测试框架。 然而,我不期待构建一个新的测试套件。 幸运的是,有可用的帮助。
Ryan Davis 编写了一个很棒的工具,名为 ZenTest,它可以为现有的代码体创建测试套件。 因为很多人对重构、单元测试和 ZenTest 感到陌生,所以本文旨在介绍这三种工具。
Martin 的示例代码是围绕视频商店应用程序构建的。 他的原始代码包括三个类——Customer、Movie 和 Rental。 在本文中,我只关注 Customer 类。 这是原始代码
class Customer
attr_accessor :name
def initialize(name)
@name = name
@rentals = Array.new
end
def addRental(aRental)
@rentals.push(aRental)
end
def statement
totalAmount = 0.0
frequentRenterPoints = 0
rentals = @rentals.length
result = "\nRental Record for #{@name}\n"
thisAmount = 0.0
@rentals.each do |rental|
# determine amounts for each line
case rental.aMovie.pricecode
when Movie::REGULAR
thisAmount += 2
if rental.daysRented > 2
thisAmount += (rental.daysRented - 2) * 1.5
end
when Movie::NEW_RELEASE
thisAmount += rental.daysRented * 3
when Movie::CHILDRENS
thisAmount += 1.5
if each.daysRented > 3
thisAmount += (rental.daysRented - 3) * 1.5
end
end
# add frequent renter points
frequentRenterPoints += 1
# add bonus for a two day new release rental
if ( rental.daysRented > 1) &&
(Movie::NEW_RELEASE == rental.aMovie.pricecode)
frequentRenterPoints += 1
end
# show figures for this rental
result +="\t#{rental.aMovie.title}\t#{thisAmount}\n"
totalAmount += thisAmount
end
result += "Amount owed is #{totalAmount}\n"
result += "You earned #{frequentRenterPoints} frequent renter points"
end
end
这不是世界上最干净的代码,但这正是重点——这代表了您从用户那里获得的代码。 它不包含任何测试,并且布局很差,但它可以工作。 你的工作是让它在不破坏它的情况下更好地工作。 那么,从哪里开始呢? 当然是单元测试。 是时候使用 ZenTest 了。
您可以告诉 ZenTestit 执行此操作
$ zentest videostore.rb > test_videostore.rb
这会生成一个包含大量测试的文件。 然而,运行测试套件并没有完全达到我们希望的效果
$ ruby testVideoStore.rb Loaded suite testVideoStore
Started
EEEEEEEEEEE
Finished in 0.008974 seconds.
1) Error!!!
test_addRental(TestCustomer):
NotImplementedError: Need to write test_addRental
testVideoStore.rb:11:in `test_addRental'
testVideoStore.rb:54
2) Error!!!
test_name=(TestCustomer):
NotImplementedError: Need to write test_name=
testVideoStore.rb:15:in `test_name='
testVideoStore.rb:54
3) Error!!!
test_statement=(TestCustomer):
NotImplementedError: Need to write test_statement
testVideoStore.rb:19:in `test_statement'
testVideoStore.rb:54
.
.
.
11 tests, 0 assertions, 0 failures, 11 errors
$
像这样运行 ZenTest,我们到底得到了什么? 这是我们新的测试套件中对 Customer 类重要的部分
# Code Generated by ZenTest v. 2.1.2
# classname: asrt / meth = ratio%
# Customer: 0 / 3 = 0.00%
require 'test/unit'
class TestCustomer < Test::Unit::TestCase
def test_addRental
raise NotImplementedError, 'Need to write test_addRental'
end
def test_name=
raise NotImplementedError, 'Need to write test_name='
end
def test_statement
raise NotImplementedError, 'Need to write test_statement'
end
end
ZenTest 构建了三个测试方法:一个用于访问器方法,一个用于 addRental 方法,另一个用于 statement 方法。 为什么初始化程序没有任何内容? 这是跳过的,因为初始化程序往往非常可靠。 如果不是,您可以自己轻松添加测试方法。 此外,当我们编写时,我们将间接测试它test_name=,访问器方法的测试。 我们需要添加另一件事——测试套件不会加载我们正在测试的代码。 将脚本的开头更改为 require videostore.rb 文件应该可以为我们解决问题。
# Code Generated by ZenTest v. 2.1.2
# classname: asrt / meth = ratio%
# Customer: 0 / 3 = 0.00%
require 'test/unit'
require 'videostore'
顶部的注释片段让我们知道我们在 Customer 类中有三个方法正在测试,零个断言测试它们,并且没有覆盖率。 让我们修复它。 我们首先为 test_name= 编写一些测试。 我们在这里以什么顺序进行并不重要,test_name= 只是一个方便的起点。
def test_name=
aCustomer = Customer.new("Fred Jones")
assert_equal("Fred Jones",aCustomer.name)
aCustomer.name = "Freddy Jones"
assert_equal("Freddy Jones",aCustomer.name
end
再次运行 testVideoStore.rb 给我们
$ ruby testVideoStore.rb
Loaded suite testVideoStore
Started
E.EEEEEEEEE
Finished in 0.011233 seconds.
1) Error!!!
test_addRental(TestCustomer):
NotImplementedError: Need to write test_addRental
testVideoStore.rb:13:in `test_addRental'
testVideoStore.rb:58
2) Error!!!
test_statement(TestCustomer):
NotImplementedError: Need to write test_statement
testVideoStore.rb:23:in `test_statement'
testVideoStore.rb:58
.
.
.
11 tests, 2 assertions, 0 failures, 10 errors
$
到目前为止,一切都很好。 显示测试运行中错误的 E 行已减少了一行,并且底部的摘要行告诉我们大致相同的内容。
我们没有直接测试 addRental 的方法,所以我们现在只需编写一个存根测试。
def test_addRental
assert(1) # stub test, since there is nothing in the method to test
end
当我们再次运行测试时,我们得到
$ ruby testVideoStore.rb
Loaded suite testVideoStore
Started
..EEEEEEEEE
Finished in 0.008682 seconds.
1) Error!!!
test_statement(TestCustomer):
NotImplementedError: Need to write test_statement
testVideoStore.rb:22:in `test_statement'
testVideoStore.rb:57
.
.
.
11 tests, 3 assertions, 0 failures, 9 errors
$
我们做得越来越好了; TestCustomer 类中只剩下一个错误。 让我们完成一个测试,清除我们的 test_statement 错误并验证 addRental 是否正常工作
def test_statement
aMovie = Movie.new("Legacy",0)
aRental = Rental.new(aMovie,2)
aCustomer = Customer.new("Fred Jones")
aCustomer.addRental(aRental)
aStatement = "\nRental Record for Fred Jones\n\tLegacy\t2.0
Amount owed is 2.0\nYou earned 1 frequent renter points"
assert_equal(aStatement,aCustomer.statement)
end
我们再次运行测试并看到
$ ruby testVideoStore.rb
Loaded suite testVideoStore
Started
...EEEEEEEE
Finished in 0.009378 seconds.
.
.
.
11 tests, 4 assertions, 0 failures, 8 errors
$
太棒了! 剩余的错误发生在 Movie 和 Rental 类中; Customer 类是干净的。
我们可以像这样继续处理剩余的类,但我不会用这些细节来烦扰您。 相反,我想看看当您已经有一些测试到位时,ZenTest 如何提供帮助。 后来的开发允许我们完全做到这一点。 例如,假设视频商店所有者想要一个新的基于 Web 的报表,客户可以在线访问。 经过一些重构和新的开发,代码看起来像这样
class Customer
attr_accessor :name
def initialize(name)
@name = name
@rentals = Array.new
end
def addRental(aRental)
@rentals.push(aRental)
end
def statement
result = "\nRental Record for #{@name}\n"
@rentals.each do
|each|
# show figures for this rental
result +="\t#{each.aMovie.title}\t#{each.getCharge}\n"
end
result += "Amount owed is #{getTotalCharge}\n"
result +=
"You earned #{getFrequentRenterPoints} frequent renter points"
end
def htmlStatement
result = "\n<H1>Rentals for <EM>#{name}</EM></H1><P>\n"
@rentals.each do
|each|
result += "#{each.aMovie.title}: #{each.getCharge}<BR>\n"
end
result += "You owe <EM>#{getTotalCharge}</EM><P>\n"
result +=
"On this rental you earned <EM>#{getFrequentRenterPoints}" +
"</EM> frequent renter points<P>"
end
def getTotalCharge
result = 0.0
@rentals.each do
|each|
result += each.getCharge()
end
result
end
def getFrequentRenterPoints
result = 0
@rentals.each do
|each|
result += each.getFrequentRenterPoints
end
result
end
end
现在代码中有很多新内容。 如果我们再次运行 ZenTest,它会拾取我们没有覆盖率的方法。 我们应该在编写新方法时编写它们,但此方法更具说明性。 所以这次,我们以稍微不同的方式调用 ZenTest
$ zentest videostore.rb testVideoStore.rb > Missing_tests
我们的(修剪后的)输出看起来像这样
# Code Generated by ZenTest v. 2.1.2
# classname: asrt / meth = ratio%
# Customer: 4 / 6 = 66.67%
require 'test/unit'
class TestCustomer < Test::Unit::TestCase
def test_getFrequentRenterPoints
raise NotImplementedError,
'Need to write test_getFrequentRenterPoints'
end
def test_getTotalCharge
raise NotImplementedError, 'Need to write test_getTotalCharge'
end
def test_htmlStatement
raise NotImplementedError, 'Need to write test_htmlStatement'
end
end
我们需要填写另外三个测试方法才能获得完整的覆盖率。 在我们编写这些内容时,我们可以将它们迁移到我们现有的 testVideoStore.rb 测试套件。 然后,我们可以继续进行重构和添加新功能。 当然,将来,我们将随着我们的进行而简单地添加测试。 ZenTest 在这里也可以提供帮助。 我们可以为新开发编写存根,然后运行 ZenTest 以创建新的测试存根。 经过一些重构,例如提取方法,ZenTest 可以以相同的方式使用。
重构和单元测试是程序员的强大工具,ZenTest 提供了一种在 Ruby 环境中开始使用它们的简便方法。 希望这篇介绍已经引起了您的兴趣。
如果您有兴趣了解更多关于重构的信息,请获取一本《重构:改进现有代码的设计》并访问 www.refactoring.com。 有关单元测试的更多信息,请参阅 c2.com/cgi/wiki?UnitTest www.junit.org/index.htm 和 www.extremeprogramming.org/rules/unittests.html。
关于 Test::Unit 和 ZenTest 的最新信息可以在它们的主页上找到:testunit.talbott.ws 和 www.zenspider.com/ZSS/Products/ZenTest。
