Ruby On Rails的第一个应用(三)--单元测试

 

二、迭代B2:模型的单元测试

 

rails在每个项目创建时,就生成了一个基本的测试环境,在/test/unit下。

 

E:\works\ruby\depot\test\unit>dir
 驱动器 E 中的卷是 work
 卷的序列号是 F4C3-30B8

 E:\works\ruby\depot\test\unit 的目录

2013-03-14  14:45    <DIR>          .
2013-03-14  14:45    <DIR>          ..
2013-03-14  14:33                 0 .gitkeep
2013-03-14  14:45    <DIR>          helpers
2013-03-14  14:45               121 product_test.rb
               2 个文件            121 字节
               3 个目录 21,651,755,008 可用字节

 

 

这里的product_test.rb文件就是用来保存我们生成的模型单元测试的。

 

require 'test_helper'

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

 

 

ProductTest是ActiveSupport::TestCase的子类,而ActiveSupport::TestCase又是Test::Unit::TestCase的子类,这里说明rails生成的测试是基于Test::Unit框架的,这个框架来自Ruby。

 

1.真正的单元测试

首先,如果创建一个没有属性集的商品我们期待它是无效的,而且会显示与每个字段关联的错误信息。那么可以使用该模型的errors和invalid?方法,另外可以使用错误清单的any?方法来查看是否有和特定的属性相关联的错误。

可以通过断言(accertions)来告诉测试框架该代码是否通过了测试。断言是一个简单的方法调用,它告诉框架什么才是所期望的真。最简单的断言方法是assert这个方法预期其参数为真。如果参数为真,那么就不会发生什么;如果参数为假,则断言失败,该框架将输出一条消息,并停止执行包含错误的测试方法。

我们预计一个空的Product模型无法通过验证,用assert product.invalid?。修改测试代码为/test/unit/product_test.rb(下载的源代码中此文件在test/unit/models/下):

	require 'test_helper'

	class ProductTest < ActiveSupport::TestCase
	  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
	end

 

可以通过rake test:units来运行单元测试:

  rake test:units

	E:\works\ruby\depot>rake test:units
	        SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
	        This poses a security threat. It is strongly recommended that you
	        provide a secret to prevent exploits that may be possible from crafted
	        cookies. This will not be supported in future versions of Rack, and
	        future versions will even invalidate your existing user cookies.
	
	        Called from: D:/dev/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/actionpack-3.2.1/lib/action_dispatch/middleware/session/abstract_store.rb:28:in `initialize'.
	
	Rack::File headers parameter replaces cache_control after Rack 1.5.
	Run options:
	
	# Running tests:
	
	.
	
	Finished tests in 0.390625s, 2.5600 tests/s, 12.8000 assertions/s.
	
	1 tests, 5 assertions, 0 failures, 0 errors, 0 skips

 

再深入点来验证价格,在/test/unit/product_test.rb添加一个测试方法:

	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]

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

    product.price = 1
    assert product.valid?
  end

 

  把这个测试方法的三种测试分为三个独立的方法,也是合理的。

  rake test:units

	E:\works\ruby\depot>rake test:units
	        SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
	        This poses a security threat. It is strongly recommended that you
	        provide a secret to prevent exploits that may be possible from crafted
	        cookies. This will not be supported in future versions of Rack, and
	        future versions will even invalidate your existing user cookies.
	
	        Called from: D:/dev/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/ac
	tionpack-3.2.1/lib/action_dispatch/middleware/session/abstract_store.rb:28:in `i
	nitialize'.
	
	Rack::File headers parameter replaces cache_control after Rack 1.5.
	Run options:
	
	# Running tests:
	
	..
	
	Finished tests in 0.343750s, 5.8182 tests/s, 29.0909 assertions/s.
	
	2 tests, 10 assertions, 0 failures, 0 errors, 0 skips

 

再来测试验证图片url是否以.gif,.jpg,.png结尾,/test/unit/product_test.rb:

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

  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

 

  rake test:units

...

Finished tests in 0.406250s, 7.3846 tests/s, 46.7692 assertions/s.

3 tests, 19 assertions, 0 failures, 0 errors, 0 skips
 

 

  最后测试商品标题是否唯一。一种方法是,创建一个商品,然后保存它,然后再创建一个商品,使用前一个商品相同的标题,也保存到数据库中,这是可行的,但是Rails的fixtures更简单。

 

2.静态测试

静态测试(test fixture)只是为待测试的一个或多个模型而准备的初始内容的规范。如:如果要确保products表在每一个单元测试都从书籍的数据开始,可以在一次静态测试中指定下面内容,Rails会处理剩下的事情。

可以在/test/fixtures目录内的文件中详细说明静态测试数据。这些文件包含以逗号分隔值(CSV)或YAML格式的测试数据。在测试中将首选YAML格式。每个静态测试文件单个模型的测试数据。静态测试文件的名称是很重要的;文件的基本名称必须与数据库表的名称相匹配。因为需要给Product模型一些数据,而这些数据存在products表中,因为要把它添加到名为products.yml的文件中。

rails开始已经生成了这个静态测试文件,/test/fixtures/products.yml:

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  title: MyString
  description: MyText
  image_url: MyString
  price: 9.99

two:
  title: MyString
  description: MyText
  image_url: MyString
  price: 9.99

 

这个静态测试文件都包含了一个要插入数据库的条目,并且每行都给出了字段名。one/two的行对数据库是没有意义的,不会插入到数据库。

在每个条目中会看到一个名称/值组合的缩进列表。就像在config/database.yml文件中,每个数据行的开始必须使用空格,而不是Tab键,并且数据库中与同一记录相关的所有行都必须有相同的缩进。在修改程序时须格外小心,因为必须保证每个条目中的列名是正确的,与数据库中的列不匹配名称可能导致难以跟踪的异常。

添加更多有用数据来测试Product模型,/test/fixtures/products.yml添加:

ruby: 
  title:       Programming Ruby 1.9
  description: 
    Ruby is the fastest growing and most exciting dynamic
    language out there.  If you need to get working programs
    delivered fast, you should add Ruby to your toolbox.
  price:       49.50
  image_url:   ruby.png 
#END:ruby

 

在运行单元测试时,rails已经把这些测试数据加载到products表了;我们也可以通过在文件/test/unit/product_test.rb中指定以下行来控制加载哪些静态测试:

fixtures :products

测试文件名决定要加载的表,用:products表示使用products.yml静态测试文件。

在ProductTest使用fixtures表示,在运行每个测试方法之前,products表将清空,然后用静态测试中定义的数据来填充表。

要注意,大部分rails生成的脚手架并不包含fixtures方法。因为测试之前默认加载所有静态测试。

products方法通过加载静态测试为生成的表创建索引。需要修改这个索引来匹配在静态测试中所给出的名称。

目前为止我们一直是在开发数据库中工作。但是现在我们要进行测试,rails需要使用一个测试数据库。如果看看/config/database.yml,会发现在实际上rails为3个独立的数据库创建了配置:

db/development.sqlite3是开发数据库。所有编程工作将在这里完成。

db/test.sqlite3是一个测试数据库

db/production.sqlite3是实际产品数据库。应用程序正式上线时就使用这个数据库。

在测试数据库中每个测试方法都有一张刚刚初始化的表加载了所提供的静态测试数据。这是由命令rake test自动完成的,但也可单独运行rake db:test:prepare来初始化数据库表。

 

3.使用静态测试数据

如何使用这些静态测试数据呢?一种办法是使用模型中的finder方法来读取数据。但是rails中对于每一个加载到测试中的静态测试,都定义有具有和静态测试市民名称的方法。可以使用此方法来访问已经预装了的、包含了静态测试数据的模型对象:简单地yaml静态测试文件中定义的行名,它会返回包含该行数据的模型对象。这里我们可以用products(:ruby)返回我们定义的这个Product模型,这里用来验证商品名称的唯一性/test/until/product_test.rb:

	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]
  end

 

  这里用一数据库中已存在的标题来创建一个新的Product模型,并断言嘎这个模型会失败,并且输出和tile属性相关的错误信息。

  rake test:units

 

 ....

Finished tests in 0.375000s, 10.6667 tests/s, 56.0000 assertions/s.

4 tests, 21 assertions, 0 failures, 0 errors, 0 skips

 

  如果想避免在Active Record错误中使用硬编码的字符串,可以将返回的消息和其内置的错误信息表进行比较来解决这个问题,用i18n函数,/test/unit/product_test.rb中使用如下测试方法:

 

 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.invalid?
    assert_equal [I18n.translate('errors.messages.taken')],
                 product.errors[:title]
  end

 

  rake test:units

  

E:\works\ruby\depot>rake test:units
        SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
        This poses a security threat. It is strongly recommended that you
        provide a secret to prevent exploits that may be possible from crafted
        cookies. This will not be supported in future versions of Rack, and
        future versions will even invalidate your existing user cookies.

        Called from: D:/dev/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/actionpack-3.2.1/lib/action_dispatch/middleware/session/abstract_store.rb:28:in `initialize'.

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

# Running tests:

...F.

Finished tests in 0.390625s, 12.8000 tests/s, 58.8800 assertions/s.

  1) Failure:
test_product_is_not_valid_without_a_unique_title_-_i18n(ProductTest) [E:/works/ruby/depot/test/unit/product_test.rb:69]:
<["translation missing: en.errors.messages.taken"]> expected but was
<["has already been taken"]>.

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

 /test/unit/product_test.rb文件最终代码:

 

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
	fixtures :products
  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
  
  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]

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

    product.price = 1
    assert product.valid?
  end
  
  def new_product(image_url)
    Product.new(title:       "My Book Title",
                description: "yyy",
                price:       1,
                image_url:   image_url)
  end

  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

  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]
  end

  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.invalid?
    assert_equal [I18n.translate(['activerecord.errors.messages.taken'])],
                 product.errors[:title]
  end
   
end

 

  现在该商品现在有一个模型、一组视图、一个控制器和一组单元测试。

 

 

你可能感兴趣的:(Rails,ruby on rails,depot)