BDD testing C++ Library

Why should a tester who focuses on use cases and requirements use highly specific and complex C++ language?

If the software architecture is already thinking in components and concerns, there will probably be a library that we can test.

So how can we use for example python for testing C++ libraries in an easy way?

For python there’s already a nice project bringing Gherkins BDD language in. https://behave.readthedocs.io/en/stable/

So it’s easy to define a Feature and map the steps to python methods, that will be called on run.

Feature: showing off behave

  Scenario: run a simple test
     Given we have behave installed
      When we implement a test
      Then behave will test it for us!
from behave import *

@given('we have behave installed')
def step_impl(context):
    pass

@when('we implement a test')
def step_impl(context):
    assert True is not False

@then('behave will test it for us!')
def step_impl(context):
    assert context.failed is False

Running it in console is just calling behave.

% behave
Feature: showing off behave # features/tutorial.feature:1

  Scenario: run a simple test        # features/tutorial.feature:3
    Given we have behave installed   # features/steps/tutorial.py:3
    When we implement a test         # features/steps/tutorial.py:7
    Then behave will test it for us! # features/steps/tutorial.py:11

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
3 steps passed, 0 failed, 0 skipped, 0 undefined

This is actually very nice! Using the context object instances can be created and passed to following steps.

If the requriements are now already written in Gherkins, everything a tester now needs to do is to ensure that the test frameworks supports all steps that are used and that the test setup works.

Testing C++ Library

Assuming some fancy busines logic written in C++ and by architectural design compiled into a library.

#pragma once

#ifdef LIBRARY_EXPORTS
#define LIBRARY_API __declspec(dllexport)
#else
#define LIBRARY_API __declspec(dllimport)
#endif

#ifdef __cplusplus 
#define LIBRARY_API extern "C" LIBRARY_API 
#endif

LIBRARY_API void api_say_hello();
LIBRARY_API unsigned int api_call_count();
LIBRARY_API int api_sum(int a, int b);

struct vector_s
{
  float x, y;
};

LIBRARY_API float api_vector_size(struct vector_s* vect);
LIBRARY_API void api_vector_normalize(struct vector_s* vect);
#include "lib.h"
#include <stdio.h>
#include <math.h>

void api_say_hello()
{
  printf("Lib: Hello!\n");
}

unsigned int api_call_count()
{
  static unsigned int count = 0;
  return ++count;
}

int api_sum(int a, int b)
{
  return a + b;
}

float api_vector_size(struct vector_s* vect)
{
	printf("Lib: Calc vector_size for: %f, %f\n", vect->x, vect->y);
	float size = sqrtf(vect->x * vect->x + vect->y * vect->y);
	printf("Lib: Size is: %f\n", size);
	return size;
}

void api_vector_normalize(struct vector_s* vect)
{
	printf("Lib: api_vector_normalize for: %f, %f\n", vect->x, vect->y);
	float size = api_vector_size(vect);
	vect->x /= size;
	vect->y /= size;
	printf("Lib: normalized is: %f, %f\n", vect->x, vect->y);
}

Compiled by a highly complex CMake configuration.

cmake_minimum_required(VERSION 3.15)
project(lib)

add_library(lib SHARED src/lib.c)
target_compile_definitions(lib PRIVATE LIBRARY_EXPORTS=y)

It’s possible to call this library using python with the ctypes library.

Already taking in hook from behave we use:

from behave import *
from ctypes import *
from ctypes import wintypes


def CloseDLL(dll):
  handle = dll._handle
  del dll
  kernel32 = WinDLL('kernel32', use_last_error=True)
  kernel32.FreeLibrary.argtypes = [wintypes.HMODULE]
  kernel32.FreeLibrary(handle)


def before_scenario(context, scenario):
  pass

def after_scenario(context, scenario):
  if context.dll:
    CloseDLL(context.dll)
from behave import *
from ctypes import *

LIB_PATH='../Lib/out/Build/x64-Debug/lib.dll'



@given(u'Library')
def step_impl(context):
  context.dll = cdll.LoadLibrary(LIB_PATH)

@when(u'\'say_hello\' is called')
def step_impl(context):
  context.result = context.dll.api_say_hello()

@when(u'\'call_count\' is called')
def step_impl(context):
  context.result = context.dll.api_call_count()

@when(u'\'sum\' is called with {value1} and {value2}')
def step_impl(context, value1, value2):
  context.result = context.dll.api_sum(int(value1), int(value2))


class Vector(Structure):
  _fields_ = (
    ("x", c_float),
    ("y", c_float)
  )

  def Dump(self):
    print("Vector is: ")
    for fname, ftype in self._fields_:
      print(fname, getattr(self, fname))


@when(u'vector is Vector({value1},{value2})')
def step_impl(context, value1, value2):
  vect = Vector(float(value1), float(value2))
  vect.Dump()
  context.result = {"vect": vect}

@when(u'vector is normalized')
def step_impl(context):
  context.dll.api_vector_normalize(byref(context.result["vect"]))
  context.result["vect"].Dump()


@Then(u'vector is Vector({value1},{value2})')
def step_impl(context, value1, value2):
  vect = Vector(float(value1), float(value2))
  assert round(getattr(context.result["vect"], "x"), 2) == round(getattr(vect, "x"), 2)
  assert round(getattr(context.result["vect"], "y"), 2) == round(getattr(vect, "y"), 2)

@then(u'vector_size is {value}')
def step_impl(context, value):
  context.dll.api_vector_size.restype = c_float
  size = context.dll.api_vector_size(byref(context.result["vect"]))
  assert float(value) == round(size, 2)
  
@then(u'result is {value}')
def step_impl(context, value):
  assert context.result == float(value)

@then(u'pass')
def step_impl(context):
  assert True == True

It’s already possible to handle basic scenarios like

Feature: Library

Scenario: Say Hello
  Given Library
  When 'say_hello' is called
  Then pass

Scenario: Sum
  Given Library
  When 'sum' is called with 12 and -4
  Then result is 8

Scenario Outline: Call count
  Given Library
  When 'call_count' is called
  Then result is <result>

  Examples: 
    | result |
    | 1|
    | 1|

Scenario: Vector Size
  Given Library
  When vector is Vector(4, 4)
  Then vector_size is 5.66

Scenario: Vector Normalize
  Given Library
  When vector is Vector(4, 4)
  And vector is normalized
  Then vector is Vector(0.71, 0.71)
  And vector_size is 1

Breaking down the first Scenario, Given Library will result in behave calling the matching function that is loading the business logic library and storing it’s handle to context.

When 'say_hello' is called then calls the exposed function of the library, which then prints “Lib: Hello” to console.

bdd_test> behave
Feature: Library # features/lib.feature:1

  Scenario: Say Hello          # features/lib.feature:3
    Given Library              # features/steps/lib.py:8
    When 'say_hello' is called # features/steps/lib.py:12
Lib: Hello!
    Then pass                  # features/steps/lib.py:45

Passing structs

It’s also possible to call functions that take non trivial types like structs.
To achive this, the struct is setup using ctypes.

class Vector(Structure):
  _fields_ = (
    ("x", c_float),
    ("y", c_float)
  )

And then plainly created by vect = Vector(value1, value2)

Ensuring ctype uses correct return value interpretation is achived by   context.dll.api_vector_size.restype = c_float And finally calling the exposed library function context.result = context.dll.api_vector_size(byref(vect)).

Code can be found here: https://gitlab.com/dominik.gausa/bdd-test-shared-library

Leave a Reply

Your email address will not be published. Required fields are marked *