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