There is a discussion in the spanish TDD group about whether it is good or bad, when you have a stub object, to return a different value depending on the input arguments of the method. I don't think it's the right thing to do because, normally, when a stub object cares about the input arguments in its method calls it's because it should not be a stub object, it should be a mock object. Let me explain it with an example:
The payment gateway is a stub that returns true when user_has_funds is called with an input argument of 50.
If we look carefully at the test, what we are really doing is testing more than one behavior. First, we're asserting that a user can buy stuff if she has sufficient funds. Second, we are expecting that the payment gateway is called with the right amount.
So, if it is an expectation, why don't we use a mock instead of a stub? We're mixing the concepts of mock and stub and, as a consequence, we're testing more than one thing.We're breaking the single responsibility principle.
Steve Freeman and Nat Pryce, authors of the great Growing objects oriented software, guided by tests, recommend to follow a simple rule of thumb, "specify exactly what you want to happen and no more". If we listen to them (and we should!), those are the resulting tests:
The first test specifies the relationship between the order and the payment gateway (The message protocol). It uses a mock object to replace the gateway payment object and it expects a call at user_has_funds method with the order amount as an input argument. We want to know that the payment gateway is going to be called with the right amount, so we are interested in its input argument (which is right when mocking). The second one specifies the behavior of the order when the user has funds, and I don't really care about how the order interacts with the payment gateway. I just want to know that the user has sufficient funds. That's why, in this case, we use a stub.
What I like about the new tests is that, if we change the order behavior by adding some taxes to the product price, the second test is not going to break (we still confirm the order if the payment gateway says that the user has funds, nothing has changed in that specification). Only the first one would be red (and for the right cause, we've changed exactly that specification).
What do you think?
PS: I know that, in RSpec:mocks, stub() is an alias for mock() and I don't like it very much :P
Suscribirse a:
Enviar comentarios (Atom)
Si, tienes razón, esta claro que en el primer test se están probando dos características diferentes, no fue muy buen ejemplo :P.
ResponderEliminarMi única duda es si se podía considerar una recomendación general. Pero en realidad después de pensarlo tampoco se me ocurre ningún ejemplo donde pueda tener sentido decidir que se devuelve en función de lo que se reciba. Todos los ejemplos que se ocurren al final son malos olores de diseño.
PD: que bonito es el jodio rspec, le perdonamos lo de los stubs y mocks xd.
According to Meszaros:
ResponderEliminar- test stub:
- injects indirect inputs into SUT
- ignores indirect outputs from SUT
- mock:
- optionally injects indirect inputs into SUT
- verifies indirect outputs from SUT through behaviour expectations
So far the theory.
In your 1st example:
product.stub(:price).and_return(50) - stub, no messages from SUT intercepted
payment_gateway.stub(:user_has_funds).with(50).and_return(true) - stub as far the return value is concerned. But you verify the behaviour (SUT -> payment_gateway message: 'user_has_funds', so this should be mock or spy)
In the 2nd example stubs are stubs and mocks are mocks.
Yeah! Thanks Marcin, that's what I wanted to say :D
ResponderEliminar