BDD C++ en Windows con Boost y cucumber-cpp

Introducción

BDD del inglés Behaviour Driven Development, o desarrollo guiado por comportaminto. BDD es la evolución del TDD (Test Driven Development) que nace como consecuencia de analizar los problemas del TDD, como son la distancia conceptual que hay entre los analistas (los que conocen que debe hacer el producto) y los probadores/desarrolladores (los que conocen que es lo que realmente hace).

Con BDD y frameworks como cucumber se pretende acortar dicho salto conceptual. La idea es poder definir pruebas unitarias con un lenguaje muy parecido al lenguaje natural (Gherkin):

En el texto anterior se puede ver que se quiere probar una característica (feature) de suma en una calculadora, en la parte de abajo dónde se describe un escenario (sumar dos números) se proporcionan de manera legible (para los analistas) los parámetros de la prueba, con lo que se consiguen dos objetivos: que la especificación de pruebas se auto documente en un lenguaje asequible y que se implementen pruebas automáticamente (en realidad, casi automáticamente dado que como veremos adelante todavía hay que hacer más trabajo).

Entorno elegido

Para desarrollar pruebas en lenguaje Gherkin, usaremos cucumber como framework de ejecución de pruebas; además necesitamos el conector para C++ llamado cucumber-cpp, el cual debe ser compilado junto con las librerías boost.

El IDE elegido para Windows es Visual Studio 2010.

[important]En plataforma Linux (incluso Mac OS y Cygwin) la compilación e instalación de este framework es mucho más sencilla, de ahí que escriba este post para documentar el tenebroso camino que he tenido que recorrer :)[/important]

Instalación

Compilar boost

Lo primero es bajarse la última versión de boost, en mi caso la 1.50.0.

Descomprimimos dentro de C:\Programa Files (x86)\boost\ (sin el (x86) para Windows de 32 bits), con lo que debiera haber una ruta al final tal que C:\Program Files (x86)\boost\boost_1_50_0\, y abrimos una línea de comandos en dicha carpeta:

.\bootstrap.bat
.\b2.exe tooset=msvc --build-type=complete --build-dir=lib --stagedir=.

Después de varias horas de compilación (lo siento 🙁 ) tendréis un directorio nuevo llamado lib dónde tendréis todas las variantes de las librerías boost compiladas.

Compilar cucumber-cpp

Antes de nada tenemos que tener instalado CMake para Windows. Simplemente bajar e instalar la última versión, en mi caso la 2.8.8.

[warning]Asegúrate de seleccionar la opción Add CMake to the system PATH for all users.[/warning]

Descárgate la última versión de cucumber-cpp de la web y descomprímelo en cualquier sitio temporal, por ejemplo C:\Temp\, de modo que al descomprimir tendremos todo en C:\Temp\cucumber-cpp-73c2325\.

Para compilar NO seguiremos las instrucciones originales, sino que haremos nuestro propio proyecto Visual Studio, del tipo Visual C++\Win32\Aplicación de consola WIN32, lo crearemos dentro de la carpeta anterior en e raíz y sin seleccionar la opción Crear directorio para la solución, seleccionando las opciones de Biblioteca estática y deseleccionando la opción Encabezado precompilado.

Añadimos todos los ficheros fuente de la carpeta src, (directorio por directorio, incluidas las subcarpetas connectors\wire y drivers, de esta última sólo el fichero BoostDriver.cpp).

Modificamos la opción de proyecto Propiedades de la configuración -> C/C++ -> General -> Directorios de inclusión adicionales para Todas las configuraciones (aseguraos que no tenéis sólo seleccionado Debug), añadiendo lo siguiente include;$(ProgramFiles)\boost\boost_1_50_0.

Compilamos en modo Degug y Release.

Finalmente instalamos archivos de cabecera y librerías en la carpeta C:\Programa Files (x86)\cucumber-cpp\cucumber-cpp-73c2325\, abriendo una línea de comandos desde el directorio C:\Temp\cucumber-cpp-73c2325\:

mkdir -p C:\Programa Files (x86)\cucumber-cpp\cucumber-cpp-73c2325\lib
xcopy /E /Y Release\cucumber-cpp.lib "C:\Programa Files (x86)\cucumber-cpp\cucumber-cpp-73c2325\lib\cucumber-cpp.lib"
xcopy /E /Y Debug\cucumber-cpp.lib "C:\Programa Files (x86)\cucumber-cpp\cucumber-cpp-73c2325\lib\cucumber-cpp-gd.lib"
xcopy /E /Y include "C:\Programa Files (x86)\cucumber-cpp\cucumber-cpp-73c2325\include\"

Cucumber

Para instalar cucumber hay que tener instalado Ruby, sino estuviera instalado, bajar la última versión, en mi caso 1.9.3-p194. Luego instalamos las gemas necesarias:

gem install cucumber rspec

Pruebas

Proyecto en Visual Studio

Para probar crearemos un proyecto Visual Studio nuevo proyecto, en las opciones del proyecto configuraremos los siguiente:

  • Todas las configuraciones: añadimos los directorios de archivos de cabecera y de librerías compiladas de boost y cucumber-cpp:
    • Propiedades de configuración -> C/C++ -> General -> Directorios de inclusión adicionales: $(ProgramFiles)\boost\boost_1_50_0;$(ProgramFiles)\cucumber-cpp\cucumber-cpp-73c2325\include;%(AdditionalIncludeDirectories)
    • Propiedades de configuración -> Vinculador -> General -> Directorios de bibliotecas adicionales: $(ProgramFiles)\boost\boost_1_50_0\lib;$(ProgramFiles)\cucumber-cpp\cucumber-cpp-73c2325\lib;%(AdditionalLibraryDirectories)
  • Debug: librerías en modo depuración de boost y cucumber-cpp:
    • Propiedades de configuración -> Vinculador -> Entrada -> Dependencias adicionales: cucumber-cpp-gd.lib;libboost_unit_test_framework-vc100-mt-gd-1_50.lib;%(AdditionalDependencies)
  • Release: librerías en modo producción de boost y cucumber-cpp:
    • Propiedades de configuración -> Vinculador -> Entrada -> Dependencias adicionales: cucumber-cpp.lib;libboost_unit_test_framework-vc100-mt-1_50.lib;%(AdditionalDependencies)

Este ejemplo está tomado del ejemplo que viene con la librería cucumber-cpp.

Ficheros fuente

Creamos una estructura de ficheros y directorios del siguiente modo:

/
+->features
|  +->step_definitions
|  |  +->cucumber.wire
|  +->addition.feature
|  +->division.feature
+->inc
|  +->Calculator.h
+->scr
   +->Calculator.cpp
   +->CalculatorSteps.cpp

[important]Lo normal sería tener dos proyectos separados, uno para el código de la librería o ejecutable, y otro para sus pruebas, pero por simplificar hemos creado todo en el mismo.[/important]

La carpeta features incluye la definición de las características a probar (será ejecutado desde Ruby mediante la gema cucumber), y se conectará a nuestro programa en C++ de prueba mediante TCP/IP.

Las carpetas inc y src incluyen la clase de ejemplo Calculator (el nombre lo dice todo 🙂 ) y un fichero para los pasos de prueba de la misma.

El contenido de los ficheros es el siguiente:

features\step_definitions\cucumber.wire

Este fichero contiene la información de equipo y puerto para que cucumber desde Ruby se conecte por TCP a nuestro ejecutable en C++ con las definiciones de los pasos de pruebas.

host: localhost
port: 3902
features\addition.feature

En este fichero se describe la característica de sumar, se usa el formato de tabla de datos (sección Examples) y los pasos se describen en Scenario Outline. El idioma es inglés, pero tal y como la documentación de cucumber nos indica se podría usar cualquier idioma.

# language: en
Feature: Addition
  In order to avoid silly mistakes
  As a math idiot 
  I want to be told the sum of two numbers

  Scenario Outline: Add two numbers
    Given I have entered <input_1> into the calculator
    And I have entered <input_2> into the calculator
    When I press <button>
    Then the result should be <output> on the screen

  Examples:
    | input_1 | input_2 | button | output |
    | 20      | 30      | add    | 50     |
    | 2       | 5       | add    | 7      |
    | 0       | 40      | add    | 40     |
features\division.feature

Igual pero para la división (esta vez no se usa el formato tabla):

# language: en
Feature: Division
  In order to avoid silly mistakes
  Cashiers must be able to calculate a fraction

  Scenario: Regular numbers
    Given I have entered 3 into the calculator
    And I have entered 2 into the calculator
    When I press divide
    Then the result should be 1.5 on the screen
inc\Calculator.h

Fichero de definición de la clase Calculator:

#ifndef __CALCULATOR_H__
#define __CALCULATOR_H__

#include <list>

class Calculator {
private:
    std::list<double> values;
public:
    void push(double);
    double add();
    double divide();
};

#endif
src\Calculator.cpp

Fichero de implementación de la clase Calculator:

#include <limits>
#include "../inc/Calculator.h"

void Calculator::push(double n) {
    values.push_back(n);
}

double Calculator::add() {
    double result = 0;
    for(std::list&lt;double&gt;::const_iterator i = values.begin(); i != values.end(); ++i) {
        result += *i;
    }
    return result;
}

double Calculator::divide() {
    double result = std::numeric_limits<double>::quiet_NaN();
    for(std::list&lt;double&gt;::const_iterator i = values.begin(); i != values.end(); ++i) {
        if (i == values.begin()) {
            result = *i;
        } else {
            result /= *i;
        }
    }
    return result;
}</double></limits>
src\CalculatorSteps.cpp

Fichero de definición de los pasos de prueba de la clase Calculator:

#include <boost/test/unit_test.hpp>
#include <cucumber-cpp/defs.hpp>

#include "../inc/Calculator.h"

struct CalcCtx {
    Calculator calc;
    double result;
};

GIVEN("^I have entered (\\d+) into the calculator$") {
    REGEX_PARAM(double, n);
    USING_CONTEXT(CalcCtx, context);
    context->calc.push(n);
}

WHEN("^I press add") {
    USING_CONTEXT(CalcCtx, context);
    context->result = context->calc.add();
}

WHEN("^I press divide") {
    USING_CONTEXT(CalcCtx, context);
    context->result = context->calc.divide();
}

THEN("^the result should be (.*) on the screen$") {
    REGEX_PARAM(double, expected);
    USING_CONTEXT(CalcCtx, context);
    BOOST_CHECK_EQUAL(expected, context->result);
}

Ejecución de los tests

Una vez compilado el proyecto anterior, lo ejecutamos en modo Release, este programa se quedará escuchando en el puerto TCP especificado anteriormente a que la ejecución de las pruebas sea lanzada (este programa se cierra una vez se han ejecutado los tests, de modo que hay que relanzarlo cada vez que queramos ejecutarlos).

La ejecución de los tests la haremos desde la carpeta del proyecto, desde la línea de comandos lanzamos el comando cucumber (este programa en Ruby se conectará por TCP al programa en C++ que tenemos lanzado) y la salida será la siguiente:

cucumber
# language: en
Feature: Addition
  In order to avoid silly mistakes
  As a math idiot
  I want to be told the sum of two numbers

  Scenario Outline: Add two numbers                    # features\addition.feature:7
    Given I have entered <input_1> into the calculator # CalculatorSteps.cpp:11
    And I have entered <input_2> into the calculator   # CalculatorSteps.cpp:11
    When I press <button>                              # CalculatorSteps.cpp:17
    Then the result should be <output> on the screen   # CalculatorSteps.cpp:27

    Examples:
      | input_1 | input_2 | button | output |
      | 20      | 30      | add    | 50     |
      | 2       | 5       | add    | 7      |
      | 0       | 40      | add    | 40     |

# language: en
Feature: Division
  In order to avoid silly mistakes
  Cashiers must be able to calculate a fraction

  Scenario: Regular numbers                     # features\division.feature:6
    Given I have entered 3 into the calculator  # CalculatorSteps.cpp:11
    And I have entered 2 into the calculator    # CalculatorSteps.cpp:11
    When I press divide                         # CalculatorSteps.cpp:22
    Then the result should be 1.5 on the screen # CalculatorSteps.cpp:27

4 scenarios (4 passed)
16 steps (16 passed)
0m1.132s

5 comentarios

  1. Mark dice: Responder

    I am a bit confused on the testing portion. When we create the testing project, are we deploying a exe, library? Also where does the wire server come into play?

  2. cukie dice: Responder

    How to i give the input to REGEX_PARAM if I have say “Given i enter number1 and number2 in calculator” ? First is this even possible?

  3. Johan dice: Responder

    Nice article, thanks for posting this.

    Just one question, how can I contact you via email? I have a question about the building/compiling of cucumber-cpp in Visual Studio (2012).

    1. Just ask here, so everyone can take advance of your question, and maybe of my answer.

  4. Rafa dice: Responder

    ¡Mola!

Deja un comentario