2011-05-28 10 views
5

Necesito ayuda con algunos conceptos de TDD. Supongo que tengo el siguiente códigoForma correcta de los métodos TDD que llama a otros métodos

def execute(command) 
    case command 
    when "c" 
    create_new_character 
    when "i" 
    display_inventory 
    end 
end 

def create_new_character 
    # do stuff to create new character 
end 

def display_inventory 
    # do stuff to display inventory 
end 

Ahora no estoy seguro de para qué escribir las pruebas de mi unidad. Si escribo pruebas unitarias para el método execute ¿no cubre eso prácticamente mis pruebas para create_new_character y display_inventory? ¿O estoy probando las cosas equivocadas en ese punto? ¿Debería mi prueba para el método execute probar solo que la ejecución se pasa a los métodos correctos y detenerme allí? Entonces, ¿debería escribir más pruebas unitarias que prueben específicamente create_new_character y display_inventory?

Respuesta

6

Supongo que, dado que menciona TDD, el código en cuestión no existe en realidad. Si lo hace, entonces no está haciendo un verdadero TDD sino TAD (Prueba después del desarrollo), lo que naturalmente lleva a preguntas como esta. En TDD comenzamos con la prueba. Parece que estás construyendo algún tipo de menú o sistema de comando, así que lo usaré como ejemplo.

describe GameMenu do 
    it "Allows you to navigate to character creation" do 
    # Assuming character creation would require capturing additional 
    # information it violates SRP (Single Responsibility Principle) 
    # and belongs in a separate class so we'll mock it out. 
    character_creation = mock("character creation") 
    character_creation.should_receive(:execute) 

    # Using constructor injection to tell the code about the mock 
    menu = GameMenu.new(character_creation) 
    menu.execute("c") 
    end 
end 

Esta prueba conduciría a un código similar al siguiente (recuerda, solo código suficiente para que el paso de la prueba, no más)

class GameMenu 
    def initialize(character_creation_command) 
    @character_creation_command = character_creation_command 
    end 

    def execute(command) 
    @character_creation_command.execute 
    end 
end 

Ahora vamos a añadir la siguiente prueba.

it "Allows you to display character inventory" do 
    inventory_command = mock("inventory") 
    inventory_command.should_receive(:execute) 
    menu = GameMenu.new(nil, inventory_command) 
    menu.execute("i") 
end 

Al ejecutar esta prueba nos llevará a una implementación tales como:

class GameMenu 
    def initialize(character_creation_command, inventory_command) 
    @inventory_command = inventory_command 
    end 

    def execute(command) 
    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 
end 

Esta aplicación nos lleva a una pregunta acerca de nuestro código. ¿Qué debería hacer nuestro código cuando se ingresa un comando inválido? Una vez que decidamos la respuesta a esa pregunta, podríamos implementar otra prueba.

it "Raises an error when an invalid command is entered" do 
    menu = GameMenu.new(nil, nil) 
    lambda { menu.execute("invalid command") }.should raise_error(ArgumentError) 
end 

que impulsa a cabo un cambio rápido a la execute método

def execute(command) 
    unless ["c", "i"].include? command 
     raise ArgumentError("Invalid command '#{command}'") 
    end 

    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 

Ahora que hemos pasar las pruebas podemos utilizar el Extraer método refactorización para extraer la validación de la orden en una Intención Revelando el Método.

def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 

    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 

    def invalid?(command) 
    !["c", "i"].include? command 
    end 

Ahora, finalmente, llegamos al punto en el que podemos responder a su pregunta.Como el método invalid? se eliminó mediante la refabricación del código existente bajo prueba, no es necesario escribir una prueba unitaria para él, ya está cubierto y no es válido por sí mismo. Dado que los comandos de inventario y de caracteres no son probados por nuestra prueba existente, necesitarán ser impulsados ​​por prueba de forma independiente.

Tenga en cuenta que nuestro código podría ser mejor aún, mientras las pruebas están pasando, limpiemos un poco más. Los enunciados condicionales son un indicador de que estamos violando el OCP (Principio abierto cerrado) podemos usar el Reemplazar condicionalmente el polimorfismo refactorizando para eliminar la lógica condicional.

# Refactored to comply to the OCP. 
class GameMenu 
    def initialize(character_creation_command, inventory_command) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

    def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 
    @commands[command].execute 
    end 

    def invalid?(command) 
    [email protected]_key? command 
    end 
end 

Ahora hemos refactorizado la clase de tal manera que un comando adicional simplemente nos obliga a añadir una entrada adicional a los comandos de picadillo en lugar de cambiar nuestra lógica condicional, así como el método invalid?.

Todas las pruebas deben pasar y ya casi hemos completado nuestro trabajo. Una vez probamos conducir los comandos individuales se puede volver al método de inicialización y añadir algunos valores por defecto para los comandos de este modo:

def initialize(character_creation_command = CharacterCreation.new, 
       inventory_command = Inventory.new) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

La prueba final es:

describe GameMenu do 
    it "Allows you to navigate to character creation" do 
    character_creation = mock("character creation") 
    character_creation.should_receive(:execute) 
    menu = GameMenu.new(character_creation) 
    menu.execute("c") 
    end 

    it "Allows you to display character inventory" do 
    inventory_command = mock("inventory") 
    inventory_command.should_receive(:execute) 
    menu = GameMenu.new(nil, inventory_command) 
    menu.execute("i") 
    end 

    it "Raises an error when an invalid command is entered" do 
    menu = GameMenu.new(nil, nil) 
    lambda { menu.execute("invalid command") }.should raise_error(ArgumentError) 
    end 
end 

Y la final GameMenu ve como :

class GameMenu 
    def initialize(character_creation_command = CharacterCreation.new, 
       inventory_command = Inventory.new) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

    def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 
    @commands[command].execute 
    end 

    def invalid?(command) 
    [email protected]_key? command 
    end 
end 

Espero que ayude!

Brandon

+1

+1: Excelente ejemplo de TDD. – Johnsyweb

+0

gracias por la respuesta detallada. Me diste mucho para morder y pensar. Lo único que realmente me molesta de tu ejemplo es que el inicializador de GameMenu será muy largo después de agregar muchos comandos. Y probarlo será fácil equivocarse si tengo que hacer un seguimiento de que mi nuevo comando "mostrar mapa" dice 10 parámetros en la lista. ¿Alguna buena solución para esto? – Dty

+0

@Dty Absolutamente. Yo había considerado eso. Pensé que para este pequeño ejemplo no sería tan importante, pero confirmó que es/podría ser. Hay un par de formas en que puedes manejarlo. El primero que se me ocurre es agregar un register_menu_command que podría llamarse externamente para registrar los comandos. El segundo sería reemplazar esa lista de parámetros con _Builder Pattern_ y simplemente pasar un MenuBuilder que genere el hash. Puede configurar el constructor en su prueba. Probablemente preferiría la solución de construcción. – bcarlso

3

Considere refactorización para que el código que tiene la responsabilidad de comandos de análisis (execute en su caso) es independiente del código que implementa las acciones (es decir, create_new_character, display_inventory). Eso hace que sea fácil burlarse de las acciones y probar el comando de análisis de forma independiente. Usted quiere prueba independiente de las diferentes piezas.

+0

No estoy seguro de entender lo que quieres decir. Por ejemplo, ya siento que el análisis sintáctico de comandos y la ejecución de acciones son independientes. ¿Podrías mostrarme una muestra de código corto de lo que quieres decir? Quizás eso me ayude a entender. – Dty

0

me gustaría crear pruebas normales para create_new_character y display_inventory, y finalmente para probar execute, siendo sólo una función de contenedor, establezca expectativas para comprobar que el comando adecuada exploración se llama (y el resultado devuelto). Algo así:

def test_execute 
    commands = { 
    "c" => :create_new_character, 
    "i" => :display_inventory, 
    } 
    commands.each do |string, method| 
    instance.expects(method).with().returns(:mock_return) 
    assert_equal :mock_return, instance.execute(string) 
    end 
end 
Cuestiones relacionadas