首页 > Web开发, 动态语言, 挨踢(IT) > 《Agile Web Development with Rails》抄书笔记(08):单元测试

《Agile Web Development with Rails》抄书笔记(08):单元测试

2013年5月3日 发表评论 阅读评论 517 人阅读    

《Agile Web Development with Rails》抄书笔记系列

  “《Agile Web Development with Rails》抄书笔记系列”目录

  随着JUnit的普及,”测试驱动开发” 程序开发方法的推广,同时伴随着敏捷开发、极限编程的流行,可以从”技术”上保证产品质量的单元测试,深刻影响着现在的任何软件开发。受此影响,Rails中就内部集成了测试功能,方便大家进行各种类型的测试。本节,我们就详细介绍一下Rails中集成的测试功能。

目录结构介绍

  打开%Depot%/test/目录,你会发现,这个目录下有五个目录:fixtures、functional、integration、performance、unit,还有一个文件test_helper.rb。另外,D瓜哥看Rails的文档中说明中,没有提到performance,估计是在最近的新版本中新加的,文档还没来得及更新。这几个目录和文件的含义如下:

  1. fixtures,用于组织、存放测试数据的,我们下面就回讲到;
  2. functional,用于测试Controller;
  3. integration,用于测试涉及多个Controller之间的交互;
  4. performance,这个文档中没有说明,从字面意思来看,应该是用于测试性能相关的;
  5. unit,用于测试Model;
  6. test_helper.rb,用于包含测试的默认配置。

D呱呱

  关于这节内容的代码:

  1. 这节开始之前:https://github.com/diguage/depot/tree/v-07.1
  2. 这节完成之后:https://github.com/diguage/depot/tree/v-07.2

测试模板

  打开%Depot%/test/unit/,你会发现有一个文件,product_test.rb和一个目录helpers。product_test.rb,这是执行在上一节中所述的命令rails generate后,由Rails生成的,用于针对product这个Model进行测试的。这是一个良好的开端,但是Rails也仅仅能帮我们做这么多了。

  我们的打开这个程序,代码如下:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

  从代码上可以看出,这个测试类继承了ActiveSupport::TestCase类。而事实上,ActiveSupport::TestCase则是Test::Unit::TestCase的一个子类。由此看出,Rails是集成了Test::Unit框架用来生成测试用例的。下面,我们讲述一下Rails的测试用法。

  另外,这个代码中包含了一个以test开头并被注释掉的代码块。将注释去掉,然后运行命令:

rake test:units

  在输出结果中,提示有一个测试。由此看出,这个代码块,就是我们的测试模板。是不是So Easy?!

  另外,assert语句才是一个正在的测试,这里的true只是一个占位符,实际是使用的代码来替换。

开始测试

  下面,我们开始正式的测试。首先,我们创建一个没有任何设置任何属性值的Product对象,然后我们预测这个对象是不可用的,而且对象的每个属性都会报一个错误。方法errors()和invalid?()查看对象是否通过了检查;同时,any?()方法可以查看是否有错误关联到某个特定属性上。

  我们已经知道需要测试什么,但是我们还需要知道,如何通知测试框架,我们的代码代码是成功,还是失败。这时,我们就需要使用”断言(assertion)”。一个断言,就是一个简单的方法调用,就是我们通知测试框架,我们希望得到什么样的效果。最简单的测试断言方法就是assert(),参事是我们进行用于测试的判断语句。如果判断语句结果为true,则什么也不会发生;如果是false,则测试框架就会输出一些错误信息,同时停止包含错误的测试方法的执行。这里,我们期望一个空Product对象不能通过检查。则我们的测试代码如下:

test "product attributes must not be empty" do
    product = Product.new
    assert product.invalid?
    assert product.errors[:title].any?
    assert product.errors[:description].any?
    assert product.errors[:price].any?
    assert product.errors[:image_url].any?
end

  将这段代码放置到测试模板代码后面或者直接将其替换掉也可以。再次运行如下命令:

rake test:units

  结果如下:

Rack::File headers parameter replaces cache_control after Rack 1.5.
Run options:

# Running tests:

.

Finished tests in 0.267015s, 3.7451 tests/s, 18.7255 assertions/s.

1 tests, 5 assertions, 0 failures, 0 errors, 0 skips

  由此看出,我们的测试全部通过。

  下面我们更深入地进行一些独特地检查。这次,我们检查一下Product的price属性是否如我们预期那样工作。测试代码如下:

test "product price must be positive" do
     product = Product.new(title: "My Book Title",
                                description: "yyy",
                                image_url: "zzz.jpg")

     product.price = -1
     assert product.invalid?
     assert_equal "must be greater than or equal to 0.01",
          product.errors[:price].join('; ');

     product.price = 0
     assert product.invalid?
     assert_equal "must be greater than or equal to 0.01",
          product.errors[:price].join('; ');

     product.price = 1
     assert product.valid?
end

  这里,我们创建了一个Product的对象,然后将price属性依次设置成-1、0、1,然后对price进行校验。前两个不符合要求,则assert product.invalid?正常,不会报错。然后使用assert_equal检查错误信息是否如我们预期。

D呱呱

  D瓜哥猜测,Product的校验规则greater_than_or_equal_to: 0.01,返回的错误信息是”must be greater than or equal to 0.01″。D瓜哥试着找了一些Rails的源代码,但是由于D瓜哥也刚刚开始学习Ruby,当然也包括Rails,对Rails的源代码结构还不是很了解。所以,没有成功。以后有成果了,D瓜哥再补充。

  非空校验测试过了;数字规则也测试过了;下面,我们对图片的URL进行测试。为了方便进行URL的测试,我们来创建一个工厂方法。代码如下:

def new_product(image_url)
     Product.new(title: "My Book Titile",
                    description: "yyy",
                    price: 1,
                    image_url: image_url)
end

  根据不同的图片URL创建Product对象。下面开始正式的测试代码:

test "image url" do
     ok = %w{fred.gif fred.jpg fred.png FRED.JPG FRED.Jpg

http://a.b.c/x/y/z/fred.gif}

     bad = %w{fred.doc fred.gif/more fred.gif.more }
     ok.each do |name|
          assert new_product(name).valid?, "#{name} shouldn't be invalid"
     end

     bad.each do |name|
          assert new_product(name).invalid?, "#{name} shouldn't be valid"
     end
end

  这里使用迭代器来完成校验。对迭代器不熟悉的童鞋,请看“牛逼闪闪的Ruby迭代器”

  另外,大家也许注意到了assert后面多了一个参数。实际上,所有的测试断言,都可以额外接受一个字符串参数,用于错误信息提示。

测试固件(Test fixtures)

  另外,我们还需要校验一下Product的title属性在数据库中是唯一的。下面我们来测试一下这种情况。一般情况下,我们要先创建一个对象,保存到数据库中;然后再创建一个相同title的对象,保存到数据库中进行测试。但是,Rails提供了一种更加简单的方法:测试固件。

  测试固件是为了方便为测试提供数据的文件,只需要将需要测试的数据放置到这个文件中,剩下的测试工作都可以Rails都可以提供事半功倍的便利。

  测试固件的相关文件在%Depot%/test/fixtures/目录下,可以使用CSV格式或者YAML格式的文件来提供数据。这个目录下,每个文件包含一个Model的数据。文件名也有规定:文件名必须必须和数据库中的表名想匹配。因为我们需要一些Product的数据。所以,我们将数据保存到名为products.yml的文件中。

  打开products.yml文件,你会发现这里面的的格式和%Depot%/config/database.yml中的格式类似,保存的都是键/值对。另外,每行开头只能使用空格,而不能使用制表符。修改内容时,一定要保持每个实体的属性名和表中的列名相匹配,否则在使用时会导致hard-to-track-down异常。

  根据原来的两个实体格式,我们新添加一个自定义的实体,内容如下:

ruby:
  title: Programming Ruby 1.9
  description:
    Ruby is thefastest growing andmost exciting dynamic
    language outthere. If youneed to getworking programs
    delivered fast, youshould addRuby to your toolbox.
  image_url: ruby.png
  price: 49.50

  将数据添加进来了,我们该如何使用呢?事实上,Rails已经提供了非常方便的加载fixtures数据的方法,只需要在%Depot%/test/unit/product_test.rb中添加一行代码即可:

class ProductTest < ActiveSupport::TestCase
  fixtures :products
  # 其他内容省略
end

  fixtures方法会根据给定的参数,加载相应的fixtures数据生成相应的Model,并且在所有测试方法运行之前插入到相应的数据库表中。这里,我们使用:products,products.yml的内容会被加载。

  再补充一句,再ProductTest种添加fixtures语句,意味着在每个test测试代码块运行之前,products表都将会被清空,然后使用将products.yml种包含的三条数据(会自动生成两条记录)插入到数据库中。

  到目前为止,我们的工作一直在开发库(development database)上。现在,我们运行测试方法,Rails需要使用一个测试库(test database)。如果你看一下%Depot%/config/目录下的database.yml文件的内容,你会发现,Rails实际上已经配置了三个不同用途的库,三个库的目录和用途如下:

  1. %Depot%/db/development.sqlite3,是我们平时开发使用的数据库。我们所有的代码都是基于此库运行。
  2. %Depot%/db/test.sqlite3,专门用于测试的数据库。
  3. %Depot%/db/production.sqlite3,是一个用于生产的数据库。当我们把代码发布到网络上、运行在服务器上时,使用这个数据库。

  每一个测试方法都会在测试库中,重新初始化一个表,并使用fixtures中的数据初始化表中的数据。这些工作都通过rake test命令,自动完成;当然,也可以使用rake db:test:prepare分别执行。

  下面,我们使用fixtures中的数据进行测试。我们知道,在每个测试方法运行之前,都会加载fixtures中定义的数据。其实,Rails在加载fixtures数据的同时,还根据参数名,在测试类中定义了一个同名的属性,用于存储fixtures中的数据。所以,使用products(:ruby)就可以获得我们上面在%Depot%/test/fixtures/products.yml中添加的数据。

  下面我们开始测试Product的title的唯一性约束:

test "product is not valid without a unique title" do
     product = Product.new(title: products(:ruby).title,
                                description: "yyy",
                                price: 1,
                                image_url: "fred.gif")
     assert !product.save
     assert_equal "has already been taken", product.errors[:title].join('; ')
end

  这个测试假定数据库中已经存在了一条title值为Programming Ruby 1.9的记录。创建一个Product,然后将对象的title的值设置为products(:ruby).title的值(也就是Programming Ruby 1.9)。然后保存,我们预测保存会失败,并且有一条和title属性相关的错误。然后,运行rux如下指令,查看测试结果:

  rake test:units

  也许你会认为把错误信息之间硬编码进代码不好,不利于国际化。Rails提供了对国际化需求的支持,可以很方便的把上面的测试代码修改成国际化版,代码如下:

test "product is not valid without a unique title - i18n" do
     product = Product.new(title: products(:ruby).title,
                                description: "yyy",
                                price: 1,
                                image_url: "fred.gif")
     assert !product.save
     assert_equal I18n.translate('activerecord.errors.messages.taken'),
          product.errors[:title].join('; ')
end

  至此,我们的全部测试工作已经完成。现在,针对Product,我们提供了一个model, 一组views, 一个controller,一组unit tests。Product相关的开发已经基本完成。既然已经可以将Product,那么如何展示这些Product?下一节,我们将着手解决这个问题。

  另外,D瓜哥再多说一句,通过对后续内容的阅读发现,Rails还可以测试HTML的输出。这个我们以后用到了在讲解!

D呱呱

  关于测试方面更多论述,请看Rails的官方文档:A Guide to Testing Rails Applications

  不过,现在RSpec是一个Ruby中一个更加流行的测试包。上一节,“《Agile Web Development with Rails》抄书笔记(07):数据校验”中提到的“Ruby on Rails Tutorial中文版”教程,就是用RSpec来做测试的。感兴趣的,可以看看。这里还有两个资料:

  1. 一个讲解RSpec的视频
  2. 使用 RSpec 进行行为驱动测试


作 者: D瓜哥,https://www.diguage.com/
原文链接:https://www.diguage.com/archives/14.html
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。