Monday, 29 June 2009

Las DUnit y el test Programming

En este artículo profundizaré un poco más sobre los test unitarios y su funcionamiento. Como expliqué en su día (con el Test Driven Development), tenemos que realizar las pruebas unitarias de nuestros módulos y automatizarlas. Para éste post, crearé un ejemplo sencillo con una pequeña clase llamada TArithmetic que tiene unos cuantos métodos matemáticos, y luego lanzaré un pequeño test unitario para ver si el módulo está correctamente diseñado o no.

Cómo funciona el test unitario?
Las xUnit, crean un test por cada clase que tenga, por lo tanto testa los métodos de mi clase.

  • Ejemplo clase TArithmetic:
Con esta clase básica podremos ver como Delphi realiza las pruebas unitarias.

unit Arithmetic;

interface

type
    TArithmetic = class(TObject)
    private
        Fx: integer;
        Fy: integer;
        procedure Setx(const Value: integer);
        procedure Sety(const Value: integer);
    public
        property x: integer read Fx write Setx;
        property y: integer read Fy write Sety;
        function plus(): integer;
        function minus(): integer;
        function multiply(): integer;
        function divide(): integer;
        function floatDivision(): double;
        function sinus(): double;
        function cosinus(): double;
    end;

implementation

{ TArithmetic }

function TArithmetic.cosinus: double;
begin
    result := cos(self.Fx);
end;

function TArithmetic.divide: integer;
begin
    result := Self.Fx div Self.Fy;
end;

function TArithmetic.floatDivision: double;
begin
    result := Self.Fx / Self.Fy;
end;

function TArithmetic.minus: integer;
begin
    result := Self.Fx - Self.Fy;
end;

function TArithmetic.multiply: integer;
begin
    result := Self.Fx * Self.Fy;
end;

function TArithmetic.plus: integer;
begin
    result := Self.Fx + Self.Fy;
end;

procedure TArithmetic.Setx(const Value: integer);
begin
    Fx := Value;
end;

procedure TArithmetic.Sety(const Value: integer);
begin
    Fy := Value;
end;

function TArithmetic.sinus: double;
begin
    result := sin(self.Fx);
end;

end.

  • Creando el Test Project:
Una vez tengo creada mi Unit, voy a File -> New -> Other, y en el árbol de la derecha marco Unit Test.

Selecciono Test Project, y lo añado a mi grupo de proyectos:

Su configuración es bastante sencilla, primero solo tenemos que configurar la ruta donde está nuestro proyecto y seleccionar la interfaz GUI:

Una vez tenemos el Test Project creado, tenemos que añadir una Test Case. Seleccionamos el Proyecto del test que hemos añadido, y luego nos dirigimos a File -> New -> Other y seleccionamos "Test Case".

Seleccionamos los métodos que queremos para nuestro test y luego le damos un nombre y lo asociamos a nuestro proyecto:

Una vez el asistente ha acabado, nos genera una unit llamada TestArithmetic, con el siguiente contenido:

unit TestArithmetic;
{

Delphi DUnit Test Case
----------------------
This unit contains a skeleton test case class generated by the Test Case Wizard.
Modify the generated code to correctly setup and call the methods from the unit
being tested.

}

interface

uses
    TestFramework, Arithmetic;

type
    // Test methods for class TArithmetic

    TestTArithmetic = class(TTestCase)
        strict private
            FArithmetic: TArithmetic;
    public
        procedure SetUp; override;
        procedure TearDown; override;
    published
        procedure Testplus;
        procedure Testminus;
        procedure Testmultiply;
        procedure Testdivide;
        procedure TestfloatDivision;
        procedure Testsinus;
        procedure Testcosinus;
    end;

implementation

procedure TestTArithmetic.SetUp;
begin
    FArithmetic := TArithmetic.Create;
end;

procedure TestTArithmetic.TearDown;
begin
    FArithmetic.Free;
    FArithmetic := nil;
end;

procedure TestTArithmetic.Testplus;
var
    ReturnValue: Integer;
begin
    ReturnValue := FArithmetic.plus;
    // TODO: Validate method results
end;

procedure TestTArithmetic.Testminus;
var
    ReturnValue: Integer;
begin
    ReturnValue := FArithmetic.minus;
    // TODO: Validate method results
end;

procedure TestTArithmetic.Testmultiply;
var
    ReturnValue: Integer;
begin
    ReturnValue := FArithmetic.multiply;
    // TODO: Validate method results
end;

procedure TestTArithmetic.Testdivide;
var
    ReturnValue: Integer;
begin
    ReturnValue := FArithmetic.divide;
    // TODO: Validate method results
end;

procedure TestTArithmetic.TestfloatDivision;
var
    ReturnValue: Double;
begin
    ReturnValue := FArithmetic.floatDivision;
    // TODO: Validate method results
end;

procedure TestTArithmetic.Testsinus;
var
    ReturnValue: Double;
begin
    ReturnValue := FArithmetic.sinus;
    // TODO: Validate method results
end;

procedure TestTArithmetic.Testcosinus;
var
    ReturnValue: Double;
begin
    ReturnValue := FArithmetic.cosinus;
    // TODO: Validate method results
end;

initialization
    // Register any test cases with the test runner
    RegisterTest(TestTArithmetic.Suite);
end.

Como podéis ver, ha generado una llamada a cada uno de nuestros métodos y la asignación de variables para poder realizar el test. Si ahora iniciamos el proyecto y hacemos un run para pasar las pruebas unitarias sucede lo siguiente:

Como podéis ver, no ha pasado las pruebas porqué hay divisiones por 0. Entonces nuestra clase no está protegida contra este tipo de valores, y hay que protegerla.

Si suponemos que el valor del divisor puede ser 0, entonces tenemos que indicarle al test que posiblemente le llegará una excepción de este tipo. Para realizar esto, tenemos que hacer lo siguiente:

unit TestArithmetic;

uses
    SysUtils;

procedure TestTArithmetic.Testdivide;
var
    ReturnValue: Integer;
begin
    StartExpectingException(EDivByZero);
    ReturnValue := FArithmetic.divide;
    StopExpectingException();  
end;

procedure TestTArithmetic.TestfloatDivision;
var
    ReturnValue: Double;
begin
    StartExpectingException(EInvalidOp);
    ReturnValue := FArithmetic.floatDivision;
    StopExpectingException();
end;

y poner el tipo de excepción que esperamos que suceda al realizar la operación del método. Aunque nos aparezca el mensaje:

luego las pruebas unitarias pasan los diferentes test:

También podemos especificar las operaciones añadiendo valores al método y haciendo la comparación. Por ejemplo si en la operación suma sabemos que el resultado es un valor concreto lo podemos comparar con el valor devuelto del método y así podemos ver si el resultado es correcto o no. A modo de ejemplo tendríamos:

procedure TestTArithmetic.Testplus;
var
    ReturnValue: Integer;
begin
    FArithmetic.x := 10;
    FArithmetic.y := 10;
    ReturnValue := FArithmetic.plus;
    checkEquals(ReturnValue, 20);
end;


Si miramos la respuesta del test unitario obtenemos:

Tenemos un fallo, esperábamos un 21 (a modo de ejemplo), pero el método devolvió un 20. Con ésto nos podemos imaginar como podríamos testar los otros métodos. Después de montar todos los test, las pruebas unitarias tienen que pasar sin problemas. Luego mientras hacemos cambios en nuestro código tenemos que tener en cuenta que las pruebas unitarias tienen que pasar y debemos tener la mayoría de los casos pensados. Lo interesante del TDD, es que primero se genera el test y luego se crea la clase, de esta manera el desarrollo de la clase no pierde el rumbo y solo creamos aquello que necesitamos.

  • Cómo funciona el xtesting framework?

El programa lanza threats y los lanza en paralelo para ir más rápido. Los test han de ser repetibles, por lo tanto en cada método hay que crear el estado para el test, ejecutar el test y luego destruir el estado. Para cada método del test, se llama al SetUp y al TearDown. Cada vez que se hace una prueba, se llama a una nueva instáncia del Test/Case, por lo tanto todo es bastante independiente.
El TestSuite es agrupar un montón de TestCase. Registrando tu TestCase en una TestSuite nos permite hacer lo mismo. Lo que te propone el TestSuite es hacer un test por método, pero lo que te sugieren es hacer un test por expectación.

Los test, hay que automatizarlos. Por pequeño que sea el cambio, ejecuto el test. En los siguientes post, hablaré del Extreme Programming y de diversos sistemas para realizar pruebas unitarias y de integración. Existen muchas herramientas tales como FIT (Framework for integrated test), Mock Objects, etc. Y sobretodo tenemos que tener en cuenta los bad smells en nuestro código, como el DRY (Don't Repeat Yourself) y el YAGNI (You Ain't Gonna Need It) que hablaré un poco más de éstos en los siguientes artículos.

Espero que os sirva de ayuda!.

  • Enlaces de interés:
DUnit Framework.
Pruebas automatizadas con DUnit.

0 comments:

Post a Comment