2011-06-16 12 views
13

Estoy tratando de entender el diseño basado en pruebas, específicamente RSpec. Pero estoy teniendo problemas con algunos de los ejemplos de The RSpec Book.RSpec: ¿cómo escribo una prueba que espera cierto resultado pero no me importa el método?

En el libro, ponemos a prueba para la salida en $ STDOUT como esto:

output = double('output') 
game = Game.new 
output.should_receive(:puts).with('Welcome to Codebreaker!') 
game.start() 

Bueno, que funciona en cierto modo. Pero, ¿por qué debería importarme si el objeto Game usa el método puts()? Si lo cambio a imprimir(), ¿realmente debería romper la prueba? Y, lo que es más importante, ¿no está esto en contra de uno de los principios de TDD, que debería probar lo que hace el método (el diseño) en lugar de cómo lo hace (la implementación)?

¿Hay alguna manera de que pueda escribir una prueba que simplemente pruebe qué termina en $ STDOUT, sin mirar qué método pone allí?

+1

Le estoy dando la señal a David para que se vincule con esa publicación sorprendente que afirma que los burlajes son malvados porque combinan el conocimiento de las partes internas de su código con la prueba. Eso va a tomar mucho en pensar, pero sospecho que hay mucha verdad en eso. Ese artículo hace la misma sugerencia que Andrew, y creo que allí es donde voy a hacer esto. Gracias a todos. – Andy

Respuesta

6

eche un vistazo a this post. Nick planteó preguntas sobre el mismo ejemplo, y una conversación muy interesante sigue en los comentarios. Espero que lo encuentres útil.

+0

Parece que Nick dejó el www. El enlace de arriba ya no funciona, pero esto hace: http://ngauthier.com/2010/12/everything-that-is-wrong-with-mocking.html – Tanner

8

Crea una clase de pantalla con la capacidad de escribir el estado.

Su código de producción hará uso de este objeto de visualización para que pueda cambiar cómo escribe en STDOUT. Habrá un solo lugar para esta lógica, mientras que las pruebas se basan en la abstracción.

Por ejemplo:

output = stub('output') 
game = Game.new(output) 
output.should_receive(:display).with('Welcome to Codebreaker!') 
game.start() 

Mientras que su código de producción tendrá algo como

class Output 
    def display(message) 
    # puts or whatever internally used here. You only need to change this here. 
    end 
end 

me gustaría hacer este paso de la prueba de la siguiente manera:

def start 
    @output.display('Welcome to Codebreaker!') 
end 

Aquí al código de producción no le importa cómo se muestra la salida. Podría ser cualquier forma de visualización ahora que la abstracción está en su lugar.

Toda la teoría anterior es independiente del idioma, y ​​funciona como un regalo. Todavía te burlas de cosas que no te pertenecen, como el código de un tercero, pero aún estás probando que estás realizando el trabajo a través de tu abstracción.

3

La forma en que lo probaría es con un objeto StringIO. Actúa como un archivo, pero no toca el sistema de archivos. Disculpas por la sintaxis Test :: Unit. No dude en editar la sintaxis de RSpec.

require "stringio" 

output_file = StringIO.new 
game = Game.new(output_file) 
game.start 
output_text = output_file.string 
expected_text = "Welcome to Codebreaker!" 
failure_message = "Doesn't include welcome message" 
assert output_text.include?(expected_text), failure_message 
4

Capture $stdout y pruebe eso en lugar de intentar simular los diversos métodos que pueden dar salida a stdout. Después de todo, quiere probar stdout y no algún método complicado para imitarlo.

expect { some_code }.to match_stdout('some string') 

que utiliza un Matcher personalizado (rspec 2)

RSpec::Matchers.define :match_stdout do |check| 

    @capture = nil 

    match do |block| 

    begin 
     stdout_saved = $stdout 
     $stdout  = StringIO.new 
     block.call 
    ensure 
     @capture  = $stdout 
     $stdout  = stdout_saved 
    end 

    @capture.string.match check 
    end 

    failure_message_for_should do 
    "expected to #{description}" 
    end 
    failure_message_for_should_not do 
    "expected not to #{description}" 
    end 
    description do 
    "match [#{check}] on stdout [#{@capture.string}]" 
    end 

end 

RSpec 3 has changed the Matcher API slightly.

failure_message_for_should es ahora failure_message
failure_message_for_should_not es ahora failure_message_when_negated
supports_block_expectations? se ha agregado a cometer errores más clara para los bloques.

Ver Charles' respuesta para la solución rspec3 completa.

+0

Esta es en realidad la mejor respuesta aquí. – Nowaker

+0

El inconveniente principal que he encontrado, que se produce principalmente en las pruebas de nivel superior, es cuando algún módulo/joya subyacente se ensucia con $ stdout para la impresión. Esto puede hacer que el emparejador sea inútil. – Matt

+0

Sí, pero es probable que la unidad pruebe las bibliotecas que escribe usted mismo, por lo que simplemente no se equivoca con $ stdout. :) – Nowaker

0

Una versión actualizada de respuesta de Matt como RSpec 3.0:

RSpec::Matchers.define :match_stdout do |check| 

    @capture = nil 

    match do |block| 

    begin 
     stdout_saved = $stdout 
     $stdout  = StringIO.new 
     block.call 
    ensure 
     @capture  = $stdout 
     $stdout  = stdout_saved 
    end 

    @capture.string.match check 
    end 

    failure_message do 
    "expected to #{description}" 
    end 
    failure_message_when_negated do 
    "expected not to #{description}" 
    end 
    description do 
    "match [#{check}] on stdout [#{@capture.string}]" 
    end 

    def supports_block_expectations? 
    true 
    end 
end 
1

me encontré con esta entrada del blog, que me ayudó a resolver este problema:

Mocking standard output in rspec.

Configura bloques de antes/después, y terminé haciéndolos dentro del rspec real, por alguna razón no pude hacer que funcionara desde mi spec_helper.rb como se recomienda.

Espero que ayude!

+0

Gracias, tampoco pude usar spec_helper.rb, pero esta es una solución clara y rápida. – rmcsharry

Cuestiones relacionadas