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