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
现在该商品现在有一个模型、一组视图、一个控制器和一组单元测试。
推荐阅读
-
使用Ruby on Rails快速开发web应用的教程实例
-
对Robbin《ruby on rails为什么暂时无法成为企业应用开发的主流?》的一些思考
-
Ruby on Rails所构建的应用程序基本目录结构总结
-
23个优秀的开源Ruby On Rails应用
-
Android学习笔记三:第一个应用程序的扩充
-
对Ruby on Rails进行高效的单元测试的教程
-
使用Ruby on Rails快速开发web应用的教程实例
-
Ruby on Rails所构建的应用程序基本目录结构总结
-
ruby on rails为什么暂时无法成为企业应用开发的主流?
-
ruby on rails为什么暂时无法成为企业应用开发的主流?