Voy a hablar de Mocks y Stubs desde el punto de vista de Behaviour Driven Development.
Los ejemplos de código usan RSpec y el framework de mocks y stubs es Mocha.
En BDD los desarrolladores especifican el comportamiento del código.
En cada test debes centrarte solamente en el elemento que estás testeando.
Pero normalmente estos elementos interactuan con otros elementos, los cuales a su vez interactuan con otros.
Incluso se da el caso en el que el código que esta siendo testeado depende de elementos que aún no han sido programados.
Así pues, se necesita de un mecanismo que ayude a aislar el código a testear sin preocuparnos del comportamiento de los elementos ajenos al mismo.
Este mecanismo son los Mocks y los Stubs, elementos que fingen o simulan ser otros objetos.
Estos terminos fueron introducidos por Gerard Meszaros en su libro XUnit Test Patterns, donde estos y otros elementos más son englobados como Test Doubles.
Segun Meszaros:
- Los Stubs proporcionan respuestas prefedinidas a ciertas llamadas durante los tests, sin responder a cualquier otra cosa para la que no hayan sido programados.
- Los Mocks son objetos preprogramados con expectativas que conforman la especificación de lo que se espera que reciban las llamadas.
Estos elementos son a menudo confundidos y se suele usar los mocks como stubs y viceversa, ya que pueden parecer lo mismo, sin embargo, la diferencia entre los dos, es que los mocks se utilizan para verificar el comportamiento de un elemento y los stubs para verificar el estado.
Stubs
Imaginemos que tenemos la siguiente clase Constructor:
class Constructor
def initialize(object)
@object = object
end
def show
"Constructor: #{@object.name}"
end
end
La clase Constructor necesita de un objecto para ser instanciada. Este objecto eberá implementar el metodo 'name()' ya que lo necesita el metodo 'show()' de Constructor para devolver su valor.
Si queremos testear el metodo 'show()', necesitamos disponer de una clase que al ser instanciada responda al método 'name()'. Por tanto necesitamos algo que simule ser lo que necesitamos.
Podemos crear una clase para nuestro test que responda únicamente al metodo 'name()'.
class StubObject
def name
'Object name'
end
end
Ahora ya podemos testear el metodo 'show()' de nuestra clase Constructor:
describe Constructor do
it 'should show the constructor name' do
constructor = Constructor.new(StubObject.new)
constructor.show.should == 'Constructor: Object Name'
end
end
En este ejemplo, la clase StubObject es nuestro stub, y su implementación del metodo 'name()' es un método stub, un método programado para responder un valor.
Este stub no es nada flexible, porque siempre devuelve el mismo valor y sólo nos sirve para este test, pero en vez de crear una clase Stub para cada test podemos usar el método "stubs()" que nos ofrece el framework Mocha.
describe Constructor do
it 'should show the constructor name' do
stub = stubs(:name => 'The object name')
constructor = Constructor.new(stub)
constructor.show.should == 'Constructor: The Object name'
end
end
Los Stubs pueden ser usados para simular complejas llamadas a otros sistemas, base de datos, o procesos de cálculo costosos.
Por ejemplo, si queremos testear un sistema de pago a través de una pasarela bancaria:
class Transaction
def initialize(gateway)
@gateway = gateway
end
def process
process = @gateway.process_payment
if process[:status] == 'ok'
"Transaction OK"
else
raise TransactionError
end
end
end
No queremos contactar con la pasarela bancaria en cada ejecución del test ;) Creamos un stub que responda lo que necesitamos para nuestros tests:
describe Transaction do
it 'should return ok if successful' do
gateway = stub(:process_payment => {:status => 'ok'})
transaction = Transaction.new(gateway)
transaction.process.should == "Transaction OK"
end
it 'should raise TransactionError if transaction not ok' do
gateway = stub(:process_payment => {:status => 'ko'})
transaction = Transaction.new(gateway)
lambda{
transaction.process
}.should raise TransactionError
end
end
Lo importante es que en estos ejemplos estamos comprobando el estado final de los objetos que estamos testeando, sin importar ni preocuparnos de qué hacen o de si los elementos externos que intervienen en la clase testeada están implementados o no.
Mocks
Al igual que los stubs podemos programar mocks que devuelvan respuestas predeterminadas cuando reciban ciertos mensajes o llamadas de metodos. Los mocks ofrecen una funcionalidad adicional a la de los stubs, ya que nos permiten definir expectativas.
Por tanto podemos decir qué metodos deberían ser llamados durante la ejecución del test, con qué argumentos, cuantas veces, etc..
Primero un ejemplo muy simple y luego uno más real.
class Constructor
def initialize(logger)
logger.log("Constructor has created a new object")
end
end
No vamos a comprobar el estado en el que finalize el metodo initialize, sino que vamos a comprobar su comportamiento.
La clase Constructor la instanciamos pasandole como parametro un logger que es usado para crear un registro de la creación de la instancia.
Vamos a comprobar que cuando instanciamos un objeto de la clase Constructor, ellogger ejecuta su metodo "log()".
class MockObject
def initialize
@verified = false
end
def verify
raise "Expectation not satisfied" unless @verified
end
def log(message)
@verified == true if message
end
end
describe Constructor do
it 'should log a new construction' do
logger = MockObject.new
constructor = Constructor.new(logger)
logger.verify
end
end
MockObject es nuestro mock. Cuando es instanciado una variable de instancia (@verified) se inicializa con el valor false.
Una vez que el método 'log()' es invocado se cambia el valor de la variable por true. Al final del test comprobamos el mock con el metodo 'verify()' y en caso de no haber llamado al metodo 'log()' lanzará una excepción provocado el fallo del test.
Por tanto lo que estamos comprobando en este test es cómo se comporta.
En un ejemplo más real usando ActiveRecod y los métodos del framework Mocha:
class Comment < ActiveRecord::Base
belongs_to :post
after_create :notify_new_comment
private
def notify_new_comment
CommentMailer.deliver_new_comment(self)
end
end
Cuando creamos un comentario queremos que se envie un email al autor del post.
describe Comment do
it 'should send an email when comment is created' do
CommentMailer.expects(:deliver_new_comment)
Comment.create(:text => "text", :post_id => 1)
end
end
Hemos usado un partial Mock, usando las facilidades que nos da Mocha. En vez de crear un mock nuevo hemos añadido un metodo de mock a una clase existente (CommentMailer). Con el metodo "expects()" estamos indicando que CommentMailer ha de llamar al metodo "deliver_new_comment()" durante la ejecución del test. Al final del mismo se hará la comprobación y obtendremos una excepción si no se ha producido esta llamada.
El punto importante es que estamos usando los mocks para testear el comportamiento de un elemento (en este último caso la clase Comment) y no nos importa ni nos preocupamos de otros elementos externos como la clase CommentMailer, simplemente comprobamos que cuando creamos un comment se llama al metodo correspondiente de CommentMailer.
Referencias:
Mocks aren't stubs
Don't mock yourself out
The RSpec Book
XUnit Test Patterns de Gerard Meszaros
