TDD: Programación orientada a pruebas

[Fuente: http://net.tutsplus.com/tutorials/php/the-newbies-guide-to-test-driven-development/]

Probar el código es molesto, pero el impacto de no hacerlo puede crear molestias de magnitudes muy superiores!.  En este artículo, utilizaremos programación orientada a objetos (test-driven development) para programar y probar nuestro código más eficientemente.

Qué es el TDD o Programación Orientada a Pruebas (POP)?

Desde el principio de los tiempos, los programadores y los fallos han luchado los unos con los otros. Incluso los mejores programadores tienen fallos. Ningún código es 100% seguro. Es por ello que hacemos testing. Los programadores, al menos los sanos, pruebas su código ejecutando en máquinas de desarrollo par estar seguros que el código hace lo que se supone que debe hacer.


Programador que prueba sus programas.
Image courtesy of http://www.youthedesigner.com


Programador que no prueba sus programas
Image courtesy of http://www.internetannoyanceday.com

“La programación orientada a objetos es una técnica de programación que requiere que se programen a la vez el código y sus pruebas automatizadas. Esto asegura que pruebas tu código – y te permite reprobar tu código rápida y fácilmente, ya que está automatizado”

Cómo funciona?

El desarrollo orientada a pruebas , o TDD como lo llamaremos desde ahora, recuerda como a un corto ciclo de desarrollo iterativo que va de la siguientes pasos:

  1. Antes de escribir cualquier código, debes primero escribir un test automatizado para el código. Mientras escribir el código automatizado, debes tener en cuenta todas las posibles entradas de datos, errores y salidas. De esta forma, tu mente no estará confundida con código que ya ha sido escrito.
  2. La primera vez que ejecutas las pruebas automatizados, el test debe fallar – indicando que el código no esta listo aún.
  3. Después, puedes empezara programar. Ya que hay un test automático, tan pronto como el código falle, eso significa que aún no esta listo. El código puede ser arreglado hasta que pase todos los assertions.
  4. Una vez que el código pasa el test, puedes empezar a limpiarlo, utilizando refactoring. Mientras el código siga pasando los test, significa que funcione. No te tienes que preocupar sobre los cambios que introduzcan nuevos fallos.
  5. Comienza desde el principio con cualquier otro método o programa.

Ok , pero por qué esto es mejor que las pruebas normales?

Has pasado a proposito de probar un programa porque:

  • sientes que es una perdida de tiempo, ya que solo fue un cambio pequeño?
  • te da pereza probar todo otra vez?
  • no tienes tiempo suficiente para probar porque el gestor del proyecto lo quiere en producción ASAP?
  • te dijiste a ti mismo que ya lo harías mañana?
  • tenias que elegir entre pruebas a mano , o ver el último episodio de Barrio Sesamo?

La mayor parte del tiempo, nada sucede, y pones tu código en producción sin ningún problema. Pero algunas veces, después de que lo has puesto en producción, todo empieza a ir mal. Y estas acorralado arreglando multitud de errores en un barco que se hunde, con más errores apareciendo  a cada momento. Seguro que no quieres verte en esta situación.

TDD fue creado para que no tengamos excusas. Cuando un programa ha sido desarrollado utilizando TDD, te permite hacer cambios y probarlo rápidamente y eficientemente. Todo lo que necesitamos es ejecutar los tests automatizados y voila! Si pasa todos los test automatizados, entonces vamos bien – si no, entonces hemos roto algo con los ultimos cambios que hemos hecho. Conociendo qué parte del código ha fallado, también te permite fácilmente averiguar en qué parte del código está el fallo, haciendo el arreglo de errores mucho más fácil.

Lo he vendido . ¿Cómo lo hago?

Hay muchos frameworks de testing automático para PHP que podemos utilizar, Uno de los más ampliamente utilizados es PHPUnit.

PHPUnit puede integrarse fácilmente en nuestros proyectos, u otros proyecto que han sido construidos sobre otros frameworks PHP populares.

Para nuestros propositos, no necesitamos la multitud de funciones que ofrece PHPUnit. En vez de ello, optaremos por crear nuestros propios test utilizando un framework de testing mucho más simple, llamado SimpleTest.

En los siguiente pasos, asumiremos que estamos programando una aplicación de libro de invitados donde cualquier usuario puede añadir y ver entradas del libro.

Asumiremos que ya tenemos una maqueta de la aplicación, y que simplemente estamos haciendo una clase que contiene la lógica de la aplicación, que es la que lee y escribe en la BBDD. La parte de lectura es lo que vamos a desarrollar y probar.

Primer Paso: Configurar SimpleTest

Descarga  SimpleTest here, y extrae a la carpeta que quieras – preferiblemente a la carpeta donde vas a programar tu código , o en el include_path de PHP para más comodidad.Para esto tutorial , he configurado una carpeta como se ve aqui:

index.php ejecutará nuestro guestbook.php, e invoca el método view y mostrará las entradas. Dentro de la carpeta “classes” es donde pondremos la clase “guestbook.php”, y la carpeta test es donde colocaremos la libreria simpletest.

Segundo paso. Planeemos nuestro ataque


Image courtesy of http://connections.smsd.org/veterans

El segundo paso, que es de hecho el más importante, es empezar a crear tus pruebas. Para esto, realmente necesitas planificar y pensar qué hara tu función, qué posibles entradas recoge y que correspondientes salidas enviará. Este paso recuerda a jugar al ajedrez – necesitas saber todo sobre tu oponente (el programa), incluyendo todas sus debilidades (posibles errores) y fortalezas (lo que sucede si todo se ejecuta correctamente).

Asi que para nuestra aplicación guestbook , veamos cuál es el esquema:

View

  • Esta función no tendrá ninguna entrada ya que recupera todas las entradas de la BBDD y las envía de vuelta para que sean escritas en pantalla
  • Retornará un array de los registros del libro de invitados, conteniendo cada registro el nombre y su mensaje. Si no hay registros, entonces , debe devolver un array vacío.
  • Si hay registros , el array tendrá uno o más valores dentro.
  • Al mismo tiempo, el array tendrá un estructura específica , algo como esto:
  • Array (
    	[0] => Array (
    		['name'] = "Bob"
    		['message'] = "Hi, I'm Bob."
    	)
    	[1] => Array (
    		['name'] = "Tom"
    		['message'] = "Hi, I'm Tom."
    	)
    )
    
    
    
    

Tercer paso: Escribe un test!

Ahora, podemos escribir nuestro test. Empecemos creando un fichero llamado guestbook_test.php dentro de la carpeta test.

<?php
	require_once(dirname(__FILE__) . '/simpletest/autorun.php');
	require_once('../classes/guestbook.php');
	class TestGuestbook extends UnitTestCase {

		function testViewGuestbookWithEntries()
		{
			$guestbook = new Guestbook();
			// Add new records first
			$guestbook->add("Bob", "Hi, I'm Bob.");
			$guestbook->add("Tom", "Hi, I'm Tom.");
			$entries = $guestbook->viewAll();
			$count_is_greater_than_zero = (count($entries) > 0);
			$this->assertTrue($count_is_greater_than_zero);
			$this->assertIsA($entries, 'array');
			foreach($entries as $entry) {
				$this->assertIsA($entry, 'array');
				$this->assertTrue(isset($entry['name']));
				$this->assertTrue(isset($entry['message']));
			}
		}

		function testViewGuestbookWithNoEntries()
		{
			$guestbook = new Guestbook();
			$guestbook->deleteAll(); // Delete all the entries first so we know it's an empty table
			$entries = $guestbook->viewAll();
			$this->assertEqual($entries, array());
		}
	}
?>
Con las aserciones te aseguras que una cosa es lo que se supone que debe ser – basicamente, te asegura que lo que está retornando es lo que se espera que debe retornar. Por ejemplo, si un función se supone que retorna true si ha ido todo bien, entonces en nuestro test, deberiamos hacer un assert que chequea que el valor de retorno es igual a true.
Como puedes ver aqui, en el test estamos probando el listado de las entradas del guestbook con los que tiene y con los vacios.Chequeamos is estos dos escenarios pasan nuestros criterios desde el paso 2. Probablemente hayas visto que las dos funciones de test comienzan con la palabra ‘test’. Lo hacemos asi porque, cuando  SimpleTest ejecuta esta clase, busca todas las funciones que empiezan con este prefijo para ejecutarlas.
En nuestro clase de test, también hemos puesto algunos métodos assert, como assertTrue, assertIsA y asserEquals. La función assertTrue chequea si un valor es o no true.AssertIsA chequea si una variable es de un cierto tipo o clase. Hay otros método de assertion que proporciona SimpleTest:
assertTrue($x) Fail if $x is false
assertFalse($x) Fail if $x is true
assertNull($x) Fail if $x is set
assertNotNull($x) Fail if $x not set
assertIsA($x, $t) Fail if $x is not the class or type $t
assertNotA($x, $t) Fail if $x is of the class or type $t
assertEqual($x, $y) Fail if $x == $y is false
assertNotEqual($x, $y) Fail if $x == $y is true
assertWithinMargin($x, $y, $m) Fail if abs($x – $y) < $m is false
assertOutsideMargin($x, $y, $m) Fail if abs($x – $y) < $m is true
assertIdentical($x, $y) Fail if $x == $y is false or a type mismatch
assertNotIdentical($x, $y) Fail if $x == $y is true and types match
assertReference($x, $y) Fail unless $x and $y are the same variable
assertClone($x, $y) Fail unless $x and $y are identical copies
assertPattern($p, $x) Fail unless the regex $p matches $x
assertNoPattern($p, $x) Fail if the regex $p matches $x
expectError($x) Swallows any upcoming matching error
assert($e) Fail on failed expectation object $e

Assertion method list courtesy of http://www.simpletest.org/en/unit_test_documentation.html

Cuarto Paso: Tener errores para ganar

Una vez que has terminado de escribir el código del test, ejecutas el test. La primera vez que ejecutas el test, debe FALLAR. Si no lo hace , entonces es que el test no prueba nada.
Para ejecutar el test , simplemente ejecuta guestbook_test.php en tu navegador. Primero debes ver esto:

Esto sucede porque no hemos creado nuestra clase guestbook aún. Para hacerlo, crearemos guestbook.php dentro de la carpeta classes. La clase debe contener los métodos que estamos planeando utilizar, aunque de momento los ponemos vacios. Recuerda, estamos escribiendo los test antes que el código.

<?php
class Guestbook
{
	public function viewAll() {
	}

	public function add( $name, $message ) {
	}

	public function deleteAll() {
	}
}

Si ahora ejecutamos el test otra vez, debe aparecer algo esto:

Como ves ahora, nuestro test esta ahora funcionando aunque falle!. Esto significa que nuestro test está listo para ser “respondido”.

Quinto Paso:  Responde tu test escribiendo código

Ahora que tenemos un test automático funcionando, podemos empezar a escribir código. Abramos guestbook.php y empecemos a dar respuesta a nuestro test:
<?php
class Guestbook {

	var $entries = array(
		array (
			'name' => 'Kirk',
			'message' => 'Hi, I'm Kirk.'
		),
		array (
			'name' => 'Ted',
			'message' => 'Hi, I'm Ted.'
		)
	);

	// To save time, instead of creating and connecting to a database, we're going to
	// simulate a "database" by creating a static entries array here.
	// It will be like we have two entries in the table.

	function viewAll() {
		// Here, we should retrieve all the records from the database.
		// This is simulated by returning the $entries array
		return $this->entries;

	}
	function add($name, $message) {
		// Here, we simulate insertion into the database by adding a new record into the $_entries array
		// This is the correct way to do it: self::$_entries[] = array('name' => $name, 'message' => $message );
		$this->entries [] = array('notname' => $name, 'notmessage' => $message ); //oops, there's a bug here somewhere
                return true;

	}
	function deleteAll() {
		// We just set the $_entries array to simulate
		$this->entries = array();
		return true;
	}

}
?>

Una vez que ejecutamos nuestros test , debemos ver algo como esto:

La salida del test nos muestra en qué test y en que assertions nuestro código falla. Vemos claramente que en las lineas 16 y 17 está la assertion que arrojó el error. (Se han introducido fallos a proposito en las lineas 16 y 17).

El error estaba en que la key que metiamos no era la correcta , arreglamos esas lineas y ya obtenemos que todo va bien:

Sexto paso: Refactorizar y Refinar el código

Since the code we’re testing here is pretty simple, our testing and bug fixing didn’t last very long. But if this was a more complex application, you’d have to make multiple changes to your code, make it cleaner so it’s easier to maintain, and a lot of other things. The problem with this, though, is that change usually introduces additional bugs. This is where our automated test comes in—once we make changes, we can simply run the test again. If it still passes, then it means we didn’t break anything. If it fails, we know that we made a mistake. It also informs us where the problem is, and, hopefully, how we’ll be able to fix it.

Step 7. Aclarado y repetimos

Eventually, when your program requires new functionality, you’ll need to write new tests. That’s easy! Rinse and repeat the procedures from step two (since your SimpleTest files should already be set up), and start the cycle all over again.


Conclusion

There are a lot more in-depth test-driven development articles out there, and even more functionality to SimpleTest than what was displayed in this article—things like mock objects, stubs, which make it easier to create tests. If you’d like to read more, Wikipedia’s test-driven development page should set you on the right path. If you’re keen on using SimpleTest as your testing framework, browse the online documentation and be sure to review its other features.

Testing is an integral part of the development cycle, however, it’s too often the first thing to be cut when deadlines are imminent. Hopefully, after reading this article, you’ll appreciate how helpful it is to invest in test-driven development.

What are your thoughts on Test-Driven Development? Is it something you’re interested in implementing, or do you think it’s a waste of time? Let me know in the comments!