Rails读书笔记第七章

Validation and Unit Test

Validation

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数据库。

Controller 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 case中如果assert为false,则在这个test casel中后面的代码不被执行。

编辑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。

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组成。

  • product.errors返回的是hash表,key是属性,value是错误信息:
  • presence对应”can't be blank“
  • numericality 对应”is not a number“,或者”must be greater than or equal to 0.01“

Text Fixtures

在测试title的uniqueness时,我们可以先建立一个product并保存,然后再建立一个有同样title的product并尝试保存,这样当然是可以的。但是Rails提供了更简便的方法。
在Rails中,一个text fixture就是sample data,完成对一个model的数据填充。在text/fixtures目录下,有products.yml这个YAML文件,定义了对Product model测试使用的fixture。每个YAML文件定义了一种model的初始化数据,文件名和数据库中的table名要对应。例如我们定义了Product这个model,那么数据库中的table就叫products,对应的fixture文件名也是products。
编辑test/fixtures/products.yml文件内容如下:
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
这里默认定义了两条record(row),分别叫one和two。在yml文件中缩进很重要,而且只能使用空格,不能使用tab。我们加入一条ruby的record。
不论是在products_controller_test.rb文件中还是在product_test.rb文件中,都有:
require 'test_helper'
在test/test_helper.rb文件中定义如下:

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。
Rails创建了三种数据库:
  • db/development.sqlite3 是development的数据库,用于开发时。
  • db/test.sqlite3 是测试数据库
  • db/production.sqlite3 是发online布的数据库

每个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是用来初始化测试数据的。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值