Model layer是gatekeeper,数据都是要经过Model才能进出database,所以在Model层做validation验证。例如,数据库中的product不能有空的title、description、非法imageurl或者非法price。
Model的文件在app/models下,例如app/models/product.rb的内容最初为空,添加validation后如下:
class Product < ActiveRecord::Base
validates :title, :description, :image_url, presence: true
validates :price, numericality: {greater_than_or_equal_to: 0.01}
validates :title, uniqueness: true
validates :image_url, allow_blank: true, format: {
with: %r{\.(gif|jpg|png)\Z}i,
message: 'must be a URL for GIF, JPG or PNG image.'
}
end
在上面的Product的Model中,validate是Rails的标准validator。
presence: true用来检测属性要存在,内容不能为空。
numericality 用来验证数字。回想一下对Product schema的定义(db/schema.rb)因为定义数据库时对price的constraint是:
t.decimal "price", precision: 8, scale: 2
数据库对于price的要求是有效位为8位,两位小数。
Model对于price的要求是要为数字,并且不小于0.01.
其实这两个要求还是有差距的。如果Model获得的price为1.111,那么实际上数据库中price是1.11。
uniqueness: true 用来保证属性值唯一
allow_blank: true 允许为空
format 用regex来保证后缀名为gif/jpg/png.
这样在http://localhost:3000/products/new 创建新的product时,必须要满足上面的限制,否则页面会弹出错误信息。即Model层的Validation保证了页面输入数据的合法性。Controller将数据交Model后会进行validation,每个validate语句依次执行。
这里在image_url加上了allow_black是因为如果image_url留空的话,在这里会报两个错,一个是由第一个validate产生的image_url不能为空,另一个是由这个validate产生的要以.jpg等结尾。加上了allow_blank的话,image_url留空只报第一个错,当image_url不为空的时候才会验证是否已jpg等结尾。
在products数据库中,因为会默认生成ID作为primary_key,所以数据库本身对其他属性并没有非空的要求。
可以通过下面的方法让数据库对title加入unique的constraint:
rails generate migration add_index_to_products title:uniq
然后打开这个migration文件,将add_column删除:
class AddIndexToProducts < ActiveRecord::Migration
def change
add_index :products, :title, unique: true
end
end
其实也可以先drop掉当前的migration,然后编辑migration文件,在create_table后面添加上上面add_index的命令。
然后运行:rake db:migrate
这样在db/schema.rb中就变为:
ActiveRecord::Schema.define(version: 20151012221043) do
create_table "products", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "image_url"
t.decimal "price", precision: 6, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "products", ["title"], name: "index_products_on_title", unique: true
end
生成scaffold时也可直接定义title:string:uniq。
这样如果在app/models/product.rb文件中没有对title的uniqueness做检查,如果插入一个已有的title时,Model不会报错,但是数据库会报错了:
SQLite3::ConstraintException: UNIQUE constraint failed
同样地,如果在数据库定义了title是unique的,当fixture提供了one和two有相同的title的话,test会报上面的错误。
也就是说,Rails在测试中装载fixture时,并没有经过Model的validation,直接写入了test数据库。
此时运行rake test会报错,是因为没有给测试提供测试数据。
test继承自TestCase,基本格式如下:
test “the truth” do
assert true
end
assert的语法介绍如下:
assert test, "This test should be true"
assert(test, "This test should be true")
test返回值应该为true,否则显示后面的错误信息。使用括号时与assert之间不能有空格。
编辑test/controllers/products_controller_test.rb文件如下:
require 'test_helper'
class ProductsControllerTest < ActionController::TestCase
setup do
@product = products(:one) #使用了fixture中名为one的record
@update = {
title: 'Lorem Ipsum',
description: 'Wibbles are fun!',
image_url: 'lorem.jpg',
price: 19.95
}
end
test "should get index" do
get :index
assert_response :success
assert_not_nil assigns(:products)
end
test "should get new" do
get :new
assert_response :success
end
test "should create product" do
assert_difference('Product.count') do #在执行do..end中的命令后Product.count应该发生变化,添加了一个prodcut
# post :create, product: { description: @product.description, image_url: @product.image_url, price: @product.price, title: @product.title }
post :create, product: @update
end
assert_redirected_to product_path(assigns(:product))
end
test "should show product" do
get :show, id: @product
assert_response :success
end
test "should get edit" do
get :edit, id: @product
assert_response :success
end
test "should update product" do
#patch :update, id: @product, product: { description: @product.description, image_url: @product.image_url, price: @product.price, title: @product.title }
put :update, id: @product, product: @update
assert_redirected_to product_path(assigns(:product))
end
test "should destroy product" do
assert_difference('Product.count', -1) do
delete :destroy, id: @product
end
assert_redirected_to products_path
end
end
在上面的controller的test中,测试了新建product,编辑product,显示product,删除product等功能。@update定义了一个hash,作为参数传到测试中。
从代码可以看出,在test/controllers/products_controller_test.rb文件中定义了对app/controllers/products_controller.r中各个action的unit test。
在test/models/product_test.rb文件中添加对Product Model的各种测试:
require 'test_helper'
class ProductTest < ActiveSupport::TestCase
test "product attributes must not be empty" do
product = Product.new #生成一个Product
assert product.invalid? #调用Product的validate函数,由于没有给各属性赋值,invalid应该为true
assert product.errors[:title].any? #title的validation应该报错,any应为true
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", #新建了一个Product并给属性赋值
description: "yyy",
image_url: "zzz.jpg")
product.price = -1 #给price赋一个非法值
assert product.invalid? #调用Product的validate函数,invalid应为true
assert_equal ["must be greater than or equal to 0.01"], #price报错的信息应该等于“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 #给price赋一个合法值
assert product.valid? #Product的validation应该为true
end
def new_product(image_url) #根据image_url来生成Product
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 #定义合法图片url
http://a.b.c/x/y/z/fred.gif }
bad = %w{ fred.doc fred.gif/more fred.gif.more } #定义非法图片url
ok.each do |name|
assert new_product(name).valid?, "#{name} shouldn't be invalid" #调用new_product方法,根据图片url生成product,valid应该为true,否则报错
end
bad.each do |name|
assert new_product(name).invalid?, "#{name} shouldn't be valid" #同上,invalid应该为true,否则报错
end
end
test "product is not valid without a unique title" do
product = Product.new(title: products(:ruby).title, #新建了一个product并给属性赋值,其中的title使用了fixture里的ruby测试数据
description: "yyy",
price: 1,
image_url: "fred.gif")
assert product.invalid? #由于title应该是unique的,invalid应该为true。也可尝试product.save,保存不成功。
assert_equal ["has already been taken"], product.errors[:title] #title报错信息应该为"has already been taken"
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
从代码可以看出,test/models/product_test.rb文件定义了对于app/models/product.rb中的validation的各种测试,由各种属性的unit test组成。
one:
title: MyString
description: MyText
image_url: MyString
price: 9.99
two:
title: MyString
description: MyText
image_url: MyString
price: 9.99
#START:ruby
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
require 'test_helper'
fixtures :all
这样在每个test method运行前,就会先把fixture中的所有record填入products table。这里有点奇怪,因为fixture里可以定义对于Model来说不合法的record,如果对于数据库也不合法的话,在运行test时会报SQLite的constraint fail;如果对于数据库合法的话,即便对于Model不合法,也可进行测试。所以,fixture在这里的作用是用来测试Model的。而数据库这一层的validate有数据库来做,不过如果数据库层报错,就是很严重的了。所以应该尽量在Model层将输入数据控制好,避免数据过了Model层但是却在数据库层发送错误。
@product = products(:one)
通过上面的命令,我们就可以获得名为one的record。
每个test method都会得到从test database获得一个用fixture初始化的table。rake test命令会自动完成这些步骤,当然你也可以通过下面的命令单独完成:
rake db:test:prepare
Rails会定义一个和fixture名字一样的方法,例如对于products.yml这个fixture,就有products()这个方法,传入record的名字就可以获得含有该record的model object。例如使用ruby这个record:
@product = products(:ruby)
实际上products是一个空数组(Array),由products(:ruby)返回的是一个用ruby fixture测试数据填充的Product Model类对象。
单独运行unit test可以使用下面的命令:
rake test:unit
综合第六章,seed:db是用来初始化development数据库的,用于view的显示。fixture是用来初始化测试数据的。