Shoulda es un plugin para hacer BDD en Rails. Se trata de una alternativa sencilla a RSpec que, a diferencia de este, no proporciona un framework completo sino una serie de helpers, macros y assertions sobre la librería estándar de testing de Ruby. Así, permite escribir tests especificar comportamientos, por ejemplo, de la siguiente manera:

class PostTest < Test::Unit::TestCase
  load_all_fixtures
 
  should_belong_to :user
  should_have_many :tags, :through => :taggings
 
  should_require_unique_attributes :title
  should_require_attributes :body, :message => /wtf/
  should_require_attributes :title
  should_only_allow_numeric_values_for :user_id
end

He robado el ejemplo de la misma página principal del plugin. El caso es que, para satisfacer el test, podríamos escribir una clase parecida a esta:

class Post < ActiveRecord::Base
  belongs_to :user
  has_many :tags, :through => :taggings
 
  validates_uniqueness_of :title
  validates_presence_of :body, :message => "wtf"
  validates_presence_of :title
  validates_numericability_of :user_id
end

No lo he llegado a testear, pero tiene pinta de que más o menos esa sería la implementación. La cuestión es que no dejo de tener la sensación de que hay algo incorrecto en esta manera de testear. El test se corresponde línea a línea con la implementación. Es lo mismo, pero dicho con otro lenguaje. Sería perfectamente posible generar la clase a partir del test. Con lo que llego a la conclusión de que lo único que hacemos siguiendo este enfoque es escribir dos veces lo mismo, duplicar la información y, por tanto, hacer menos mantenible nuestra aplicación.

Me inquieta especialmente pensar que Shoulda no es más que una refactorización sobre la manera estándar de testear en Rails. Por ejemplo:

class PostTest < Test::Unit::TestCase
  ...
  # Esto:
  should_require_attributes :title
 
  # Es lo mismo que esto:
  def test_should_require_title
    post = Post.new
    assert !post.valid?
    assert post.errors.on(:title)
  end

De hecho, con un poquito de metaprogramación podríamos facilmente escribir nuestra propia versión del should_require_attributes:

def should_require_attributes(attribute)
  define_method "test_should_require_#{attribute}" do
    klass = self.name.gsub(/Test$/, '').constantize
    object = klass.new
    assert !object.valid?
    assert object.errors.on(attribute)
  end
end

Y he aquí la angustiosa conclusión a la que llego, que me hace despertar con taquicardias por los noches: Si los tests declarativos de Shoulda me parecen incorrectos, y los tests declarativos de Shoulda son solo una refactorización y, por tanto, equivalentes funcionalmente a la manera normal de testear en Rails:

TODO LO DECLARATIVO DE MIS MODELOS NO TIENE SENTIDO SER TESTEADO A NIVEL DE UNIDAD

Dicho queda, en mayúsculas y negrita. A ver si alguien encuentra el error en mi razonamiento, el detalle que se me escapa, desmonta mi teoría, y por fin me devuelve el sosiego, la paz interior y el flow.


6 Responses to “¿Una verdad incómoda?”  

  1. 1 Fernando

    Gran post, sí señor.

    En mi opinión la diferencia entre escribir tests unitarios sobre la parte declarativa sí que tiene sentido por una razón:

    si tu modelo post no estuviera relacionado con un usuario, ¿tu modelo seguiría siendo correcto? ¿o debería de haber algo que hiciera saltar una alarma y dijera: “esto no está bien, tiene que existir un usuario asociado al post”?

    No sé si el lugar para definir esa “alerta” es un test de unidad, porque de teoría controlo bastante poco, pero a mí no me parece mal sitio, ¿no?

  2. 2 Ernesto Jiménez

    Yo diferenciaría relaciones y validaciones.

    Las relaciones no las testeo prácticamente nunca (si se pierde un belongs_to, saltará en cualquiera de las decenas de tests que lo usan), y puestos a testearlo, testearía que el modelo proporciona la interfaz de la relación, no la relación en sí (de eso ya se encargan los tests de AR)

    Sobre las validaciones, eso ya es otro cantar. La cuestión de testear las validaciones viene del concepto de “fat models” donde tenemos controllers que prácticamente no hacen nada.

    e.g:

    def create
      @post = Post.find(params[:post_id])
      @comment = @post.comments.build(params[:comment])
      
      if @comment.save
        notify :notice, 'Comment added'
      else
        notify_errors @comment
      end
    end

    Como ves, toda la carga de la validación cae sobre el modelo, con lo que deberíamos testear esa parte.

    Lo que sí que creo es que quizá cabría hacer una mejor refactorización para testear los validadores (y asociaciones). Dicha refactorización consistiría en no escribir esos tests, sino que el framework identificase las validaciones y crease los tests por metaprogramación.

    Algún día quizá intente hacer el auto_testeado, al fin y al cabo, lo que estamos haciendo con shoulda es repetir lo que ya hemos escrito en el modelo :)

  3. 3 Ernesto Jiménez

    Anoche después de comentar tenía el gusanillo, así que cutre-codee esto:
    http://pastie.caboo.se/141826

    Limitaciones, si tienes alguna clase de tests de modelo que no deriva de Test::Units los tests cascarán.

    Si alguien quiere meterle mano al plugin ya tiene un primer comienzo ;)

    Un saludo!

  4. 4 Juanjo Bazán

    Creo que tienes toda la razón Luismi, en general testear la parte declarativa de los modelos no tiene mucho sentido, en realidad no testeamos nuestra aplicación sino que estamos repitiendo uno de los tests de rails.

    La útilidad de alerta que menciona Fernando creo que en la práctica desaparece y esos tests en general casi no sirven ni para avisar de que la implementación ha cambiado porque lo primero que va a hacer el programador que borre un validate_presence_of es ir y borrar el test correspondiente. Estamos utilizando un test para explicitar que las lineas de código en las que se establecen relaciones son importantes y no hay que borrarlas, pero eso debería ser obvio.

    Sin embargo sí creo que hay algun caso en el que esos tests son útiles, por ejemplo: en un desarrollo BDD puro en el que los tests se escriben antes que el código las especificaciones de los modelos pueden ser directamente los tests, escritos potencialmente por una persona distinta de quien se encargará después de la implementación.
    En ese caso sí son útiles los tests sobre validaciones o relaciones, aunque más como guía o requerimientos previos que como chequeo de correcto funcionamiento.

  5. 5 Luismi Cavallé

    @fernando: Esa “alerta” de la que hablas no sería más que un recordatorio cada vez que quitases alguna de las declaraciones de tu modelo. “— Oye, que has quitado el has_many! — Ya, ya se que lo he quitado”. Pienso que finalmente esa relación existe para satisfacer alguna funcionalidad y esta puede ser testada a alto nivel con un test de integración.

    @ernesto: Interesante propuesta. Aunque no le acabo de ver utilidad xD, no sirve ni como la “alarma” de la que habla Fernando ni como la “guía para implementar” de la que habla Juanjo. Sin embargo, sí que sirve para apoyar mi argumento: si podemos llegar a automatizar la generación de la implementación a partir del test o, incluso como tú demuestras, el test a partir de la implementación, entonces es que algo falla, no?

    @juanjo: Incluso el escenario que propones en el que una persona escribe el test/especificación y otro la implementación creo que tiene más sentido a nivel de integración que a nivel de unidad.

  6. 6 Ernesto Jiménez

    @luismi: cierto que no sirve para nada, pero como tú has dicho en tu último post: hay que alimentar al hacker xDDDD

Leave a Reply