AWD 07: Tarea B: Validación y Unit Testing - LPC-Ltda/Ruby-on-Rails GitHub Wiki
##7.1 Iteración B: Validando!
Partamos validando
- que todos los string contengan algo antes de que la fila sea agregada.
- que el precio sea un número positivo válido.
- que el título sea único
- que la url de la imagen sea válida
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
El método validates() es el validador estándar de Rails. Puede chequear uno o más campos de modelo contra una o más condociones.
presence: true le cuenta al validador que chequee que cada campo nombrado esté presente y su contenido no esté vacío.
No le pedimos que sea mayor que cero porque si se ingresa '0.001' lo aceptará y aparecerá como cero.
Usamos la opción allow_blank para evitar múltiples mensajes de error cuando el campo esté en blanco.
Ahora corrimos nuestro test
$ rake test
Arroja dos errores uno en should create product y otro en should update product. La solución es dar data de prueba válida en `test/controllers/products_controller_test.rb
[rails40/depot_b/test/controllers/products_controller_test.rb]
require 'test_helper'
class ProductsControllerTest < AcionController::TestCase
setup do
@product = products(:one)
# Nuevo
@update = {
title: 'Lorem Ipsum',
description: 'Wibbles are fun!',
image_url: 'lorem.jpg',
price: 19.95
}
# Fin Nuevo
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
# Nuevo
post :create, product: @update
end
assert_redirected_to product_path(assigns(:product))
end
#...
test "should update product" do
# Nuevo
patch :update, id: @product, product: @update
assert_redirected_to product_path(assigns(:product))
end
#...
end
##7.2 Iteración B2: Unit Testing y modelos
product_test.rb es el archivo que Railsgeneró dentro de test/models/.
[rails40/depot_a/test/models/product_test.rb]
require 'test_helper'
class ProductTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
El ProductTest generado es una subclase de ActiveSupport::TestCase. La que es una subclase de MiniTest::unit::TestCase, lo que nos cuenta que Rails genera test basado en el framework MiniTest (http://www.ruby-doc.org/stdlib-2.0/libdoc/minitest/unit/rdoc/MiniTest.htmml) que viene preinstalado con Ruby.
Dentro de este test case, Rails generó un test comentado llamado "the truth". La sintaxis test ... do puede seorprenderle un poco pero Active Support esta combinando un método de clase, paréntesis opcionales y un block para definir un método de test en la forma más simple para usted.
La línea assert en este método es un test real. Todo lo que hace es probar si true es verdadero.
Un test Unit real
Probemos las validaciones. Primero, si creamos un producto sin setear los atributos, esperamos que no sea válido y que haya un error asociado con cada campo. Podemos usar los métodos errors() e invalid() del modelo para ver si valida, y podemos usar el método any?() de la lista de errores para ver si hay un error asociado a un atributo particular.
Ahora que sabemos que probar, necesitamos saber como contarle al framework si nuestro código pasa o falla. Hacemos esto usando assertions (afirmaciones). Una afirmación es sólo una llamada a un método que le cuenta al framework lo que esperamos sea verdadero.
La afirmación más simples es el método assert(), a cual espera que su argumento sea verdadero. Si lo es nada especial pasa, pero si es falso la afirmación falla. El framework emitirá un mensaje y detendrá la ejecución del método test que contiene la falla. En nuestro caso, esperamos que un modelo Product vacío no pase la validación, podemos expresar esa espectativa afirmando que no es válido.
assert product.invalid?
Reemplacemos el test "truth" por el siguiente código:
[rails40/depot_b/test/models/product_test.rb]
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
Corremos el test:
$ rake test:models
.
Finished tests in 0.257961s, 3.8766 test/s, 19.3828 assertions/s.
1 tests, 5 assertions, 0 failures, 0 errors, 0 skips
Ahora validemos el precio:
[rails40/depot_b/test/models/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
A continuación validamos que la URL de la imágen termine con .gif, .jpg p .png.
[rails40/depot_b/test/models/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} should be valid"
end
bad.each do |name|
assert new_product(name).invalid?, "#{name} shouldn't be valid"
end
end
En lugar de escribir nueve test separados, hicimos dos loops. Uno para chequear lo que debe pasar y otro para chequear lo que no.
Agregamos un mensaje al método asset el que será escrito como mensaje de error si la afirmación falla.
Finalmente nuestro modelo contiene una validación que chequea que todos los títulos de productos deben ser único. Para probar esto necesitaremos almacenar productos en la base de datos.
Una forma es crear un producto y luego intetar crear otro con el mismo nombre, pero hay una forma más simple de hacerlo, podemos usar las fixtures de Rails. (accesorios).
Test fixtures
En el mundo de las pruebas, una fixture es un ambiente en el cual usted puede correr una prueba. Si usted está probando una tarjeta de circuito, por ejemplo, usted puede montarla en un test fixture que le provea con la potencia y los inputs necesarios para conducir la función que se probará.
En el mundo de Rails, una fixture es simplemente una expecificación de un contenido inicial de un modelo (o modelos) bajo prueba.
Usted especifica data fixture en archivos en el directorio test/fixtures. Estos archivos data de prueba en formato YAML. Cada archivo fixture contiene la data de un único modelo. El nombre debe conicidir con el nombre de la tabla. Ya que necesitamos data para el modelo Product, el cual se almacena en la tabla products, necesitamos agregar un archivo llamado products.yml. Rails ya lo creo cuando creamos el modelo.
[rails40/depot_b/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
El archivo fixture contiene una entrada para cada fila que deseamos insertar en la base de datos. Cada fila tiene un nombre dado.
Usted debe usar espacios y no tabs al comienzo de cada línea de datos y todas las líneas deben tener la misma indentación.
[rails40/depot_b/test/fixtures/products.yml]
ruby:
title: Programming Ruby 1.9
description:
Ruby is the fastest growing and most exciting dynamic
languaje aout there. If you need to get working programs
delivered fast, you should add Ruby to your toolbox.
price: 49.50
image_url: ruby.png
Podems controlar que fixture se carga especificando la siguiente línea
[test/models/product_test.rb]
class ProductTest < ActiveSupport::TestCase
fixtures :products
end
En el caso de nuestra clase ProductTest, agregar la directiva fixtures significa que la tabla será vaciada y luego llenada con las filas definidas en la fixture antes de que el método sea ejecutado.
La mayoría de los scaffolding que Rails genera no contienen llamadas a métodos fixtures. Esto es porque los test por defecto cargan todas las fixtures antes de correr el test.
Cada método test toma una tabla recién inicializada en la base de datos de prueba, caragada de las fixtures que uno provee. Esto es automáticamente hecho por el comando rake test pero puede ser hecho separadamente corriendo rake db:test:prepare.
Usando data de fixtures
Ahora que sabemos como cargar un fixture en la base de datos, necesitamos encontrar formas de usarla en nuestros test.
Una forma es usar los metodos finder en el modelo para leer la data. Sin embargo, Rails lo hace más simple que eso. Para cada fixture cargada en un test, Rails define un método con el mismo nombre que la fixture. Usted puede usar ese método para acceder al objeto del modelo pre-cargado, sólo pase el nombre de la fila como está definido en el archivo YAML.
[rails40/depot_c/test/models/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.invalid?
assert_equal ["has already been taken"], product.errors[:title]
end
Si usted desea evitar usarl la descripción de los errores puede comparar contra la tabla de mensajes.
[rails40/depot_c/test/models/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.invalid?
assert_equal [I18n.translate('errors.messages.taken')], product.errors[:title]
end