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