Ir al contenido principal

Prueba de máquinas de estados planas y jerárquicas mediante casos de prueba

El artículo explora una serie de estrategias, como punto de partida, para probar software que representa máquinas de estados tanto tradicionales (o planas) como de estados anidados (o jerárquicas). Estas últimas también conocidas como Statecharts [5] o máquinas UML.

Dichas estrategias se aplican bajo conceptos como el desacople de módulos, el uso de stub, spy y mock [1][6], los casos de prueba unitarios y sus fases, entre otros, los cuales permiten aplicar de manera inmediata el desarrollo de software dirigido por pruebas (TDD), en este caso para las máquinas de estados, con los beneficios que ello implica. Aunque lamentablemente poco explorado para la aplicación en software dirigido por eventos (reactivo).

Cada una de las estrategias expuestas se respalda con ejemplos de código fuente, escritos en lenguaje C para el framework de prueba Unity y Cmock, aunque pueden extrapolarse fácilmente a otros similares. 

Finalmente, el presente artículo muestra una estrategia para probar máquinas de estados anidados, resuelta por el framework RKH, el cual permite representar máquinas de estados y objetos activos concurrentes en lenguaje C/C++. 

Indirectamente el artículo provee de conceptos de programación que fomentan la calidad de nuestro código fuente.

Introducción

Una máquina de estados (SM) se compone de una estructura y sus acciones/comportamiento. Donde la estructura se constituye por estados, eventos y transiciones.

De esta manera, el software que representa una SM implica dos grandes cuestiones, la representación de su estructura que respeta la semántica del modelo (Mealy, Moore, Statechart u otra) y la representación de sus acciones. Ambas cuestiones, deben probarse para verificar el correcto comportamiento de la SM bajo prueba (SMUT), ya sea en conjunto o de manera independiente.

A si mismo, para probar una SM debemos tener en cuenta que su modelo de ejecución es discreto, es decir, sólo entra en ejecución cuando recibe una entrada o evento a la vez, esto último se conoce como principio RTC (Run To Completion). Durante su ejecución procesa el evento despachado, que implica transitar por su estructura de acuerdo con la semántica, ejecutando ciertas acciones de manera ordenada.

Por ejemplo del diagrama de la figura F1, estando en el estado s1 y recibiendo el evento evA se transita al estado s2, ejecutando las acciones xS1, effectTrn y nS2, en dicho orden.

F1 - Transición de estados

De acuerdo a este simple ejemplo, ¿cómo realizaríamos una prueba para determinar que la SMUT realiza la transición de estados esperada, es decir cuando reciba la señal evA en el estado s1, transite al estado s2 ejecutando ordenadamente las acciones xS1, effectTrn y nS2?. 

En principio descomponemos la prueba en partes bien marcadas:
  • las precondiciones como el estado origen s1 y el resultado esperado de la prueba, en este caso el estado destino s2 y la lista ordenada de acciones a ejecutar xS1, effectTrn y nS2, 
  • la excitación de la SM, y por supuesto, 
  • la verificación de la misma. 
Básicamente, esto implica que antes de realizar la prueba establecemos las precondiciones, en este caso el estado de la SM bajo prueba y los resultados esperados, posteriormente la ejercitamos, excitándola con la señal evA, luego verificamos que los resultados obtenidos sean los esperados. Este procedimiento está de acuerdo con la estructura de cuatro fases de una prueba, según los patrones de prueba xUnit:
  1. Fase de establecimiento (setup): establece las precondiciones a la prueba.
  2. Fase de ejercitación (exercise): hace algo contra el sistema.
  3. Fase de verificación (verify): verifica el resultado esperado.
  4. Fase de limpieza (cleanup): luego de la prueba vuelve el sistema bajo prueba a su estado inicial.

Probar la estructura de una máquina de estados tradicional

El objetivo es verificar la tabla de transición de estados (estructura) de una máquina de estados (SM) plana, es decir, sin anidamiento de estados. La estrategia consiste simplemente en realizar un caso de prueba para cada estado de la SM bajo prueba, estimulándola en el estado en cuestión con los eventos de su alfabeto de entrada, para luego verificar que el estado destino de la transición y su acción asociada, es decir los efectos del procesamiento del evento, sean los esperados, de acuerdo con su diagrama de estados (o tabla de transición de estados). 

Para demostrar la aplicación de las estrategias propuestas, se utiliza un simple ejemplo, la SM CommentInC, representada por diagrama de estados de la figura F2, cuyo propósito consiste en eliminar los comentarios en C de un archivo, basado en el enunciado del ejercicio 1-23 del libro “El lenguaje de programación C” de K&R.

    F2 - Diagrama de estados de CommentInC

    La figura F3 muestra un fragmento de la tabla de transición de estados correspondiente a CommnetInC, donde cada celda (intersecciones fila/columna) contiene el siguiente estado si ocurre el evento y la acción asociada a esta transición de estados.

    State/Event
    evAny(c)
    evApost
    ...
    Idle
    printChar(c)/Idle
    printChar(‘\’’)/InApost
    ...
    PossComm
    printSlashAndNextChar()/Idle
    printSlashAndApos()/Idle
    ...
    ...
    ...
    ...
    ...
    F3 - Tabla de transición de estados de CommentInC

    Especificación e implementación

    Por razones de simplicidad la implementación adoptada para resolver CommentInC es de las más simples y legibles, de la cual sólo se muestran sus detalles más relevantes. Donde el archivo commentInC.h provee la especificación de CommentInC mientras que commentInC.c su implementación.

    Desacoplando las acciones

    Es importante aclarar que para lograr la estrategia de prueba propuesta, es fundamental desacoplar las acciones de la estructura de la máquina de estados, concentrándolas en funciones contenidas en un archivo específico, en este caso se definen en commentInCAct.h y se implementan en el archivo commentInCAct.c. El objetivo del desacoplamiento es independizar la prueba de la SM de las acciones específicas de la misma, y así probar la estructura de la SM de manera unitaria. Esto permite cambiar la implementación de las acciones para controlarlas desde la prueba, sin cambio alguno en la estructura que representa la SM.

    Mock de las acciones

    Esta estrategia implementa las acciones como un objeto simulado o mock, el cual permite determinar desde la prueba no sólo las llamadas a las funciones (acciones) sino también el orden de ejecución de estas. En este ejemplo, el mock se genera a partir del archivo commentInCAct.h, utilizando Cmock.

    Los listados L1, L2 muestran, a modo de referencia, parte de los archivos commentInC.h, commentInCAct.h y commentInC.c respectivamente. Estos están de acuerdo con el correspondiente diagrama de estados de CommentInc.

    /** 
     * \file CommentInC.h 
     */
    #ifdef __COMMENTINC_H__
    #define __COMMENTINC_H__
    
    /* States */
    enum
    {
        Idle, PossComm, InComm, PossOut, 
        InApost, EscApost, InQuote, EscQuote 
    };
    
    /* Event signals */
    enum
    {
        evAny, evSlash, evAster, evApost, 
        evBSlash, evQuot
    };
    
    typedef struct Event Event;
    struct Event
    {
        int signal;
        char inputChar;
    };
    
    void CommentInC_init(void);
    int CommentInC_dispatch(Event *event);
    
    void CommentInC_setState(int currState);
    int CommentInC_getState(void);
    
    #endif
    

    L1 - Fragmento del archivo de especificación de CommentInC, commentInC.h

    /** 
     *    \file CommentInCAct.h 
     */
    #ifdef __COMMENTINCACT_H__
    #define __COMMENTINCACT_H__
    
    #include "CommentInC.h"
    
    /* Effect actions */
    void CommentInC_printChar(char c);
    void CommentInC_space(void);
    ...
    #endif
    
    L2 - Fragmento del archivo de especificación de las acciones de CommentInC, commentInCAct.h

    /** 
     * \file CommentInC.c 
     */
    ...
    #include “CommentInC.h”
    #include “CommentInCAct.h”
    
    static int state;
    ...
    int 
    CommentInC_dispatch(Event *event)
    {
        switch (state)
        {
            case Idle:
                switch (event->signal)
                {
                    case evAny:
                        CommentInC_printchar(event->inputChar);
                        break;
                    case evApost:
                        CommentInC_printchar('\'');
                        state = InApost;
                        break;
                    case evQuot:
                        CommentInC_printchar('"');
                        state = InQuote;
                        break;
                    ...
                }
                break;
            case PossComm:
              ...
        }
        return state;
    }
    
    L3 - Fragmento del archivo de implementación de CommentInC, commentInC.c

    Donde:
    • Es importante resaltar que, en general, los archivos incluídos en el módulo bajo prueba, en este caso CommentInC.c, a excepción de aquel que provee su propia especificación CommentInC.h, no sólo indican las dependencias sino también el código que debe desacoplarse para lograr una correcta prueba unitaria. Siguiendo esta regla, el módulo bajo prueba no debe utilizar explícitamente especificaciones que provengan de archivos que no hayan sido incluídos explícitamente, porque de lo contrario la dependencia es confusa y compleja de desacoplar.
    • La función CommentInC_dispatch() despacha un evento a la SM,  la cual devuelve el siguiente estado de la transición. Desde esta se invocan las acciones.
    • A diferencia del diagrama de estados, el nombre de las funciones que implementan las acciones, posee el prefijo “CommentInC_”, indicando que pertenecen al módulo CommentInC, siendo esto sólo una cuestión estilística.
    • El evento, representado por el tipo Event, se constituye por la señal que efectivamente es parte del alfabeto de entrada (evAny, evSlash, etc) de la SM y su parámetro, que transporta información asociada con la señal, en este caso el caracter leído. Aunque esto último sólo se utiliza por el evento evAny que identifica un todos los caracteres distintos a ‘/’, ‘*’, ‘\’’, ‘\\’ y ‘”’.
    • Si bien las funciones CommentInC_setState() y CommentInC_getState() pertenecen a la especificación de CommentInC, no se utilizan fuera de sus pruebas. Por dicha razón podrían ocultarse mediante un spy de CommetInC. Este concepto se aplica más adelante en la sección Alternativa 2: utilizando un spy/stub de las acciones.

    Desarrollo guiado por pruebas

    Esta y las siguientes estrategias del artículo permiten aplicar la metodología del desarrollo guiado por pruebas o simplemente TDD, donde la SMUT se construye o modifica a partir de sus pruebas. Para mayor información consultar las referencias.

    Prueba

    En base a la estrategia propuesta, y a modo demostrativo, se bosqueja un fragmento del archivo test_CommentInC.c donde se implementan los casos de prueba de CommentInC, escritos de acuerdo con Unity y Cmock. Por razones de simplicidad el listado L4 muestra únicamente la prueba del estado Idle y del estado por defecto luego de la inicialización, junto con ciertos detalles relevantes para su implementación. De acuerdo con el uso tradicional de Unity, restan los archivos test_CommentInC_runner.c y all_test.c.


    /** 
     * \file test_CommentInC.c 
     */
    ...
    #include "unity_fixture.h"
    #include "CommentInC.h"
    #include "Mock_CommentInCAct.h"
    ...
    TEST_GROUP(Structure);
    ...
    static void
    setProfile(int currState, int nextState, int signal, char evParam)
    {
        CommentInC_setState(currState);
        event.signal = signal;
        event.inputChar = evParam;
        expectedNextState = nextState;
    }
    
    TEST_SETUP(Structure)
    {
        Mock_CommentInCAct_Init();
        CommentInC_init();
    }
    
    TEST_TEAR_DOWN(Structure)
    {
        Mock_CommentInCAct_Verify();
        Mock_CommentInCAct_Destroy();
    }
    
    TEST(Structure, DefaultStateAfterInit)
    {
        TEST_ASSERT_EQUAL(Idle, CommentInC_getState());
    }
    
    TEST(Structure, stateTransitionTableForIdle)
    {
        setProfile(Idle, Idle, evAny, 'x');
        CommentInC_printChar_Expect('x');
        state = CommentInC_dispatch(&event);
        TEST_ASSERT_EQUAL(expectedNextState, state);
    
        setProfile(Idle, InApost, evApost, '\'');
        CommentInC_printChar_Expect('\'');
        state = CommentInC_dispatch(&event);
        TEST_ASSERT_EQUAL(expectedNextState, state);
        
        ...
    }
    
    L4 - Fragmento del archivo de prueba de CommentInC, test_CommentInC.c

    Donde:

    • Las pruebas están escritas basadas en el framework Unity con el módulo fixture, para lograr una mejor legibilidad, ya que se asemeja al framework CPPUTest, por lo cual se incluye el archivo unity_fixture.h. Este además permite administrar los grupos de prueba desde la línea de comandos.
    • Como bien se dijo anteriormente, en general, los archivos incluídos en el módulo bajo prueba no sólo indican las dependencias sino también el código que debe desacoplarse para lograr una correcta prueba unitaria. Por tal motivo, y de acuerdo con las dependencias de CommentInC.c, se aisla la implementación de CommentInCAct.h, mediante su correspondiente mock.
    • Se incluye el archivo de especificación de la SMUT CommentInC.h, y el archivo de acciones simuladas, Mock_CommentInC.h, generado automáticamente por Cmock a partir del archivo CommentInCAct.h.
    • Cada caso de prueba comienza con la inicialización de la SM, invocada desde TEST_SETUP() del grupo Structure, el cual ha sido instanciado mediante TEST_GROUP().
    • El primer caso de prueba, DefaultStateAfterInit, consiste en verificar el estado por defecto (o inicial) luego de iniciar la SM, en el ejemplo estado Idle. En términos de UML o Statechart es la transición emergente de un pseudoestado Initial, o bien transición por defecto de la SM.
    • El caso de prueba stateTransitionTableForIdle del grupo Structure, estimula la SM CommentInC en el estado Idle, con todos los eventos de su alfabeto de entrada para luego verificar que el estado destino de la transición y su acción asociada sean las esperadas, de acuerdo con su diagrama de estados (o tabla de transición de estados). El alfabeto de entrada también incluye aquellos eventos que no son disparadores en el estado bajo prueba, por lo tanto esta prueba podría dividirse en dos partes, aquella que recibe los disparadores del estado (la cual denominamos "sunny day") y otra que recibe el resto de los eventos que no son parte de sus disparadores ("rainy day").
    • La función de ayuda setProfile() establece el estado actual de la SM de acuerdo con el estado bajo prueba, el estado destino esperado, y el evento de entrada.
    • El mock de las acciones permite, antes de realizar el ensayo, es decir, estimular la SM, esperar el orden correcto de las llamadas a las acciones, esto se realiza mediante las llamadas a CommentInC_<action>_Expect(). En este caso son generadas por Cmock a partir del archivo commentInCAct.h. Esto se verifica al finalizar cada ensayo, mediante  Mock_CommentInCAct_Verify(), invocada desde TEST_TEAR_DOWN().

    Probar el comportamiento de una máquina de estados plana

    Alternativa 1: utilizando un mock de las acciones

    El objetivo es probar, de manera simple, el comportamiento de la SM por medio de un mock del módulo de acciones commentInCAct, de manera similar a lo propuesto por el apartado Probar la estructura de una máquina de estados plana. En este caso, estimulando la SM con diversos patrones de entrada, los cuales constituyen los casos de prueba, por ejemplo, el patrón “int i; /*.*!*/”, donde cada caracter (encerrado entre comillas) representa un evento de entrada a la SM. De esta manera, el caso de prueba determina la salida de la SM y verifica así su comportamiento. Por ejemplo, para el  patrón de entrada “int i; /*.*!*/” la salida esperada es “int i; ”, donde efectivamente se eliminan los comentarios en C. En esta estrategia la salida de la SM se verifica indirectamente mediante las llamadas a las acciones que realiza cada despacho de eventos.

    Prueba

    A continuación, y a modo demostrativo, se bosqueja un fragmento del archivo test_CommentInC.c donde se implementan los casos de prueba de CommentInC, en este caso aquellos relacionados con el comportamiento de la SM, basado en su diagrama de estados. Por razones de simplicidad el listado L5 muestra únicamente el caso de prueba CommentWithAsterics, en el cual se prueba el comportamiento de CommentInC para el patrón de entrada “int i; /*.*!*/”, donde el comentario incluye caracteres ‘*’.  Asimismo se incluyen ciertos detalles relevantes para su implementación. De acuerdo con el uso tradicional de Unity, restan los archivos test_CommentInCBehaviour_runner.c y all_test.c.


    /**
     * \file test_CommentInC.c 
     */
    ...
    #include "unity_fixture.h"
    #include "Mock_CommentInCAct.h"
    ...
    TEST_SETUP(Behavior)
    {
        Mock_CommentInCAct_Init();
        CommentInC_init();
    }
    
    TEST_TEAR_DOWN(Behavior)
    {
        Mock_CommentInCAct_Verify();
        Mock_CommentInCAct_Destroy();
    }
    
    TEST(Behavior, CommentWithAsterics)
    {
        Event inEvents[] = {{evAny, 'i'}, 
                            {evAny, 'n'}, 
                            {evAny, 't'}, 
                            {evAny, ' '}, 
                            {evAny, 'i'}, 
                            {evAny, ';'}, 
                            {evAny, ' '}, 
                            {evAny, '/'}, 
                            {evAny, '*'}, 
                            {evAny, '.'}, 
                            {evAny, '*'}, 
                            {evAny, '!'}, 
                            {evAny, '*'}, 
                            {evAny, '/'}, 
                            {evAny, 'n'}};
        Event *pInEvent;
    
        CommentInC_printChar_Expect('i');
        CommentInC_printChar_Expect('n');
        CommentInC_printChar_Expect('t');
        CommentInC_printChar_Expect(' ');
        CommentInC_printChar_Expect('i');
        CommentInC_printChar_Expect(';');
        CommentInC_printSpace_Expect();
    
        for (pInEvent = inEvents; ...; ++pInEvent)
        {
            CommentInC_dispatch(&(*pInEvent));
        }
    }
    
    L5 - Fragmento del archivo de prueba de CommentInC, test_CommentInC.c


    Donde:

    Más patrones de entrada

    1. int i; /*.*!*/
    2. /* Hello! */
    3. // Hello!
    4. /**/
    5. "/* Hello!\"
    6. /* Hello*! */
    7. ‘/’
    8. ‘\'

    Alternativa 2: utilizando un spy/stub de las acciones

    Al igual que la alternativa 1, el objetivo es probar, de manera simple, el comportamiento de la SM, pero esta vez utilizando un spy/stub del módulo de acciones commentInCAct, en lugar de un mock. La estrategia en cuestión utiliza el stub para resolver fácilmente la realimentación que se necesita para determinar cuál es el resultado de la prueba en cuestión. Al igual que en las estrategias anteriores, se necesita desacoplar las acciones de la estructura de la SM. 

    De la misma manera que la alternativa 1, la prueba se realiza estimulando la SM con diversos patrones de entrada, los cuales constituyen los casos de prueba, por ejemplo, el patrón “int i; /*.*!*/”, para luego verificar si la salida es la esperada.

    Prueba

    De manera similar a la alternativa 1, se bosqueja un fragmento del archivo test_CommentInC.c, listado L6, que incluye únicamente el caso de prueba CommentWithAsterics.


    /**
     * \file test_CommentInC.c 
     */
    ...
    #include "unity_fixture.h"
    #include "CommentInCActSpy.h"
    ...
    TEST_SETUP(Behavior)
    {
        CommentInCActSpy_init();
        CommentInC_init();
    }
    
    TEST_TEAR_DOWN(Behavior)
    {
    }
    
    TEST(Behavior, CommentWithAsterics)
    {
        const char *expectedOutput = "int i; ";
           Event inEvents[] = {{evAny, 'i'}, 
                               {evAny, 'n'}, 
                               {evAny, 't'}, 
                               {evAny, ' '}, 
                               {evAny, 'i'}, 
                               {evAny, ';'}, 
                               {evAny, ' '}, 
                               {evAny, '/'}, 
                               {evAny, '*'}, 
                               {evAny, '.'}, 
                               {evAny, '*'}, 
                               {evAny, '!'}, 
                               {evAny, '*'}, 
                               {evAny, '/'}, 
                               {evAny, 'n'}};
        Event *pInEvent;
    
        for (pInEvent = inEvents; ...; ++pInEvent)
        {
            CommentInC_dispatch(&(*pInEvent));
        }
    
        TEST_ASSERT_EQUAL_STRING(expectedOutput, CommentInCSpy_getBuffer()); 
    }
    
    L6 - Fragmento del archivo de prueba de CommentInC, test_CommentInC.c
    Donde:
    • Cada caso de prueba comienza y termina con TEST_SETUP() y TEST_TEAR_DOWN() respectivamente.
    • Para lograr la estrategia aplicada en el caso de prueba CommentWithAsterics, el stub de las acciones agrega funciones específicas para determinar el comportamiento de la SM, estas se nombran con el prefijo “CommentInCSpy_”. Estas las utiliza únicamente la prueba, es decir, son transparentes a la implementación de CommentInC.
    • La función CommentInCSpy_getBuffer() permite obtener el resultado del procesamiento de CommentInC al patrón de entrada en cuestión, para luego compararlo con la salida esperada en expectedOutput. Esta función podríamos llamarla “espía”.
    • La implementación de las acciones es tal que permita obtener fácilmente el resultado total o parcial del procesamiento de CommentInC a los eventos de entrada, esto se explica más adelante.
    • Siguiendo la estructura del ensayo CommentWithAsterics las pruebas restantes necesitarán una función de ayuda común, que permita ingresar el patrón de entrada y la salida esperada.

    Stub de las acciones 

    Como bien se dijo anteriormente, el stub provee una implementación de las acciones de CommentInC de forma tal que pueda verificarse fácilmente su comportamiento, para lo cual las acciones no imprimen los caracteres sino más bien los almacenen durante toda la prueba. Luego, terminado el ensayo, la prueba consulta los caracteres almacenados para determinar si CommentInC responde como se espera.

    A continuación, y a modo demostrativo, el listado L7 muestra un fragmento de la implementación del stub de las acciones, archivo commentInCActStub.c.

    /** 
     * \file CommentInCActSpy.c 
     */
    ...
    static char buffer[MAX_BUFFER_SIZE];
    static char pBuffer;
    
    void
    CommentInC_printChar(char c)
    {
        if (...) /* is there room? */ 
        {
            *pBuffer++ = c;
        }
        else
        {
            /* assertion() */
        }
    }
    ...
    /* Spy functions */
    void
    CommentInCSpy_init(void)
    {
        pBuffer = buffer;
    }
    
    char *
    CommentInCSpy_getBuffer(void)
    {
        return buffer;
    }
    
    L7 - Fragmento de la implementación del stub de acciones de CommentInC, commentInCActStub.c

    La estructura del archivo commentInCActSpy.h, mostrado en el listado L8, incluído en la prueba, permite que las funciones espía sean transparentes al módulo commentInCAct.

    /** 
     * \file CommentInCActSpy.h 
     */
    #ifdef __COMMENTINCACTSPY_H__
    #define __COMMENTINCACTSPY_H__
    
    #include "CommentIncAct.h"
    
    char *CommentInCSpy_getBuffer(void);
    void CommentInCSpy_init(void);
    
    #endif
    
    L8 - Fragmento de la especificación del stub de acciones de CommentInC, commentInCActStub.h

    Assertions en código de producción

    Es probable que durante el resto de las pruebas de CommentInC la función CommentInC_printChar() devuelva el resultado si pudo o no imprimir el caracter. También podríamos obviar esto último y utilizar una assertion para manejar esta condición tal como se muestra en L7.

    Alternativa 3: probando las acciones independientemente de la SM

    Si la SM es lo suficientemente compleja, los métodos anteriores pueden no ser apropiados, en cuyo caso las acciones podrían probarse de manera independiente a la SM, es decir, fuera del contexto de esta última, en cuyo caso las pruebas no utilizarían la función de despacho de eventos CommentInC_dispatch().

    Probar estructura de un Statechart (SM jerárquica)

    Una manera particular para probar Statecharts lo explora el framework RKH, que permite representar Statecharts y objetos activos en C/C++. En ese sentido RKH utiliza varias estrategias para verificar que la representación de las SM es la esperada. Por ejemplo, para corroborar el correcto funcionamiento de todos los tipos de transiciones en una SM jerárquica, RKH utiliza el siguiente diagrama de estados, del cual genera una serie de casos de prueba.




    F4 - Diagrama de estados para prueba de transiciones en RKH

    Por ejemplo para probar la transición de un estado simple a uno compuesto, donde el estado fuente tiene mayor jerarquía que el estado destino, RKH aplica el caso de prueba SimpleToCompositeFromHighToLowLevel del grupo TrnWoutUnitrazer, el cual utiliza el concepto de las estrategias anteriores.


    TEST_SETUP(trnWoutUnitrazer)
    {
        Mock_smTestAct_Init();
        sm_ignore();
    }
    
    TEST_TEAR_DOWN(trnWoutUnitrazer)
    {
        Mock_smTestAct_Verify();
        Mock_smTestAct_Destroy();
    }
    
    TEST(trnWoutUnitrazer, simpleToCompositeFromHighToLowLevel)
    {
        expectedState = RKH_STATE_CAST(&s2211);
    
        smTest_init_Expect(RKH_CAST(SmTest, smTest));
        smTest_xS0_Expect(RKH_CAST(SmTest, smTest));
        smTest_tr22_Expect(RKH_CAST(SmTest, smTest), &evD);
        smTest_nS2_Expect(RKH_CAST(SmTest, smTest));
        smTest_nS22_Expect(RKH_CAST(SmTest, smTest));
        smTest_iS22_Expect(RKH_CAST(SmTest, smTest));
        smTest_nS221_Expect(RKH_CAST(SmTest, smTest));
        smTest_iS221_Expect(RKH_CAST(SmTest, smTest));
        smTest_nS2211_Expect(RKH_CAST(SmTest, smTest));
        setProfileWoutUnitrazer(smTest,
                                RKH_STATE_CAST(&s0),
                                RKH_STATE_CAST(&s0),
                                expectedState,
                                INIT_STATE_MACHINE);
    
        result = rkh_sm_dispatch((RKH_SM_T *)smTest, &evD);
    
        TEST_ASSERT_TRUE(expectedState == getState(smTest));
        TEST_ASSERT_EQUAL(RKH_EVT_PROC, result);
    }
    
    L9 - Fragmento del caso de prueba simpleToCompositeFromHighToLowLevel
    Donde:
    • A diferencia del diagrama de estados SmTest, la implementación agrega a las acciones el prefijo “smTest_”, indicando que pertenecen al módulo smTest.
    • smTest_<action>_Expect() pertenecen al mock de las acciones de la SM
    • smTest_tr<x>() son las acciones de transición
    • smTest_n<state>() son las acciones de entrada al estado
    • smTest_x<state>() para las acciones de salida del estado
    • smTest_i<composite_state>() son las acciones por defecto de los estados compuestos
    • La función setProfileWoutUnitrazer() inicializa la SM, establece el estado actual y el estado fuente de la prueba, como así también el estado destino esperado
    • La función rkh_sm_dispatch() despacha un evento a la SM en cuestión 
    • Finalmente se verifica que: 
      • el estado destino sea el esperado, 
      • el evento despachado haya sido procesado 
      • y por último que se hayan llamado a las acciones esperadas (inicial, transición, entrada y salida) en el orden preestablecido, de acuerdo con el diagrama de estados de SmTest.

    Más casos de prueba

    El fragmento L10 del archivo test_smTransitionWoutUnitrazer_runner.c muestra algunos casos de prueba que ensaya RKH para probar las transiciones en un Statechart.

    TEST_GROUP_RUNNER(trnWoutUnitrazer)
    {
        RUN_TEST_CASE(trnWoutUnitrazer, firstStateAfterInit);
        RUN_TEST_CASE(trnWoutUnitrazer, simpleToSimpleAtEqualLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, simpleToSimpleFromHighToLowLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, simpleToSimpleFromLowToHighLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, simpleToCompositeAtEqualLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, simpleToCompositeFromHighToLowLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, simpleToCompositeFromLowToHighLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, compositeToSimpleAtEqualLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, compositeToSimpleFromHighToLowLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, compositeToSimpleFromLowToHighLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, compositeToCompositeAtEqualLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, compositeToCompositeFromHighToLowLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, compositeToCompositeFromLowToHighLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, loopSimpleStateOnTop);
        RUN_TEST_CASE(trnWoutUnitrazer, loopNestedSimpleState);
        RUN_TEST_CASE(trnWoutUnitrazer, loopCompositeStateOnTop);
        RUN_TEST_CASE(trnWoutUnitrazer, loopNestedCompositeState);
        RUN_TEST_CASE(trnWoutUnitrazer, internalInSimpleState);
        RUN_TEST_CASE(trnWoutUnitrazer, internalInCompositeState);
        RUN_TEST_CASE(trnWoutUnitrazer, fails_EventNotFound);
        RUN_TEST_CASE(trnWoutUnitrazer, fails_GuardFalseOnInternalTrn);
        RUN_TEST_CASE(trnWoutUnitrazer, fails_GuardFalseOnExternalTrn);
        RUN_TEST_CASE(trnWoutUnitrazer, fails_ExceededHierarchicalLevel);
        RUN_TEST_CASE(trnWoutUnitrazer, multipleEnabledTrn_FiringFirstTrueGuard);
        RUN_TEST_CASE(trnWoutUnitrazer, multipleEnabledTrn_FiringFirstEmptyGuard);
        RUN_TEST_CASE(trnWoutUnitrazer, defaultTrnWithAssociatedEffect);
        RUN_TEST_CASE(trnWoutUnitrazer, generatedCompletionEventBySimpleState);
        RUN_TEST_CASE(trnWoutUnitrazer, generatedCompletionEventByFinalState);
        RUN_TEST_CASE(trnWoutUnitrazer, syncDispatchingToStateMachine);
    }
    
    L10 - Casos de prueba de transciones

    A su vez, RKH aplica otra estrategia más precisa y detallada, mediante su módulo nativo Tracer, que permite almacenar diversos rastros durante la ejecución de una SM. Estos contienen información relevante para determinar en tiempo de ejecución:
    • Los segmentos de una transición compuesta
    • El estado y pseudoestado destino de una transición
    • Las acciones ejecutadas
    • La lista de estados de salida y entrada de una transición
    • El estado inicial
    • Resultado de una transición condicionada
    • Evento no reconocido
    • Excepciones y errores
    • Entre otros
    RKH permite establecer las precondiciones de una prueba, instruyendo que rastros espera obtener y en qué orden, durante el ejercicio de la misma. También permite ignorar rastros particulares y argumentos de rastros, para flexibilizar las pruebas.
    El listado L11 muestra el mismo caso de prueba que la estrategia anterior pero esta vez utilizando el módulo Tracer. Se ejemplo para probar la transición de un estado simple a uno compuesto, donde el estado fuente tiene mayor jerarquía que el estado destino, RKH aplica el caso de prueba SimpleToCompositeFromHighToLowLevel del grupo Transition.

    TEST(transition, simpleToCompositeFromHighToLowLevel)
    {
        UtrzProcessOut *p;
        const RKH_ST_T *targetStates[] = 
        {
            RKH_STATE_CAST(&s22), RKH_STATE_CAST(0)
        };
        const RKH_ST_T *entryStates[] = 
        {
            RKH_STATE_CAST(&s2), RKH_STATE_CAST(&s22), 
            RKH_STATE_CAST(&s221), RKH_STATE_CAST(&s2211), 
            RKH_STATE_CAST(0)
        };
        const RKH_ST_T *exitStates[] = 
        {
            RKH_STATE_CAST(&s0), RKH_STATE_CAST(0)
        };
    
        smTest_xS0_Expect(RKH_CAST(SmTest, smTest));
        smTest_tr22_Expect(RKH_CAST(SmTest, smTest), &evD);
        smTest_nS2_Expect(RKH_CAST(SmTest, smTest));
        smTest_nS22_Expect(RKH_CAST(SmTest, smTest));
        smTest_iS22_Expect(RKH_CAST(SmTest, smTest));
        smTest_nS221_Expect(RKH_CAST(SmTest, smTest));
        smTest_iS221_Expect(RKH_CAST(SmTest, smTest));
        smTest_nS2211_Expect(RKH_CAST(SmTest, smTest));
    
        smTest_init_Expect(RKH_CAST(SmTest, smTest));
        setProfile(smTest, 
                   RKH_STATE_CAST(&s0), 
                   RKH_STATE_CAST(&s0), 
                   targetStates, entryStates, exitStates, 
                   RKH_STATE_CAST(&s2211), 1, 
                   TRN_NOT_INTERNAL, INIT_STATE_MACHINE, 
                   &evD, RKH_STATE_CAST(&s0));
    
        rkh_sm_dispatch((RKH_SM_T *)smTest, &evD);
    
        p = unitrazer_getLastOut();
        TEST_ASSERT_EQUAL(UT_PROC_SUCCESS, p->status);
    }
    
    L11 - Caso de prueba simpleToCompositeFromHighToLowLevel


    Donde:
    • Utiliza el mock de las acciones de la SM
    • La función setProfile() establece las precondiciones de la prueba, instruyendo que rastros espera obtener y en qué orden. Para mayor claridad ver listado L12.
    • La función rkh_sm_dispatch() despacha un evento a la SM en cuestión
    • Finalmente, el módulo nativo Unitrazer, verifica que se cumplan las precondiciones esperadas.


    void
    setProfile(RKH_SMA_T *const me, const RKH_ST_T *currentState, 
               const RKH_ST_T *sourceState, const RKH_ST_T **targetStates, 
               const RKH_ST_T **entryStates, const RKH_ST_T **exitStates, 
               const RKH_ST_T *mainTargetState, int nExecEffectActions, 
               int kindOfTrn, int initStateMachine, const RKH_EVT_T *event,
               const RKH_ST_T *dispatchCurrentState)
    {
        int nEntryStates, nExitStates;
    
        if (initStateMachine)
        {
            sm_init_expect(RKH_STATE_CAST(RKH_SMA_ACCESS_CONST(me, istate)));
            sm_enstate_expect(RKH_STATE_CAST(RKH_SMA_ACCESS_CONST(me, istate)));
        }
        sm_dch_expect(event->e, RKH_STATE_CAST(dispatchCurrentState));
        sm_trn_expect(RKH_STATE_CAST(sourceState), RKH_STATE_CAST(*targetStates));
    
        if (kindOfTrn == TRN_NOT_INTERNAL)
        {
            executeExpectOnList(targetStates, EXPECT_TS_STATE);
            nExitStates = executeExpectOnList(exitStates, EXPECT_EXSTATE);
        }
        sm_ntrnact_expect(nExecEffectActions, 1);
    
        if (kindOfTrn == TRN_NOT_INTERNAL)
        {
            nEntryStates = executeExpectOnList(entryStates, EXPECT_ENSTATE);
            sm_nenex_expect(nEntryStates, nExitStates);
            sm_state_expect(RKH_STATE_CAST(mainTargetState));
        }
        sm_evtProc_expect();
    
        if (initStateMachine)
        {
            rkh_sm_init((RKH_SM_T *)me);
        }
        if (currentState)
        {
            setState(me, RKH_STATE_CAST(currentState));
        }
    }
    
    L12 - Función de ayuda setProfile()

    Referencias

    [1] James W. Grenning, “Test Driven Development for Embedded C”, PragmaticBookshelf, 2011
    [2] Robert C. Martin, “CleanCode”, Prentice Hall, 2009
    [3] Kernighan & Ritchie, "C Programming Language (2nd Edition)", April 1, 1988
    [4] RKH, “RKH Sourceforge download site,”, http://sourceforge.net/projects/rkh-reactivesys/, August7, 2010
    [5] D. Harel, Statecharts: A Visual FormalismforComplexSystems, Sci. Comput. Programming8 (1987), pp. 231–274
    [6] Gerard Mezaros, xUnit Test Patterns: Refactoring Test Code, Financial Times Prentice Hall

    Comentarios