113
Test-Driven Development con Python y un ejemplo: la librería algoritmia Andrés Marzal Departamento de Lenguajes y Sistemas Informáticos Universitat Jaume I [email protected] viernes 9 de abril de 2010

TDD Con Python

Embed Size (px)

DESCRIPTION

tdd con python en pdf

Citation preview

Page 1: TDD Con Python

Test-Driven Developmentcon Python

y un ejemplo: la librería algoritmia

Andrés MarzalDepartamento de Lenguajes y Sistemas Informáticos

Universitat Jaume [email protected]

viernes 9 de abril de 2010

Page 2: TDD Con Python

viernes 9 de abril de 2010

Page 3: TDD Con Python

viernes 9 de abril de 2010

Page 4: TDD Con Python

“If you look at how most programmers spend their time, you’ll find that writing code is actually a small fraction. Some time is spent figuring out what ought to be going on, some time is spent designing, but most time is spent debugging. I’m sure every reader can remember long hours of debugging, often long into the night. Every programmer can tell a story of a bug that took a whole day (or more) to find. Fixing the bug is usually pretty quick, but finding it is a nightmare. And then when you do fix a bug, there’s always a chance that another one will appear and that you might not even notice it until much later. Then you spend ages finding that bug.

Martin Fowler

viernes 9 de abril de 2010

Page 5: TDD Con Python

• ¿Qué es TDD?

• ¿Con qué herramientas hacemos TDD con Python?

• Una demo

• unitest

• mockito

• coverage

• Algunas conclusiones

• Algoritmia

Guión

viernes 9 de abril de 2010

Page 6: TDD Con Python

¿Qué es TDD?

viernes 9 de abril de 2010

Page 7: TDD Con Python

Desarrollo Tradicional

viernes 9 de abril de 2010

Page 8: TDD Con Python

Desarrollo TradicionalUnified

ModelingLanguage(UML)

RationalUnifiedProcess(RUP)

CapabilityMaturityModel(CMM)Big Design

Up-Front(BDUF)

Desarrolloen Cascada

COCOMO

viernes 9 de abril de 2010

Page 9: TDD Con Python

Desarrollo Tradicional

viernes 9 de abril de 2010

Page 10: TDD Con Python

2001viernes 9 de abril de 2010

Page 11: TDD Con Python

Mike Beedle

Arie van Bennekum

Alistair Cockburn

Ward Cunningham

Martin FowlerJim Highsmith

Andy Hunt

Ron Jeffries

Jon Kern

Brian Marick

Robert C. MartinUncle Bob

Ken Schwaber

Jeff Sutherland

Dave Thomas

viernes 9 de abril de 2010

Page 12: TDD Con Python

XP Programming

Scrum

Kanbanviernes 9 de abril de 2010

Page 13: TDD Con Python

Manifesto for Agile Software Development

viernes 9 de abril de 2010

Page 14: TDD Con Python

Manifesto for Agile Software Development

Individuals and interactions over processes and tools

viernes 9 de abril de 2010

Page 15: TDD Con Python

Manifesto for Agile Software Development

Individuals and interactions over processes and tools

Working software over comprehensive documentation

viernes 9 de abril de 2010

Page 16: TDD Con Python

Manifesto for Agile Software Development

Individuals and interactions over processes and tools

Working software over comprehensive documentation

Customer collaboration over contract negotiation

viernes 9 de abril de 2010

Page 17: TDD Con Python

Manifesto for Agile Software Development

Individuals and interactions over processes and tools

Working software over comprehensive documentation

Customer collaboration over contract negotiation

Responding to change over following a plan

viernes 9 de abril de 2010

Page 18: TDD Con Python

Manifesto for Agile Software Development

Individuals and interactions over processes and tools

Working software over comprehensive documentation

Customer collaboration over contract negotiation

Responding to change over following a plan

That is, while there is value in the items on the right, we value the items on the left more.

viernes 9 de abril de 2010

Page 19: TDD Con Python

Manifesto for Agile Software Development

Individuals and interactions over processes and tools

Working software over comprehensive documentation

Customer collaboration over contract negotiation

Responding to change over following a plan

That is, while there is value in the items on the right, we value the items on the left more.

http://agilemanifesto.org

viernes 9 de abril de 2010

Page 20: TDD Con Python

Desarrollo ágil

• Programación por parejas

• Historias de usuario

• La metáfora del sistema

• Clientes in situ

• Unidades de prueba

• Desarrollo dirigido por pruebas

• Refactorización

• Diseño simple

• Iteraciones cortas

• Propiedad colectiva del código

• Reflexión continua

• Integración continua

viernes 9 de abril de 2010

Page 21: TDD Con Python

¿Qué es una prueba unitaria?

• Una unidad de prueba (unit test) es un trozo de código (típicamente un método) que invoca a otro trozo de código y comprueba después la corrección de algunas asunciones.

• Si las asunciones no eran válidas, se dice que la unidad de prueba ha fallado.

• Una unidad (unit) es un método o función.

viernes 9 de abril de 2010

Page 22: TDD Con Python

El sistema a prueba

• El sistema sometido a prueba (system under test) recibe el nombre de SUT.

• Hay quien llama CUT a la clase sometida a prueba.

viernes 9 de abril de 2010

Page 23: TDD Con Python

No confundir

• Prueba de aceptación: el programa supera una demanda del cliente

• Prueba de integración

• Prueba de regresión

• Prueba de prestaciones

• Prueba de carga

• Prueba de estrés

viernes 9 de abril de 2010

Page 24: TDD Con Python

Buenos “unit test”

• Automatizable y repetible

• Fácil de implementar

• Debe permanecer, una vez se ha escrito

• Cualquiera debe poder ejecutarlo

• Ejecutarlo debe ser sencillo

• Debe ser rápido

viernes 9 de abril de 2010

Page 25: TDD Con Python

Frameworks

• Los frameworks de unit testing permiten:

• Simplificar el diseño de pruebas unitarias

• Facilitar un entorno para la ejecución de las pruebas

• Proporcionar informes de los resultados

viernes 9 de abril de 2010

Page 26: TDD Con Python

Frameworks

• xUnit:

• JUnit: Java

• NUnit: C#

• PyUnit: Python

• Test::Unit: Ruby

• ...

viernes 9 de abril de 2010

Page 27: TDD Con Python

The various meanings of TDD

• 1) Test Driven Development: the idea of writing your code in a test first manner. You may already have an existing design in place.

• 2) Test Oriented Development: Having unit tests of integration tests in your code and write them out either before or after our write the code. Your code has lots of tests. You recognize the value of tests but you don't necessarily write them before you write the code. Design probably exists before you write the code

• 3) Test Driven Design(the eXtreme Programming way):  The idea of using a test-first approach as a fully fledged design technique, where tests are a bonus but the idea is to drive full design from little to no design whatsoever. You design as you go.

• 4) Test Driven Development and Design: using the test-first technique to drive new code and changes, while also allowing it to change and evolve your design as an added bonus. You may already have some design in place before starting to code, but it could very well change because the tests point out various smells.

http://weblogs.asp.net/rosherove/archive/2007/10/08/the-various-meanings-of-tdd.aspx

viernes 9 de abril de 2010

Page 28: TDD Con Python

viernes 9 de abril de 2010

Page 29: TDD Con Python

Desarrollo dirigido por las pruebas (TDD)

viernes 9 de abril de 2010

Page 30: TDD Con Python

More NUnit attributes 39

!"#$%&'()*' In NUnit, an ignored test is marked in yellow (the middle test), and the reason for not running the test is listed under the Tests Not Run tab on the right.

It can look like this:

[Test][Ignore("there is a problem with this test")] public void IsValidFileName_ValidFile_ReturnsTrue() { /// ... }

Running this test in the NUnit GUI will produce a result like that shown in figure 2.6.

What happens when you want to have tests running not by a namespace but by some other type of grouping? That’s where test cate-gories come in.

()+), -&.."/#'.&0.'12.&#3%"&0

You can set up your tests to run under specific test categories, such as slow tests and fast tests. You do this by using NUnit’s [Category] attri-bute:

[Test][Category("Fast Tests")] public void IsValidFileName_ValidFile_ReturnsTrue() { /// ... }

viernes 9 de abril de 2010

Page 31: TDD Con Python

¿Con qué herramientas hacemos TDD con

Python?

viernes 9 de abril de 2010

Page 32: TDD Con Python

Python

• Versión 3.1.2

• http://www.python.org

viernes 9 de abril de 2010

Page 33: TDD Con Python

unittest (PyUnit)

• Viene de serie con Python 3.1

viernes 9 de abril de 2010

Page 34: TDD Con Python

Eclipse

• Versión 3.5.2, Galileo

• http://eclipse.org

viernes 9 de abril de 2010

Page 35: TDD Con Python

• Versión 1.5.6

• http://pydev.org

• Instalación: http://pydev.org/updates

viernes 9 de abril de 2010

Page 36: TDD Con Python

Pydev

viernes 9 de abril de 2010

Page 37: TDD Con Python

Una demo

viernes 9 de abril de 2010

Page 38: TDD Con Python

Sudoku

• Queremos un programa que permita jugar a Sudokus

• En aras de la brevedad, Sudokus de 4x4

viernes 9 de abril de 2010

Page 39: TDD Con Python

Historias de usuario

1. Dada una lista con 4 filas de números, saber si describe un Sudoku de 4x4

2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y asteriscos en posición libre, obtener las lista que lo describe

3. Resolver automáticamente un Sudoku

4. Jugar partidas contra un jugador humano

viernes 9 de abril de 2010

Page 40: TDD Con Python

Creación de un proyecto Pydev

• File:: New:: Pydev project

viernes 9 de abril de 2010

Page 41: TDD Con Python

viernes 9 de abril de 2010

Page 42: TDD Con Python

Asociar una gramática Python e intérprete

• Menu contextual del proyecto

• Properties

• PyDev - Interpreter/Grammar

• Configurar un intérprete

viernes 9 de abril de 2010

Page 43: TDD Con Python

Fijar perspectiva Pydev

• Open Perspective :: Other... ::

viernes 9 de abril de 2010

Page 44: TDD Con Python

Empezamos

• Ye hemos creado el proyecto Sudoku

• Contendrá una carpeta src

• Dentro creamos un package para las pruebas (menú contextual en el proyecto: New :: Pydev package) y le llamamos test

viernes 9 de abril de 2010

Page 45: TDD Con Python

Historias de usuario

1. Dada una lista con 4 filas de números, saber si describe un Sudoku de 4x4

2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y asteriscos en posición libre, obtener las lista que lo describe

3. Resolver automáticamente un Sudoku

4. Jugar partidas contra un jugador humano

viernes 9 de abril de 2010

Page 46: TDD Con Python

Pruebas de aceptación1. Dada una lista con 4 filas de números, saber si describe un

Sudoku de 4x4

1.1. Rechaza una “no lista”

1.2. Rechaza lista que no es de 4x4

1.3. Rechaza lista que no contiene 4x4 enteros

1.4. Se suministra una lista de 4x4 con números menores que 0 o mayores que 4 y la rechaza

1.5. Rechaza lista con repetidos en fila

1.6. Rechaza lista con repetidos en columna

1.7. Rechaza lista con repetidos en región 2x2

viernes 9 de abril de 2010

Page 47: TDD Con Python

• Dentro creamos un package para las pruebas (menú contextual en el proyecto: New :: Pydev package) y le llamamos test

• En test creamos un módulo Python de tipo Unittest: test_SudokuValidator

• Y creamos el primer ¿método de test?

Manos a la obra

viernes 9 de abril de 2010

Page 48: TDD Con Python

'''Created on 06/04/2010

@author: amarzal'''import unittest

class Test(unittest.TestCase):

def testName(self): pass

if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()

viernes 9 de abril de 2010

Page 49: TDD Con Python

from unittest import TestCase

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku), "Acepta una no lista")

viernes 9 de abril de 2010

Page 50: TDD Con Python

from unittest import TestCase

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku), "Acepta una no lista")

TestCase: Clase cuyos métodos son

tests

viernes 9 de abril de 2010

Page 51: TDD Con Python

from unittest import TestCase

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku), "Acepta una no lista")

TestCase: Clase cuyos métodos son

tests

Método de test: empieza por test_

viernes 9 de abril de 2010

Page 52: TDD Con Python

from unittest import TestCase

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku), "Acepta una no lista")

TestCase: Clase cuyos métodos son

tests

Método de test: empieza por test_

SUT

viernes 9 de abril de 2010

Page 53: TDD Con Python

from unittest import TestCase

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku), "Acepta una no lista")

TestCase: Clase cuyos métodos son

tests

Método de test: empieza por test_

SUT

Aserto

viernes 9 de abril de 2010

Page 54: TDD Con Python

from unittest import TestCase

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku), "Acepta una no lista")

TestCase: Clase cuyos métodos son

tests

Método de test: empieza por test_

SUT

Aserto Mensaje de fallo

viernes 9 de abril de 2010

Page 55: TDD Con Python

¿Pasa la prueba?

• Evidentemente, no puede pasarlo. El SUT no existe aún.

• Pero veamos cómo falla:

• Menú contextual en test_SudokuValidator.py, Run As :: Python unit-test_SudokuValidator.py

viernes 9 de abril de 2010

Page 56: TDD Con Python

Finding files...['/Users/amarzal/Documents/workspace/Sudoku/src/tests/test_SudokuValidator.py'] ... doneImporting test modules ... done.

test_sudoku_isNotAList_isRejected (test_SudokuValidator.TestSudokuValidator) ... ERROR

======================================================================ERROR: test_sudoku_isNotAList_isRejected (test_SudokuValidator.TestSudokuValidator)----------------------------------------------------------------------Traceback (most recent call last): File "/Users/amarzal/Documents/workspace/Sudoku/src/tests/test_SudokuValidator.py", line 7, in test_sudoku_isNotAList_isRejected validator = SudokuValidator()NameError: global name 'SudokuValidator' is not defined

----------------------------------------------------------------------Ran 1 test in 0.001s

FAILED (errors=1)

viernes 9 de abril de 2010

Page 57: TDD Con Python

A por el SUT• En src creamos un package: sudoku

• En el package creamos un módulo: validator

• En ese módulo definimos la clase SudokuValidator con el método check

class SudokuValidator: def check(self, sudoku): if not isinstance(sudoku, list): return False return True

viernes 9 de abril de 2010

Page 58: TDD Con Python

from unittest import TestCasefrom sudoku.validator import SudokuValidator

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku), "Acepta una no lista")

viernes 9 de abril de 2010

Page 59: TDD Con Python

Finding files...['/Users/amarzal/Documents/workspace/Sudoku/src/tests/test_SudokuValidator.py'] ... doneImporting test modules ... done.

test_sudoku_isNotAList_isRejected (test_SudokuValidator.TestSudokuValidator) ... ok

----------------------------------------------------------------------Ran 1 test in 0.000s

OK

viernes 9 de abril de 2010

Page 60: TDD Con Python

Tests de aceptación1. Dada una lista con 4 filas de números, saber si describe un

Sudoku de 4x4

1.1. Rechaza una “no lista”

1.2. Rechaza lista que no es de 4x4

1.3. Rechaza lista que no contiene 4x4 enteros

1.4. Se suministra una lista de 4x4 con números menores que 0 o mayores que 4 y la rechaza

1.5. Rechaza lista con repetidos en fila

1.6. Rechaza lista con repetidos en columna

1.7. Rechaza lista con repetidos en región 2x2

viernes 9 de abril de 2010

Page 61: TDD Con Python

from unittest import TestCasefrom sudoku.validator import SudokuValidator

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku)) def test_sudoku_isNotA4x4List_isRejected(self): validator = SudokuValidator() sudoku = [] self.assertFalse(validator.check(sudoku))

Los mensajes pueden ser superfluos si se

nombran bien los test

viernes 9 de abril de 2010

Page 62: TDD Con Python

Finding files...['/Users/amarzal/Documents/workspace/Sudoku/src/tests/test_SudokuValidator.py'] ... doneImporting test modules ... done.

test_sudoku_isNotA4x4List_isRejected (test_SudokuValidator.TestSudokuValidator) ... FAILtest_sudoku_isNotAList_isRejected (test_SudokuValidator.TestSudokuValidator) ... ok

======================================================================FAIL: test_sudoku_isNotA4x4List_isRejected (test_SudokuValidator.TestSudokuValidator)----------------------------------------------------------------------Traceback (most recent call last): File "/Users/amarzal/Documents/workspace/Sudoku/src/tests/test_SudokuValidator.py", line 15, in test_sudoku_isNotA4x4List_isRejected self.assertFalse(validator.check(sudoku))AssertionError: True is not False

----------------------------------------------------------------------Ran 2 tests in 0.001s

FAILED (failures=1)

viernes 9 de abril de 2010

Page 63: TDD Con Python

class SudokuValidator: def check(self, sudoku): if not isinstance(sudoku, list): return False

if len(sudoku) != 4: return False for row in sudoku: if not isinstance(sudoku, list): return False if len(row) != 4: return False

return Trueviernes 9 de abril de 2010

Page 64: TDD Con Python

Finding files...['/Users/amarzal/Documents/workspace/Sudoku/src/tests/test_SudokuValidator.py'] ... doneImporting test modules ... done.

test_sudoku_isNotA4x4List_isRejected (test_SudokuValidator.TestSudokuValidator) ... oktest_sudoku_isNotAList_isRejected (test_SudokuValidator.TestSudokuValidator) ... ok

----------------------------------------------------------------------Ran 2 tests in 0.000s

OK

viernes 9 de abril de 2010

Page 65: TDD Con Python

from unittest import TestCasefrom sudoku.validator import SudokuValidator

class TestSudokuValidator(TestCase): def test_sudoku_isNotAList_isRejected(self): validator = SudokuValidator() sudoku = 0 self.assertFalse(validator.check(sudoku)) def test_sudoku_isNotA4x4List_isRejected(self): validator = SudokuValidator() sudoku = [] self.assertFalse(validator.check(sudoku))

viernes 9 de abril de 2010

Page 66: TDD Con Python

from unittest import TestCasefrom sudoku.validator import SudokuValidator

class TestSudokuValidator(TestCase):

def setUp(self): self.validator = SudokuValidator() def test_sudoku_isNotAList_isRejected(self): sudoku = 0 self.assertFalse(self.validator.check(sudoku)) def test_sudoku_isNotA4x4List_isRejected(self): sudoku = [] self.assertFalse(self.validator.check(sudoku))

viernes 9 de abril de 2010

Page 67: TDD Con Python

Tests de aceptación1. Dada una lista con 4 filas de números, saber si describe un

Sudoku de 4x4

1.1. Rechaza una “no lista”

1.2. Rechaza lista que no es de 4x4

1.3. Rechaza lista que no contiene 4x4 enteros

1.4. Se suministra una lista de 4x4 con números menores que 0 o mayores que 4 y la rechaza

1.5. Rechaza lista con repetidos en fila

1.6. Rechaza lista con repetidos en columna

1.7. Rechaza lista con repetidos en región 2x2

viernes 9 de abril de 2010

Page 68: TDD Con Python

class SudokuValidator: def check(self, sudoku): if not isinstance(sudoku, list): return False if len(sudoku) != 4: return False for row in sudoku: if not isinstance(sudoku, list): return False if len(row) != 4: return False for row in sudoku: for number in row: if not isinstance(number, int): return False for row in sudoku: for number in row: if not (0 <= number <= 4): return False

for i in range(4): numbers = set() for j in range(4): n = sudoku[i][j] if n != 0 and n in numbers: return False numbers.add(n) for j in range(4): numbers = set() for i in range(4): n = sudoku[i][j] if n != 0 and n in numbers: return False numbers.add(n) for region in (((0,0), (0,1), (1,0), (1,1)), ((0,2), (0,3), (1,2), (1,3)), ((2,0), (2,1), (3,0), (3,1)), ((2,2), (2,3), (3,2), (3,3))): numbers = set() for (i, j) in region: n = sudoku[i][j] if n != 0 and n in numbers: return False numbers.add(n) return True

viernes 9 de abril de 2010

Page 69: TDD Con Python

¡Hora de refactorizar!

• Pero ya no saltamos sin red:

podemos refactorizar con la tranquilidad de que los tests “nos vigilan”

viernes 9 de abril de 2010

Page 70: TDD Con Python

class SudokuValidator: def __init__(self): rows = tuple(tuple((i, j) for j in range(4)) for i in range(4)) columns = tuple(tuple((i, j) for i in range(4)) for j in range(4)) sectors = (((0,0), (0,1), (1,0), (1,1)), ((0,2), (0,3), (1,2), (1,3)), ((2,0), (2,1), (3,0), (3,1)), ((2,2), (2,3), (3,2), (3,3))) self.regions = rows + columns + sectors def check(self, sudoku): if not isinstance(sudoku, list) or len(sudoku) != 4: return False for row in sudoku: if not isinstance(sudoku, list) or len(row) != 4: return False for number in row: if not (isinstance(number, int) and 0 <= number <= 4): return False for region in self.regions: numbers = set() for (i, j) in region: n = sudoku[i][j] if n != 0 and n in numbers: return False numbers.add(n) return True

viernes 9 de abril de 2010

Page 71: TDD Con Python

Finding files...['/Users/amarzal/Documents/workspace/Sudoku/src/tests/test_SudokuValidator.py'] ... doneImporting test modules ... done.

test_sudoku_isNotA4x4List_isRejected (test_SudokuValidator.TestSudokuValidator) ... oktest_sudoku_isNotAList_isRejected (test_SudokuValidator.TestSudokuValidator) ... oktest_sudoku_withNotIntegers_isRejected (test_SudokuValidator.TestSudokuValidator) ... oktest_sudoku_withOutOfRangeNumbers_isRejected (test_SudokuValidator.TestSudokuValidator) ... oktest_sudoku_withRepeatedNumbersInColumn_isRejected (test_SudokuValidator.TestSudokuValidator) ... oktest_sudoku_withRepeatedNumbersInRow_isRejected (test_SudokuValidator.TestSudokuValidator) ... oktest_sudoku_withRepeatedNumbersInSector_isRejected (test_SudokuValidator.TestSudokuValidator) ... ok

----------------------------------------------------------------------Ran 7 tests in 0.001s

OK

viernes 9 de abril de 2010

Page 72: TDD Con Python

Un poco de “teoría”• Las pruebas deben ser

• Legibles

• Fácilmente ejecutables

• Rápidos

• Sin estado

• Las pruebas guían el diseño y evitan la sobreingeniería: YAGNI (You ain’t gonna need it)

• La refactorización es más segura: los cambios se prueban instantáneamente contra una batería de pruebas de aceptación

viernes 9 de abril de 2010

Page 73: TDD Con Python

Historias de usuario

1. Dada una lista con 4 filas de números, saber si describe un Sudoku de 4x4

2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y asteriscos en posición libre, obtener las lista que lo describe

3. Resolver automáticamente un Sudoku

4. Jugar partidas contra un jugador humano

viernes 9 de abril de 2010

Page 74: TDD Con Python

Pruebas de aceptación2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y

asteriscos en posición libre, obtener las lista que lo describe

2.1. Si se pasa un dato que no es una cadena, debe rechazarla

2.2. Si la cadena no tiene 4 líneas de 4 caracteres, debe rechazarla

2.3. Si tiene caracteres diferentes de ‘1’, ‘2’, ‘3’, ‘4’ o ‘*’ en las líneas, debe rechazarla

2.4. Si se le pasa una cadena correcta, debe proporcionar la lista de lista correspondiente

viernes 9 de abril de 2010

Page 75: TDD Con Python

import unittestfrom sudoku.parser import SudokuParser

class TestSudokuParser(unittest.TestCase): def setUp(self): self.parser = SudokuParser() def test_parses_withNonString_returnsParseException(self): unproper_sudoku = 0 self.assertRaises(TypeError, self.parser.parse, unproper_sudoku)

def test_parses_unproperSizeString_returnsParseException(self): unproper_sudoku="""***************""" self.assertRaises(ValueError, self.parser.parse, unproper_sudoku)

def test_parses_invalidChars_returnsParseException(self): unproper_sudoku="""***************=""" self.assertRaises(ValueError, self.parser.parse, unproper_sudoku)

def test_parses_validSudoku_returnsSudoku(self): sudoku="""1****2****3****4""" self.assertEquals(self.parser.parse(sudoku), [[1,0,0,0], [0,2,0,0], [0,0,3,0], [0,0,0,4]])

if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()

viernes 9 de abril de 2010

Page 76: TDD Con Python

Historias de usuario

1. Dada una lista con 4 filas de números, saber si describe un Sudoku de 4x4

2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y asteriscos en posición libre, obtener las lista que lo describe

3. Resolver automáticamente un Sudoku

4. Jugar partidas contra un jugador humano

viernes 9 de abril de 2010

Page 77: TDD Con Python

Historias de usuario

3. Resolver automáticamente un Sudoku

3.1. Dado un sudoku completo, devolverlo tal cual

3.2. Dado un sudoku incompleto, devolverlo resuelto

viernes 9 de abril de 2010

Page 78: TDD Con Python

import unittestfrom sudoku.solver import SudokuSolver

class TestSudokuSolver(unittest.TestCase):

def setUp(self): self.solver = SudokuSolver() self.sudoku = [[0,0,4,0],[1,0,0,0], [0,0,0,3], [0,1,0,0]] self.solution = [[[2, 3, 4, 1], [1, 4, 3, 2], [4, 2, 1, 3], [3, 1, 2, 4]]] def test_solver_withCompleteSudoku_returnSameSudoku(self): self.assertEquals(list(self.solver.solve(self.solution[0])), self.solution)

def test_solver_withValidSudoku_returnSolution(self): self.assertEquals(list(self.solver.solve(self.sudoku)), self.solution)

if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()

viernes 9 de abril de 2010

Page 79: TDD Con Python

from copy import deepcopy

class SudokuSolver: def __init__(self): self.rows = tuple(tuple((i, j) for j in range(4)) for i in range(4)) self.columns = tuple(tuple((i, j) for i in range(4)) for j in range(4)) sectors = (((0,0), (0,1), (1,0), (1,1)), ((0,2), (0,3), (1,2), (1,3)), ((2,0), (2,1), (3,0), (3,1)), ((2,2), (2,3), (3,2), (3,3))) self.sector = {} for sector in sectors: for (i, j) in sector: self.sector[i, j] = sector self.regions = self.rows + self.columns + sectors

def solve(self, sudoku): return self._backtrack(deepcopy(sudoku), 0, 0)

def _backtrack(self, sudoku, i, j): for n in self._options(sudoku, i, j): old_value, sudoku[i][j] = sudoku[i][j], n sudoku[i][j] = n if self._has_next(i, j): for solution in self._backtrack(sudoku, *self._next(i, j)): yield solution else: yield deepcopy(sudoku) sudoku[i][j] = old_value

viernes 9 de abril de 2010

Page 80: TDD Con Python

import unittestfrom sudoku.solver import SudokuSolver

class TestSudokuSolver(unittest.TestCase):

def setUp(self): self.solver = SudokuSolver() self.sudoku = [[0,0,4,0],[1,0,0,0], [0,0,0,3], [0,1,0,0]] self.solution = [[[2, 3, 4, 1], [1, 4, 3, 2], [4, 2, 1, 3], [3, 1, 2, 4]]] def test_solver_withCompleteSudoku_returnSameSudoku(self): self.assertEquals(list(self.solver.solve(self.solution[0])), self.solution)

def test_solver_withValidSudoku_returnSolution(self): self.assertEquals(list(self.solver.solve(self.sudoku)), self.solution)

def test_options_ofFreeCell_areEnumerated(self): options = set(self.solver._options(self.sudoku, 0, 0)) self.assertEquals(options, {2,3})

def test_options_ofCellWithNumber_isTheNumber(self): options = set(self.solver._options(self.sudoku, 0, 2)) self.assertEquals(options, {4})

def test_hasNext_beforeLast_returnTrue(self): self.assertTrue(self.solver._has_next(3, 2))

def test_hasNext_atLast_returnTrue(self): self.assertFalse(self.solver._has_next(3, 3))

def test_next_fromOrigin_iteratesAllCellIndices(self): (x, y) = (0, 0) for i in range(4): for j in range(4): self.assertEquals((x,y), (i,j)) if self.solver._has_next(x, y): (x, y) = self.solver._next(x, y)

if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()

viernes 9 de abril de 2010

Page 81: TDD Con Python

class SudokuSolver:

...

def _has_next(self, i, j): if i < 3: return True if j < 3: return True return False

def _next(self, i, j): if j < 3: return (i, j+1) if i < 3: return (i+1, 0) def _options(self, sudoku, i, j): if sudoku[i][j] != 0: yield sudoku[i][j] else: used = set(sudoku[x][y] for (x, y) in self.rows[i] + self.columns[j] \ + self.sector[i, j]) for n in set(range(1, 5)) - used: yield n

viernes 9 de abril de 2010

Page 82: TDD Con Python

Historias de usuario

1. Dada una lista con 4 filas de números, saber si describe un Sudoku de 4x4

2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y asteriscos en posición libre, obtener las lista que lo describe

3. Resolver automáticamente un Sudoku

4. Jugar partidas contra un jugador humano

viernes 9 de abril de 2010

Page 83: TDD Con Python

Nos faltas historias de usuario

• Ahora caemos en que hemos de preparar una visualización apropiada para el Sudoku

• ¿Cómo?

viernes 9 de abril de 2010

Page 84: TDD Con Python

Historias de usuario1. Dada una lista con 4 filas de números, saber si

describe un Sudoku de 4x4

2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y asteriscos en posición libre, obtener las lista que lo describe

3. Resolver automáticamente un Sudoku

4. Presentar gráficamente los Sudoku

5. Jugar partidas contra un jugador humano

viernes 9 de abril de 2010

Page 85: TDD Con Python

import unittestfrom sudoku.presenter import SudokuPresenter

class TestSudokuPresenter(unittest.TestCase):

def setUp(self): self.presenter = SudokuPresenter()

def test_show_validSudoku_returnValidString(self): sudoku = [[0, 0, 3, 0], [4, 0, 1, 0], [0, 1, 4, 3], [0, 0, 0, 0]] presentation = """ 1 2 3 4 +-+-+-+-+1 | | |3| | +-+-+-+-+2 |4| |1| | +-+-+-+-+3 | |1|4|3| +-+-+-+-+4 | | | | | +-+-+-+-+""" self.assertEquals(self.presenter.show(sudoku), presentation)

if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()

viernes 9 de abril de 2010

Page 86: TDD Con Python

class SudokuPresenter: def show(self, sudoku): s = [] s.append(" 1 2 3 4") for (i, row) in enumerate(sudoku): s.append(" +-+-+-+-+") s.append("{} |".format(i+1)) for number in row: r = " " if number == 0 else repr(number) s[-1] += r + "|" s.append(" +-+-+-+-+") return '\n'.join(s)

viernes 9 de abril de 2010

Page 87: TDD Con Python

Historias de usuario1. Dada una lista con 4 filas de números, saber si

describe un Sudoku de 4x4

2. Dada una cadena con 4 líneas de caracteres entre 1 y 4 y asteriscos en posición libre, obtener las lista que lo describe

3. Resolver automáticamente un Sudoku

4. Presentar gráficamente los Sudoku

5. Jugar partidas contra un jugador humano

viernes 9 de abril de 2010

Page 88: TDD Con Python

¿Cómo hacemos pruebas con un humano?

• Se usan impostores (dobles de prueba) para simular el acceso a recursos:

• de producción y/o que interactúan con el entorno

• demasiado lentos para las pruebas

• tan complejos que podrían fallar y llamarnos a engaño sobre el responsable del fallo

viernes 9 de abril de 2010

Page 89: TDD Con Python

Herramientas

• Mockito for Python

• Mockito es un framework para Java, con port para Python 3.1

• http://code.google.com/p/mockito/

• http://code.google.com/p/mockito/wiki/MockitoForPython

viernes 9 de abril de 2010

Page 90: TDD Con Python

Un jugador “impostable”

• Vamos a diseñar una clase que modela al jugador

• El jugador introducirá coordenadas de la casilla que quiere modificar y el número que quiere poner en esa casilla (0 será borrar)

viernes 9 de abril de 2010

Page 91: TDD Con Python

class SudokuPlayer(): def get_coordinates_and_number(self): while True: print("Play i j n:", end="") line = input().strip() words = line.split() if len(words) == 3: try: i = int(words[0]) j = int(words[1]) n = int(words[2]) if 1 <= i <= 4 and 1 <= j <= 4 and 0 <= n <= 9: return (i-1, j-1, n) else: raise ValueError() except ValueError: print("Invalid input")

Feo: imprime en pantalla, lee de teclado.Difícil de impostar: no hay costuras (seams)

viernes 9 de abril de 2010

Page 92: TDD Con Python

class SudokuPlayer(): def __init__(self, prompt=lambda: print("Play i j n:", end=""), notify_error = lambda: print("Invalid input"), get_input=input): self.prompt = prompt self.notify_error = notify_error self.get_input = get_input def get_coordinates_and_number(self): while True: self.prompt() line = self.get_input().strip() words = line.split() if len(words) == 3: try: i = int(words[0]) j = int(words[1]) n = int(words[2]) if 1 <= i <= 4 and 1 <= j <= 4 and 0 <= n <= 9: return (i-1, j-1, n) else: raise ValueError() except ValueError: self.notify_error()

Podemos impostar la propia I/Oviernes 9 de abril de 2010

Page 93: TDD Con Python

import unittestfrom sudoku.player import SudokuPlayerfrom io import StringIOfrom mockito import *

class TestSudokuPlayer(unittest.TestCase):

def test_getCoordinatesAndNumber_readsGoodUserValues_returnsProperValues(self): ioMock = Mock() #@UndefinedVariable when(ioMock).get_input().thenReturn("1 1 1") #@UndefinedVariable output = StringIO() player = SudokuPlayer(prompt=lambda: None, notify_error=lambda: None, get_input=ioMock.get_input) (i, j, n) = player.get_coordinates_and_number() verify(ioMock, times=1).get_input() #@UndefinedVariable self.assertEquals((i,j,n), (0,0,1)) output.close()

def test_getCoordinatesAndNumber_readsBadAndGoodUserValues_returnsProperValues(self): ioMock = Mock() #@UndefinedVariable when(ioMock).get_input().thenReturn("1 1 100").thenReturn("1 1 1") #@UndefinedVariable output = StringIO() player = SudokuPlayer(prompt=lambda: None, notify_error=lambda: None, get_input=ioMock.get_input) (i, j, n) = player.get_coordinates_and_number() verify(ioMock, times=2).get_input() #@UndefinedVariable self.assertEquals((i,j,n), (0, 0, 1)) output.close()

def test_getCoordinatesAndNumber_readsBadAndGoodUserValues_errorIsNotified(self): ioMock = Mock() #@UndefinedVariable when(ioMock).notify_error().thenReturn("ERROR") #@UndefinedVariable when(ioMock).get_input().thenReturn("1 1 100").thenReturn("1 1 1") #@UndefinedVariable output = StringIO() player = SudokuPlayer(prompt=lambda: None, notify_error=ioMock.notify_error, get_input=ioMock.get_input) (i, j, n) = player.get_coordinates_and_number() verify(ioMock, times=1).notify_error() #@UndefinedVariable output.close() if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()

viernes 9 de abril de 2010

Page 94: TDD Con Python

class TestSudokuPlayer(unittest.TestCase):

def test_getCoordinatesAndNumber_readsGoodUserValues_returnsProperValues(self): ioMock = Mock() #@UndefinedVariable when(ioMock).get_input().thenReturn("1 1 1") #@UndefinedVariable output = StringIO() player = SudokuPlayer(prompt=lambda: None, notify_error=lambda: None, get_input=ioMock.get_input) (i, j, n) = player.get_coordinates_and_number() verify(ioMock, times=1).get_input() #@UndefinedVariable self.assertEquals((i,j,n), (0,0,1)) output.close()

verificación

Fake/Stub

Mock

Fake/Stub

viernes 9 de abril de 2010

Page 95: TDD Con Python

Mocks aren’t Stubs (Martin Fowler)

• Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.

• Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).

• Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'.

• Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

viernes 9 de abril de 2010

Page 96: TDD Con Python

import unittestfrom sudoku.game import SudokuGamefrom sudoku.player import SudokuPlayerfrom mockito import *

class TestSudokuGame(unittest.TestCase):

def test_game_withInvalidSudokuString_raisesException(self): game = SudokuGame(sudoku_chooser=lambda x: "") self.assertRaises(ValueError, game.start, None)

def test_game_withIncompleteSudoku_raisesException(self): game = SudokuGame(sudoku_chooser=lambda x: "234\n341*\n214*\n*321") self.assertRaises(ValueError, game.start, None)

def test_game_withImpossibleSudoku_raisesException(self): game = SudokuGame(sudoku_chooser=lambda x: "2234\n341*\n214*\n*321") self.assertRaises(ValueError, game.start, None)

def test_game_withValidSudoku_plasyOK(self): player = Mock() #@UndefinedVariable when(player).get_coordinates_and_number().thenReturn((0,0,1)) \ .thenReturn((1,3,2)) \ .thenReturn((2,3,3)) \ .thenReturn((3,0,4))

game = SudokuGame(sudoku_chooser=lambda x: "*234\n341*\n214*\n*321") self.assertTrue(game.start(player))

if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()

viernes 9 de abril de 2010

Page 97: TDD Con Python

from sudoku.solver import SudokuSolverfrom sudoku.presenter import SudokuPresenterfrom sudoku.validator import SudokuValidatorfrom sudoku.parser import SudokuParserfrom sudoku.player import SudokuPlayer

from random import choicefrom copy import deepcopy

class SudokuGame: def __init__(self, sudoku_chooser=lambda sudokus: choice(sudokus)): self.parser = SudokuParser() self.validator = SudokuValidator() self.solver = SudokuSolver() self.presenter = SudokuPresenter() self.sudoku_chooser = sudoku_chooser self.sudokus = ["2143\n4**1\n1**4\n3412", "*234\n341*\n214*\n*321", "*23*\n1**4\n2**3\n*14*", "4**2\n*31*\n*42*\n3**1", "4*1*\n1*2*\n*4*1\n*1*2", "12**\n**21\n24**\n**42", "*13*\n****\n****\n3421", "*3*1\n**2*\n**3*\n*4*2", "****\n12**\n3***\n***1", "**3*\n****\n*2**\n*14*"] def start(self, player): sudoku_string = self.sudoku_chooser(self.sudokus) sudoku = self.parser.parse(sudoku_string) if not self.validator.check(sudoku): raise ValueError("Invalid Sudoku\n" + self.presenter.show(sudoku)) original_sudoku = deepcopy(sudoku) print(self.presenter.show(sudoku)) while not self.validator.complete(sudoku): (i, j, n) = player.get_coordinates_and_number() if sudoku[i][j] == 0: sudoku[i][j] = n if not self.validator.check(sudoku): sudoku[i][j] = 0 elif n == 0 and original_sudoku[i][j] == 0: sudoku[i][j] = 0 print(self.presenter.show(sudoku)) return True if __name__ == "__main__": game = SudokuGame() game.start(SudokuPlayer())

viernes 9 de abril de 2010

Page 98: TDD Con Python

from sudoku.solver import SudokuSolverfrom sudoku.presenter import SudokuPresenterfrom sudoku.validator import SudokuValidatorfrom sudoku.parser import SudokuParserfrom sudoku.player import SudokuPlayer

from random import choicefrom copy import deepcopy

class SudokuGame: def __init__(self, sudoku_chooser=lambda sudokus: choice(sudokus)): self.parser = SudokuParser() self.validator = SudokuValidator() self.solver = SudokuSolver() self.presenter = SudokuPresenter() self.sudoku_chooser = sudoku_chooser self.sudokus = ["2143\n4**1\n1**4\n3412", "*234\n341*\n214*\n*321", "*23*\n1**4\n2**3\n*14*", "4**2\n*31*\n*42*\n3**1", "4*1*\n1*2*\n*4*1\n*1*2", "12**\n**21\n24**\n**42", "*13*\n****\n****\n3421", "*3*1\n**2*\n**3*\n*4*2", "****\n12**\n3***\n***1", "**3*\n****\n*2**\n*14*"] def start(self, player): sudoku_string = self.sudoku_chooser(self.sudokus) sudoku = self.parser.parse(sudoku_string) if not self.validator.check(sudoku): raise ValueError("Invalid Sudoku\n" + self.presenter.show(sudoku)) original_sudoku = deepcopy(sudoku) print(self.presenter.show(sudoku)) while not self.validator.complete(sudoku): (i, j, n) = player.get_coordinates_and_number() if sudoku[i][j] == 0: sudoku[i][j] = n if not self.validator.check(sudoku): sudoku[i][j] = 0 elif n == 0 and original_sudoku[i][j] == 0: sudoku[i][j] = 0 print(self.presenter.show(sudoku)) return True if __name__ == "__main__": game = SudokuGame() game.start(SudokuPlayer())

viernes 9 de abril de 2010

Page 99: TDD Con Python

Pruebas de cobertura

• ¿Hemos puesto a prueba todas y cada una de las líneas de nuestro código?

• Muy importante en lenguajes dinámicos

viernes 9 de abril de 2010

Page 100: TDD Con Python

Herramientas

• coverage.py

• Versión 3.3.1

• Instalación: easy_install coverage

• http://nedbatchelder.com/code/coverage/

viernes 9 de abril de 2010

Page 101: TDD Con Python

import osimport unittestimport coverageimport importlibfrom unittest import TestResult

def find_test_paths(startDir="test"): result = [] directories = [startDir] while len(directories)>0: directory = directories.pop() for name in os.listdir(directory): fullpath = os.path.join(directory,name) if os.path.isfile(fullpath) and \ name.startswith("test"): result.append(fullpath) elif os.path.isdir(fullpath): directories.append(fullpath) return result

test_paths = find_test_paths()

cov = coverage.coverage()cov.start()

loaded = set()suite = unittest.TestSuite()for module in test_paths: mod = importlib.import_module(''.join(

module.split(".")[:-1]).replace("/", ".")) exec("import {}".format(mod.__name__)) for c in dir(mod): if c.startswith("Test"): fullname = mod.__name__ + "." + c testclass = eval(fullname) if testclass not in loaded: loaded.add(testclass) suite.addTest(unittest.TestLoader()\ .loadTestsFromTestCase(testclass))

result = TestResult()suite.run(result)print(result)cov.stop()cov.report()

viernes 9 de abril de 2010

Page 102: TDD Con Python

<unittest.TestResult run=52 errors=0 failures=0>Name Stmts Exec Cover Missing---------------------------------------------------------sudoku/__init__ 1 1 100% sudoku/game 35 30 85% 40-42, 47-48sudoku/parser 16 16 100% sudoku/player 20 20 100% sudoku/presenter 12 12 100% sudoku/solver 36 36 100% sudoku/validator 36 34 94% 17, 44test/__init__ 1 1 100% test/test_SudokuGame 21 20 95% 33test/test_SudokuParser 19 18 94% 38test/test_SudokuPlayer 34 33 97% 49test/test_SudokuPresenter 11 10 90% 27test/test_SudokuSolver 30 29 96% 41test/test_SudokuValidator 28 27 96% 39---------------------------------------------------------TOTAL 300 287 95%

viernes 9 de abril de 2010

Page 103: TDD Con Python

algoritmiaUna librería de estructuras de datos, algoritmos clásicos

y esquemas algorítmicos

MIT License

viernes 9 de abril de 2010

Page 104: TDD Con Python

Mercurial

• Versión 1.5

• http://mercurial.selenic.com

viernes 9 de abril de 2010

Page 105: TDD Con Python

HgEclipse

• Versión 1.5

• http://www.javaforge.com/project/HGE

• Instalación: http://hge.javaforge.com/hgeclipse

viernes 9 de abril de 2010

Page 106: TDD Con Python

viernes 9 de abril de 2010

Page 107: TDD Con Python

CodePlex

• Repositorio Mercurial con el proyecto algoritmia

• http://algoritmica.codeplex.com

viernes 9 de abril de 2010

Page 108: TDD Con Python

Clonar algoritmia

viernes 9 de abril de 2010

Page 109: TDD Con Python

viernes 9 de abril de 2010

Page 110: TDD Con Python

Importar algoritmia

viernes 9 de abril de 2010

Page 111: TDD Con Python

viernes 9 de abril de 2010

Page 112: TDD Con Python

Libros

M A N N I N G

the art of

with Examples in .NET

ROY O SHER OVE

ptg

From the Library of Lee Bogdanoff

this print for content only—size & color not accurate spine = 0.7904" 416 page count

BOOKS FOR PROFESSIONALS BY PROFESSIONALS®

Foundations of Agile Python DevelopmentDear Reader,

Python is your chosen development language. You love its power, clarity, and interactivity. But what is the best way to build and maintain Python applications? How can you blend its unique strengths with the best of agile methods to reach still higher levels of productivity and quality? And, at a practical level, where are the tools to automate it all? In this book, I give answers to these questions, backed up by a wealth of down-to-earth examples and working code.

The short development cycles of agile projects require far more automation than traditional processes. There’s simply no way to have a two-week release cycle if development involves a day of integration, a week of QA, and three days for production deployment. You must automate to succeed. But all too often, the best-known tools are language specific. For this reason, this book gives you a complete set of open source tools to turbocharge your Python projects, and shows you how to integrate them into a smoothly functioning whole.

Eclipse and Pydev make an excellent Python IDE. Python ships with an xUnit-based unit-testing framework. Nose is great for running tests, supplemented by PyFit for functional testing. Setuptools is your build harness and packaging mechanism, with functionality similar to Maven in Java. Subversion provides a place to store your code, and Buildbot is an ideal continuous integration server. What makes this book different from others is that I show you how to tie all of these pieces together into one continuous tool chain that builds your software from start to finish—fast!

While the information I present is steeped in the language of agile develop-ment, the details are not limited to that approach. This book is as much about release engineering in Python as it is about agile development.

Jeff Younker

US $42.99

Shelve in Python

User level: Intermediate–Advanced

YounkerFoundations of Agile Python Developm

ent

THE EXPERT’S VOICE® IN OPEN SOURCE

Foundations of

Agile Python Development

CYAN MAGENTA

YELLOW BLACK PANTONE 123 C

Jeff Younker

Companion eBook Available

THE APRESS ROADMAP

Beginning Python:From Novice to Professional

Foundations of PythonNetwork Programming

Foundations of Agile Python Development

Dive into Python

www.apress.comSOURCE CODE ONLINE

Companion eBook

See last page for details

on $10 eBook version

ISBN-13: 978-1-59059-981-5ISBN-10: 1-59059-981-0

9 781590 599815

54299

Python, agile project methods, and a comprehensive open source tool chain!

viernes 9 de abril de 2010

Page 113: TDD Con Python

Code and have fun!¡Gracias por vuestra atención!

viernes 9 de abril de 2010