En general Test suele ser sinónimo de aburrimiento, al menos en lo que a la opinión popular se refiere. Conozco pocos programadores que realmente disfruten escribiendo tests o que lo hagan sin una petición explícita ya sea por parte de sus jefes, el departamento de calidad… etc.
En general creo que existe el concepto de que uno programa su código y debe haber otra persona que se encargue de hacer el testing y reportarnos los bugs.
Si bien el concepto de Test y QA (Quality Assurance) están relacionados, lo uno no debiera reemplazar a lo otro.
Sobre si se debe escribir tests o no, o qué técnica es la verdadera y única (TDD, BDD, ?DD….) se han escrito infinidad de páginas, y la verdad no me interesa entrar en polémicas, así que os voy a explicar desde mi punto de vista por qué usar tests es conveniente para cualquier programador.
Un test se podría definir como una inversión a largo plazo, es cierto que inicialmente consume parte del tiempo (que como programadores estamos deseando invertir en desarrollar nuestra lógica y hacer una bonita interfaz) pero a lo largo de la vida del proyecto uno disfruta de los beneficios.
Pero dejémonos de rodeos y teoría, vamos a entrar en materia
Para este ejemplo usaré rspec, un framework de testing para Ruby on Rails.
Sus creadores lo definen como un BDD (Behavior Driven Development) framework para ruby, y sin entrar en mayores detalles sobre lo que representa el BDD diré que la filosofía detrás de esta metodología de testing consiste en centrarnos más en el cómo debe comportarse nuestra aplicación sin necesidad de preocuparnos de los detalles de implementación.
Toda esta parafernalia técnica está muy bien, pero para mí simplemente significa que escribir tests deja de ser una tarea aburrida y pasan a formar parte fundamental del código y sobre todo de la documentación.
Y esto es importante Test = Documentación, porque cuando podemos presentar nuestros tests como la documentación fiable y actualizada de nuestro código estamos matando dos pájaros de un tiro (y nos ahorramos la pesada tarea de hacer esos documentos en word con screenshots y frases cortas del tipo “X hace Y”.
Nota: He usado rspec como ejemplo y como framework para mis proyectos porque me gusta, eso no quiere decir que no podamos obtener los mismos resultados usando otros framework de testing.
Ejemplos perfectamente válidos son JUnit, RUnit, PyUnit, PHPUnit … y un largo etcétera. Prácticamente cualquier lenguaje de programación dispone de varios frameworks para testing que podréis usar según vuestras preferencias.
Igualmente da lo mismo qué filosofía de testing decidáis usar, TDD, BDD, WhateverDD, un test simplemente consiste en tener la seguridad de que independientemente de los cambios que se hagan en el código en las diversas refactorizaciones o ampliaciones del mismo, lo que funcionaba antes sigue funcionando.
Hechas las aclaraciones pertinentes…
Instalar rspec es relativamente sencillo, instalamos el gem:
[sudo] gem install rspec-rails
Y en nuestra aplicación corremos el script:
ruby script/generate rspec
Este script nos creará una nueva carpeta en nuestro proyecto llamada spec…
… con varias subcarpetas.
Para los que estéis acostumbrados a Rails y a MVC en general veréis que existe una carpeta para los controladores, otra para los modelos y otra para las vistas. Estas carpetas contendrán los tests (o specs) que escribiremos para cada uno de esos elementos.
Adicionalmente tenemos la carpeta de helpers donde podremos escribir funciones auxiliares que vayamos a usar en nuestos tests y la carpeta de fixtures, donde podemos definir algo así como los mock objects que usaremos.
Bien, cómo creamos un test? Fácil. Rspec pone a nuestra disposición varios scripts que podemos usar integrados con Rails.
Por ejemplo, al crear un controlador habitualmente escribimos el comando:
./script/generate controller user
(siendo user un nombre cualquiera)
Bien, rspec nos permite hacer lo mismo pero con un sutil cambio:
./script/generate rspec_controller user
Este comando, además de generar nuestro habitual /app/controllers/user_controller.rb
class UserController < ApplicationController
end
creará un archivo en la carpeta /spec/controllers llamado user_controller_spec.rb donde podremos escribir nuestros specs.
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe UserController do
#Delete this example and add some real ones
it "should use UserController" do
controller.should be_an_instance_of(UserController)
end
end
Qué contiene este archivo? primero carga el spec_helper para hacer disponibles las funciones auxiliares que hayamos definido y por otro lado comienza a describir nuestro controlador con el comando describe.
Describe sigue la habitual estructura de bloques de RoR conformando un bloque…
describe UserController do
#Specs varios...
end
…donde comenzaremos a definir qué queremos que haga nuestro controlador.
Rspec nos genera un ejemplo (que borraremos en breve) que si bien no nos aporta gran cosa, sí nos da una pequeña idea de como escribir nuestros specs:
it "should descriptión de la acción" do
#Comportamiento deseado
end
En este punto estamos preparados para comenzar a codificar.
Los puristas del TDD dicen que “todo comienza con un test que falla”. Así que siguiendo este consejo vamos a escribir nuestro primer test sin haber escrito todavía una sola línea de código.
Borramos el test que nos viene por defecto y escribimos nuestro primer test, algo básico que nos sirva como ejemplo podría ser:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe UserController do
it "should render the register page" do
get :register
response.should render_template 'register'
end
end
Este sencillo spec puede entenderlo cualquier persona, aun sin conocer nada de programación (vale, esa persona debe conocer algo de inglés) y nos sirve para definir el comportamiento que deseamos para nuestra aplicación.
En este caso es trivial, estamos accediendo con un método get a la acción register y esperamos que la respuesta muestre la página de registro dentro del controlador de usuarios.
Así que en cierto modo estamos definiendo para nuestra aplicación el comportamiento de un usuario. Si un usuario hace click en el link de registrar nuestra aplicación navegará a la página de registro donde el usuario puede meter sus datos y registrarse.
Así pues hemos escrito nuestro primer test y estamos listos para ver si funciona.
Para ello rspec nos proporciona algunos scripts que podemos usar:
./script/spec path/to/directory/with/specs
correrá todos los spec que existan en un determinado directorio, y
./script/spec path/to/individual_spec.rb
correrá un determinado spec.
En nuestro caso escribiremos:
./script/spec spec/controllers/user_controller_spec.rb
Y la respuesta que obtendremos probablemente será algo como:
ActionController::UnknownAction in ‘UserController should render the register page’
No action responded to register. Actions:
…..
Finished in 0.097963 seconds
1 example, 1 failure
Tenemos nuestro primer fallo!, así que según los puristas del TDD vamos por el buen camino.
Por lo que leemos al parecer se está quejando de que no reconoce una acción llamada register, así que vamos a definirla.
def register
#Código para registrar un usuario, vacío por el momento
end
Volvemos a correr nuestro test
./script/spec spec/controllers/user_controller_spec.rb
y ahora la respuesta debiera ser algo como:
Finished in 0.223996 seconds
1 example, 0 failures
Funciona!, a partir de ahora hemos definido que al acceder a la acción registrar usuarios siempre debemos presentar la página de registro.
Este ejemplo, aunque parezca trivial y una tontería, nos sirve para definir cómo trabajar con tests.
Comenzamos escribiendo algunas especificaciones para nuestro código y luego nos aseguramos de que el código cumpla con lo definido.
Por qué no al revés? Por qué no escribir primero el código y luego los test?
Hay diversas razones que nos animan a hacerlo de esta manera.
Por un lado en muchas ocasiones tenemos ideas, ya sean propias o de nuestros clientes, que se desean incorporar a un proyecto. Estas ideas, requisitos, funcionalidades, etc… especificaciones a fin de cuentas (o specs para abreviar) definen el comportamiento de la aplicación y deben ser escritas independientemente de la implementación que se vaya a realizar posteriormente. (otra cosa es poder hacerlo o no, pero eso no nos importa en este momento)
Codificando inicialmente y posteriormente escribiendo los tests de alguna manera estamos imponiendo nuestra implementación sobre la especificación, pudiendo llegar a “corromperla” en cierto modo.
Por otro lado en ocasiones simplemente deseamos anotar cosas que deseamos que nuestra aplicación haga, sin tiempo para ponernos a codificar. Tal vez estemos redactando un documento de especificación y deseamos presentar lo que hace nuestra aplicación y obtener el feedback de nuestro cliente antes de emplear recursos.
En fin, las razones son varias. No me considero un evangelista, de esos hay muchos por ahí pululando. Si os interesa realmente el tema buscad sobre TDD o BDD en google y probablemente encontréis más de lo que podáis digerir.
Siguiendo con nuestro ejemplo, alguien podría decir que el test es un poco ciego. Estamos esperando que se presente la página de registro pero en realidad no tenemos página de registro y sin embargo el test funciona.
Esto nos lleva a dos conceptos interesantes.
Convention over configuration: Rails sabe que debe existir un template con el mismo nombre que la acción que define, así que siguiendo la convención, rspec sabe que en algún momento debe existir un template con ese nombre, así que no se preocupa de si está presente o no.
Rspec es flexible: La cantidad de opciones que nos permite rspec es abrumadora, de hecho es una de las causas por las que tiene una curva de aprendizaje no tan suave como a muchos nos gustaría. Sus programadores tuvieron en cuenta las diversas capas del MVC a la hora del diseño y por tanto permite la abstracción en cuanto a la capa de presentación.
No obstante si ninguna de estos conceptos te convence, no hay problema, podemos usar un comando para que rspec integre las restricciones de las vistas dentro de los tests de nuestro controlador, basta con escribir integrate_views.
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe UserController do
integrate_views
it "should render the register page" do
get :register
response.should render_template 'register'
end
end
Si volvemos a correr nuestro test ahora debieramos obtener algo como:
1)
ActionView::MissingTemplate in ‘UserController should render the register page’
Missing template user/register.erb in view path /Users/cads/post/app/views:
….
Finished in 0.101716 seconds
1 example, 1 failure
Ahora efectivamente se queja de que le falta la página de registro, así que debemos proporcionársela para que se quede contento.
Un ejemplo rápido podría ser:
app/views/user/register.html.erb
<div id="title">
<h2>User Registration Page</h2>
</div>
<div id="form_fields">
Username: <%= text_field_tag 'user_name', nil, :size => 20 %>
<br/>
Password: <%= password_field_tag 'password', nil, :size => 20 %>
</div>
<div id="buttons">
<%= submit_tag "Register" %>
</div>
Si corremos ahora nuestro spec vemos que ya está contento:
Finished in 0.096693 seconds
1 example, 0 failures
Rspec proporciona una potente herramienta para testear interfaces, permitiéndonos validar incluso la existencia de tags en las vistas.
it "should render the register page" do
get :register
response.should render_template 'register'
#Vamos a checkear que existan los 3 divs principales
response.should have_tag("div[id=title]")
response.should have_tag("div[id=form_fields]")
response.should have_tag("div[id=buttons]")
end
Pero esto nos desvía, para más información sobre rspec podéis visitar su página de documentación (buena suerte)
El si se debe incluir la validación de interfaces dentro de los tests de un controlador es motivo de polémica y discusiones.
Podemos crear specs asociados a vistas y dedicarnos por completo a testear cada una de las vistas de nuestra aplicación.
Para ello bastaría con crear el archivo spec/views/user/template_spec.rb con un formato similar al anterior.
Nota: Si al generar nuestro rspec_controller indicamos las acciones del controlador rspec generará automáticamente specs para cada una de las vistas asociadas a las acciones.
Así pues, si hubiéramos generado nuestro user_controller de esta manera:
./script/generate rspec_controller user register
rspec habría generado: spec/views/user/register.html.erb_spec.rb
describe "/user/register do
before(:each) do
render 'user/register'
end
#Delete this example and add some real ones or delete this file
it "should tell you where to find the file" do
response.should have_tag('p', %r[Find me in app/views/user/register])
end
end
Pero como digo, no quiero complicaros la existencia con las ramificaciones de rspec.
Retomando nuestro spec, vamos a generar otra descripción no tan trivial:
it "should register a user" do
post :register, :user => {:user_name => 'Anakin', :password => 'yoda'}
#Si lo ha creado bien debe redireccionarnos a la pagina del profile
response.should redirect_to :action => 'profile'
#El usuario Anakin debe existir y estar almacenado en nuesta base de datos
User.find_by_user_name('Anakin').should_not be_nil
end
Ahora tenemos dos specs que podemos correr, con el resultado:
.F
1)
‘UserController should register a user’ FAILED
expected redirect to {:action=>”profile”}, got no redirect
./spec/controllers/user_controller_spec.rb:18:
./script/spec:5:
Finished in 0.099972 seconds
2 examples, 1 failure
Nuevamente tenemos un test que falla, y tenemos una nueva funcionalidad y comportamiento que nos interesa, así que ahora podemos implementarla:
class UserController < ApplicationController
def register
#Registrará un usuario
if request.post? and params[:user]
user = User.new(params[:user])
if user.save
redirect_to :action => 'profile'
end
end
end
def profile
#Traerá el perfil del usuario
end
end
Ahora el código debiera cumplir con lo que pide el test, así que volvemos a correr nuestro spec y…
.F
1)
NameError in ‘UserController should register a user’
uninitialized constant UserController::User
….
Finished in 0.102123 seconds
2 examples, 1 failure
Cierto! se nos había olvidado crear el modelo User.
Como vemos, a partir de un simple test vamos dando forma a nuestra aplicación, escribiendo código que cubra las necesidades de nuestras especificaciones y asegurándonos de esta manera que todo funcione según lo definido.
Así pues generamos nuestro modelo:
./script/generate rspec_model user
Definimos los atributos:
db/migrate/create_users.rb
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.column :user_name, :string
t.column :password, :string
t.timestamps
end
end
def self.down
drop_table :users
end
end
migramos
rake db:migrate
y
rake db:test:prepare
y volvemos a correr nuestro spec:
..
Finished in 0.109147 seconds
2 examples, 0 failures
Y listo, nuevamente todo funciona nuevamente.
Si os habéis fijado a la hora de generar el modelo he usado un rspec_model.
Al igual que con los controladores y las vistas, rspec nos permite crear tests para nuestros modelos y al igual que antes podemos encontrar el spec generado en spec/models/user_spec.rb
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe User do
before(:each) do
@valid_attributes = {
}
end
it "should create a new instance given valid attributes" do
User.create!(@valid_attributes)
end
end
El cual, incialmente es trivial como el resto de sus compañeros, pero nos permite validar el correcto comportamiento de los modelos.
Por ejemplo si quisieramos asegurarnos de que debe existir un user_name y un password para nuestro modelo user podemos escribir:
class User < ActiveRecord::Base
validates_presence_of :user_name, :password
end
Lo cual nos asegura de que nuestros usuarios para ser válidos deben tener un nombre de usuario y un password.
Si corremos ahora nuestro user_spec.rb debiera fallar:
./script/spec spec/models/user_spec.rb
F
1)
ActiveRecord::RecordInvalid in ‘User should create a new instance given valid attributes’
Validation failed: Password can’t be blank, User name can’t be blank
…
Finished in 0.158742 seconds
1 example, 1 failure
Ahora requerimos que nuestros usuarios tengan ciertos atributos, asi que podemos requerirlos en el test y validar que todo siga correctamente.
Para ello basta con definir dentro de @valid_attributes los atributos que requiramos como obligatorios:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe User do
before(:each) do
@valid_attributes = { :user_name => 'Sheldom', :password => 'Leonard'
}
end
it "should create a new instance given valid attributes" do
User.create!(@valid_attributes)
end
end
Si ahora volvemos a correr nuestro test veremos que…
.
Finished in 0.1016 seconds
1 example, 0 failures
Todo vuelve a funcionar.
Con esto termino este laaargo post.
La idea es que partiendo de tests podemos desarrollar el resto de la aplicación de una manera ordenada y con la seguridad de que si en el futuro, debemos refactorizar nuestro código (algo bastante habitual) y teniendo tests no “contaminados” con los detalles de la implementación, podemos estar relativamente seguros de que todo está bien sin tener que volver a navegar manualmente por todas y cada una de las funcionalidades de nuestra aplicación.
Nota: Correr los tests a mano cada vez que uno hace un cambio es bastante pesado y cansino, así que hay diversas herramientas que se encargan de hacerlo por nosotros.
Yo en concreto uso rspactor que se encarga de correr automáticamente los specs relacionados cada vez que se guardo un cambio en algun archivo y genera notificaciones con growl.
Para los que no uséis mac sé que hay versiones similares que corren en windows o en linux, pero ahora mismo no tengo el enlace por aquí.
Enjoy