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: Excelente ejemplo de TDD. – Johnsyweb
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
@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