BDD Testing C++ Library with Python Behave

Let’s try to test a shared library (.dll or .so) with python.

You can clone the code from: https://gitlab.com/dominik.gausa/bdd-test-shared-library

C++ shared library

Our library under test exports some basic functions

// Lib/src/lib.h

LIBRARY_API void api_say_hello();
LIBRARY_API unsigned int api_call_count();
LIBRARY_API int api_sum(int a, int b);
LIBRARY_API float api_vector_size(struct vector_s* vect);

To compile the library a minimal CMake file is used. This allows compilation with any given Operating system and compiler or IDE.
Linux with gcc, make and CMake
Windows with Visual Studio

// file Lib/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(lib)

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

Use shared library from Python

To load a shared library in python we write

// file bdd_test/features/steps/lib.py

from ctypes import *

LIB_PATH='../Lib/Build/Debug/lib.dll'
dll = cdll.LoadLibrary(LIB_PATH)

Calling some of the exposed functions can then be done using

// file bdd_test/features/steps/lib.py

dll.api_say_hello()
result = dll.api_call_count() 

Note that python ctypes lets us directly call the functions with the same name as the library exposed them.

For non trivial / basic types it is a bit more complex

// file bdd_test/features/steps/lib.py

// This mirrors the struct from C++ lib.h
class Vector(Structure):
  _fields_ = (
    ("x", c_float),
    ("y", c_float)
  )

// Create new Vector object
vect = Vector(value1, value2)

// Ensure return type of function is float
dll.api_vector_size.restype = c_float

// Call C++ function and pass the Python instance of vector as reference
result = context.dll.api_vector_size(byref(vect))

After dealing with the shared library we may want to unload it. This will also remove all changed values of global variables. If we reload the shared library those will be reinitialized.

// file bdd_test/features/environment.py

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

BDD testing

Having now all basics, we can start testing it with python behave

  • Creating a minimal shared library
  • Loading and unloading the library from python

Let’s start easy by just calling a function. The corresponding Gherkins could look like this:

// file bdd_test/features/lib.feature

Feature: Library

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

With implementation of the steps as

// file bdd_test/features/steps/lib.py

@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()

Running the behave framework will now load the library and call the function. To clean up after each scenario and reset globals we do some automated cleanup in features/environment.py as described in earlier paragraph.

PS D:\workspace\python\Dll\bdd_test> python -m 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

  Scenario: Sum                         # features/lib.feature:8
    Given Library                       # features/steps/lib.py:8
    When 'sum' is called with 12 and -4 # features/steps/lib.py:20
    Then result is 8                    # features/steps/lib.py:41

An failed test can look like

  Scenario: Vector                                # features/lib.feature:13
    Given Library                                 # features/steps/lib.py:8
    When 'vector_size' is called with Vector(4,4) # features/steps/lib.py:31
Lib: Calc vector_size for: 4.000000, 4.000000
Lib: Size is: 5.656854
    Then result is 8                              # features/steps/lib.py:41
      Assertion Failed: context.result == int(value)  5.656854152679443 != 8
      Captured stdout:
      5.656854152679443

Leave a Reply

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