====== Ebee Unit Tests C (Unity) ====== ~~DISCUSSION~~ ===== Modules ===== For unit tests in Ansi-C we are using the test framework Unity, information can be found here http://www.throwtheswitch.org . The tests themselves using the typical assertion mechanism as in many other unit test frameworks. There is the famous AAA(triple A) pattern which can be found for example here https://methodpoet.com/aaa-in-unit-testing/ . ==== Unity ==== Unity is the unit test framework itself with various assertion mechanisms, a good description is here https://github.com/ThrowTheSwitch/Unity. ==== Ceedling ==== This is the unit test build management system, an introduction can be found in http://www.throwtheswitch.org/ceedling . Normally a developer will not get in touch with the installation or configuration of it except for adding needed paths for include files. The system is already set up in our meta projects https://gitlab.com/ebee_smart/linux4sam/linux4sam-meta and https://gitlab.com/ebee_smart/ebee-controller-meta. ==== CMock ==== For functions which are not directly addressed there is the mocking framework CMock https://github.com/ThrowTheSwitch/CMock/blob/master/docs/CMock_Summary.md . With the help of it functions can be mocked, that means we can "simulate" the behavior of them. It is possible to let return specific return codes or completely simulate the behavior. It is also possible to check if a specific function was called and also check if the parameters were set properly. ===== How to build and call unit tests ===== ==== Makefile based ==== In the meta projects there are already prebuilt make targets. `make run-unit-tests` can be used to let run all unit tests. If they have to be compiled it will also done with this make target. The libebee_common will also be built with this target if it was not built before. One dedicated testsuite could be selected for test by option "UNITTEST_TEST=" on make call. (Testsuite means the basename of each test file or "libebee_common" as a whole). `make wipe-unit-tests` can be used to remove all files generated by the unit test build management. `make unit-test-shell` starts the Docker container and set some environment variables to just call the ceedling unit test build system directly. For example it is possible to just call/build a specific test you can then call inside the shell `ceedling test:voltage` for performing all unit tests of the file `test/system_monitoring/monitoring/test_voltage.c`. Specific tests can also be called from outside the container. Just call `CEEDLING_CMD=test:voltage make run-unit-tests`. For performing only tests which were modified since the last test run you can call `CEEDLING_CMD=test:delta make run-unit-tests`. ==== Building without meta project make files ==== It is also possible to build and run unit tests without the high level meta project included make targets. These targets are only available in newer branches >= 5.30. For older maintenance branches e.g. 5.20 here is a general guide. === Building libebee_common === First it is needed to compile the libebee_common as a shared object library. This can be done with the meta project as root folder: - cd controller_software - BUILDROOT=../buildroot test/support/build_ebee_lib.sh The file //libebee_common.so// should then be located in libebee_common/ . === Build and run all unit tests within the container === - cd to the meta project main directory - start the container with\\ make run-container - cd controller_software - start the unit test run with BUILDROOT=../buildroot CC=gcc ceedling === Build and run only modified unit tests within the container === - start the container with make run-container - cd controller_software - start the unit test run with BUILDROOT=../buildroot CC=gcc ceedling test:delta === Build and run a specific unit test within the container === - start the container with make run-container - cd controller_software - start the unit test run with BUILDROOT=../buildroot CC=gcc ceedling test:\\ e.g. BUILDROOT=../buildroot CC=gcc ceedling test:voltage ==== Visual Studio Code ==== In Visual Studio Code it is possible to let run the modified unit tests. There is the build task `5 ceedling test:delta`. You can call it with `Terminal->Run Build Task...->5 ceedling test:delta`. There is also the shortcut `Ctrl+Shift+B`. ==== Debug unit tests ==== For each unit test module built by ceedling an according binary will be created. The libebee_common is needed as a shared object //libebee_common.so//. If it is not present in //./libebee_common// or outdated it can be built with //BUILDROOT=./buildroot controller_software/test/support/build_ebee_lib.sh// from the meta project. If for example the test //test_voltage// should be debugged it has to be built first from the meta project with e.g. //CEEDLING_CMD=test:voltage make run-unit-tests// The binary is located in //controller_software/build/test/out/ //. If the test module //test_voltage// was built the binary is then //controller_software/build/test/out/test_voltage.out//. If there are errors for missing //libebee_common.so// when executing the binary the //LD_LIBRARY_PATH// has to be extended by the path where the //libebee_common.so// is located, e.g. a call from //controller_software/build/test/out/// can look like //LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libebee_common test/out/test_voltage.out// . Now the binary can be debugged with e.g. gdb. If VSCode is used there is a pre-built launch target in //.vscode/launch.json// { "name": "(gdb) ceedling unit test", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/build/test/out/test_meter_measurands.out", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ] } The program name should be then modified in //"program"//. ===== How to set up a new unit test module ===== If a new test file should be added then just add the according test file with the prefix "test_". It is strongly recommended that the same file structure will be used. ==== Adding the source file to be tested ==== If you want to add unit tests for the file %%"src/system_monitoring/monitoring/uptime.c"%% you can create an according test file in %%"test/system_monitoring/monitoring/"%% with the name %%"test_uptime.c"%%. For test modules it is mandatory to include the header %%"unity.h"%%. #include "unity.h" For adding the file to test in the test module it can be either done by adding the according header but in almost all unit test modules we just include the c file itself. It has the advantage to also have influence on static variables and also to call static functions (if it is needed) out of the unit tests. Here the example: #include "unity.h" #include "system_monitoring/monitoring/uptime.c" If there are additional headers used their paths have to be added in the file `project.yaml` in the section `:source`. **Alternatively** you can use ceedling within the `make unit-test-shell` environment to create the file for you by calling: alex@cpbuild:~$ ceedling module:create[system_monitoring/monitoring/uptime] File src/system_monitoring/monitoring/uptime.c already exists! File src/system_monitoring/monitoring/uptime.h already exists! File test/system_monitoring/monitoring/test_uptime.c created Generate Complete This will generate a test file based on a template ==== Setup/tear down ==== It is possible to call a setup and/or a teardown function which will be called for each unit test, in the example there is just a log message. The setup/teardown should only be used if needed. A good example would be to register/unregister PStore parameter to create an own parameter store. There are examples in some unit test modules, e.g. here %%"test/helpers/test_branding.c"%%. In our example (just a log message written) #include "unity.h" #include "system_monitoring/monitoring/uptime.c" #include void setUp(void) { LOG_INFO_FN("set up"); } void tearDown(void) { LOG_INFO_FN("tear down"); } ==== Adding unit tests ==== For adding tests it is mandatory to use the prefix %%"test_"%% in each unit test method, for example #include "unity.h" #include "system_monitoring/monitoring/uptime.c" void test_monitoring_uptime_timer_expired_return_correct_value(void) { } Now we can add a unit test according to the AAA pattern. #include "unity.h" #include "system_monitoring/monitoring/uptime.c" void test_monitoring_uptime_timer_expired_return_correct_value(void) { // arrange uptime_timer_ev = 123; // act int ret = monitoring_uptime_timer_expired(); // assert TEST_ASSERT_EQUAL(0, ret); // arrange uptime_timer_ev = 0; // act ret = monitoring_uptime_timer_expired(); // assert TEST_ASSERT_EQUAL(1, ret); } In the example unit test there are 2 assertions, one for the case the timer was expired, the other for the not expired case. It is also possible to have 2 separate unit tests, for example #include "unity.h" #include "system_monitoring/monitoring/uptime.c" void test_monitoring_uptime_timer_expired_return_1_if_expired(void) { // arrange uptime_timer_ev = 0; // act int ret = monitoring_uptime_timer_expired(); // assert TEST_ASSERT_EQUAL(1, ret); } void test_monitoring_uptime_timer_expired_return_0_if_not_expired(void) { // arrange uptime_timer_ev = 123; // act int ret = monitoring_uptime_timer_expired(); // assert TEST_ASSERT_EQUAL(0, ret); } ==== How to mock functions ==== === Mock with CMock === If the behavior of functions has to be simulated then mocking is the choice. For example we want to test the behavior of `uptime_timer_ev` void test_uptime_timer_cb_sets_uptime_timer_ev_to_zero(void) { // some arrangements uptime_timer_cb(NULL); // some assertions } If the test will be compiled then some linker errors will appear like ebee-controller-meta-new-buildroot/controller_software/src/system_monitoring/monitoring/uptime.c:43: undefined reference to `ocpp_is_ready_for_transaction_notified' Those errors come from the linker because the used functions are not defined because the source files were not compiled and linked. For this reason we can define a mocking function and also let the mock define the behavior to test different code paths. This should be done by writing a mock for every used function. The mocks will be generated when the according header files will also be included in the test module, just extended with the prefix 'mock_\'. #include "unity.h" #include "system_monitoring/monitoring/uptime.c" #include "ocpp_logic/ocpp_hooks/mock_ocpp_on_ready_for_transaction_hook.h" #include "meter/mock_meter.h" #include "error_handling/mock_ebee_error_handling.h" #include "ocpp_logic/ocpp_connector/mock_ocpp_connector.h" #include "eevt/mock_eevt.h" For all added mock headers their paths have to be added in the file `project.yaml` in the section `:source` if they are not present there yet. * src/ocpp_logic/ocpp_hooks * src/meter * src/error_handling * src/ocpp_logic/ocpp_connector * src/eevt Let's write a unit test for the function `uptime_timer_cb`. There are several function calls static void uptime_timer_cb(void *arg __attribute__((unused))) { LOG_INFO("System uptime is %d minutes", UPTIME_TIMOUT_MIN); uptime_timer_ev = 0; if (! ocpp_is_ready_for_transaction_notified() && meter_is_ready_for_billing() && ! ocpp_connector_is_unavailable_or_faulted(OCPP_LOCAL_CONNECTOR) && error_is_charging_allowed(OCPP_LOCAL_CONNECTOR)) { eevt_trigger(EEVT_OCPP_READY_FOR_TRANSACTION, &(bool){true}); } } For example we want to check if the callback calls `eevt_trigger` with the correct arguments. There are some conditions: - `ocpp_is_ready_for_transaction_notified` should return `false` - `meter_is_ready_for_billing` should return `1` - `ocpp_connector_is_unavailable_or_faulted` should be called with the parameter `OCPP_LOCAL_CONNECTOR` and should return `0` - `error_is_charging_allowed` should be called with the parameter `OCPP_LOCAL_CONNECTOR` and should return `1` `eevt_trigger` should be called with the parameters `EEVT_OCPP_READY_FOR_TRANSACTION` and `&(bool) {true}`. The conditions are met easily. There are parameters expected in some functions and some defined return codes to be set. ocpp_is_ready_for_transaction_notified_ExpectAndReturn(false); meter_is_ready_for_billing_ExpectAndReturn(1); ocpp_connector_is_unavailable_or_faulted_ExpectAndReturn(OCPP_LOCAL_CONNECTOR, 0); error_is_charging_allowed_ExpectAndReturn(OCPP_LOCAL_CONNECTOR, 1); The mocks are created by just adding the postfix `_ExpectAndReturn`. The first parameters are the expected values. Internally the mocked function will perform an assertion. It is also possible to ignore the parameters. Then the postfix `_IgnoreAndReturn` should be used. The last parameter is the return code the mock should return. In most cases this easy mock creation will work. Sometimes it is more complicated where for example pointers are used in the functions to be mocked. Then it is also possible to do this by just defining a complete custom behavior by just writing a stub. As you can see the function `eevt_trigger` expects the second parameter `&(bool) {true}`, so the address of a variable holding the boolean value `true`. For defining a stub as a separate function you can just use the function prototype and as the last parameter you have to add the number of function calls as an integer. This can then also be used to define a behavior depending of an iteration or similar. A stub could look like this: static int (**eevt_trigger_stub(eevt_type_t eevt_type, const void *data, int num_calls))() { TEST_ASSERT_EQUAL(EEVT_OCPP_READY_FOR_TRANSACTION, eevt_type); bool data_as_bool = *((bool *)data); TEST_ASSERT_TRUE(data_as_bool); return 0; }; For using the stub it can be done by eevt_trigger_StubWithCallback(eevt_trigger_stub); === Mock functions locally === An alternative way and a possibility to mock LIBC functions too, is to define the function in your test file locally again. The linker will choose this as the implementation and you could define the functionality and return values by your own. **BUT BE WARNED:** Keep the mocks of common known functions (LIBC, ...) really local (with '__attribute__ ((visibility ("hidden")));') in your implementation and be carefully that the mocked common known functions not influence 3rd party libs. Please see this example: Function to test is: int ssl_comm_read_from_pkcs_file(const char *pkcs_file_name, X509 **x509, EVP_PKEY **pkey, STACK_OF(X509) * *cas) { int ret = -1; if (! pkcs_file_name || ! x509 || ! pkey) { return -EFAULT; } FILE *fd = fopen(pkcs_file_name, "r"); if (! fd) { LOG_WARN_FN("Could not open PKCS12 container: %s", pkcs_file_name); } else { PKCS12 *pkcs12 = d2i_PKCS12_fp(fd, NULL); fclose(fd); if (! pkcs12) { LOG_WARN_FN("Could not decode PKCS12 container"); } else { int ret1 = PKCS12_parse(pkcs12, ssl_comm_pw, pkey, x509, cas); PKCS12_free(pkcs12); if (ret1 != 1) { LOG_WARN_FN("Could not parse PKCS12 container"); } else { ret = 0; // success } } } return ret; } For a successful test, one have to mock fopen(), d2i_PKCS12_fp() and PKCS12_parse(). Define the function mocks together with a static variable for the return value to set before executing the test: /* function mocks definition */ static FILE *fopen_ret; FILE *fopen(const char *filename, const char *mode) __attribute__ ((visibility ("hidden"))); FILE *fopen(const char *filename, const char *mode) { return fopen_ret; } int fclose(FILE *fp) __attribute__ ((visibility ("hidden"))); int fclose(FILE *fp) { return 0; } static void *d2i_PKCS12_fp_ret; PKCS12 *d2i_PKCS12_fp(FILE *fp, PKCS12 **p12) { return d2i_PKCS12_fp_ret; } void PKCS12_free(PKCS12 *p12) { return; } /** @return 0 ... error, 1 ... success */ static int PKCS12_parse_ret; int PKCS12_parse(PKCS12 *p12, const char *pw, EVP_PKEY **pkey, X509 **cert, STACK_OF(X509) * *ca) { return PKCS12_parse_ret; } And the test using it will set the expected return value before calling the function to test than uses the mocked functions above: void test_ssl_comm_read_from_pkcs_file(void) { X509 *x509 = NULL; EVP_PKEY *pkey = NULL; /* set expected mock return values to failure */ fopen_ret = NULL; d2i_PKCS12_fp_ret = NULL; PKCS12_parse_ret = 0; /* case 2: file not found, fopen() failed */ ret = ssl_comm_read_from_pkcs_file("dummy", &x509, &pkey, NULL); TEST_ASSERT_EQUAL_INT(-1, ret); TEST_ASSERT_NULL(x509); TEST_ASSERT_NULL(pkey); /* case 3: fopen() succeed, but d2i_PKCS12_fp() failed */ fopen_ret = (FILE *)1; /* change mock return value to success */ ret = ssl_comm_read_from_pkcs_file("dummy", &x509, &pkey, NULL); TEST_ASSERT_EQUAL_INT(-1, ret); TEST_ASSERT_NULL(x509); TEST_ASSERT_NULL(pkey); /* case 4: fopen() and d2i_PKCS12_fp() succeed, but PKCS12_parse() failed */ fopen_ret = (FILE *)1; /* change mock return value to success */ d2i_PKCS12_fp_ret = (PKCS12 *)1; /* change mock return value to success */ ret = ssl_comm_read_from_pkcs_file("dummy", &x509, &pkey, NULL); TEST_ASSERT_EQUAL_INT(-1, ret); TEST_ASSERT_NULL(x509); TEST_ASSERT_NULL(pkey); /* case 5: fopen(), d2i_PKCS12_fp() and PKCS12_parse() succeed */ fopen_ret = (FILE *)1; /* change mock return value to success (not rellay needed, for explaination) */ d2i_PKCS12_fp_ret = (PKCS12 *)1; /* change mock return value to success */ PKCS12_parse_ret = 1; /* change mock return value to success */ ret = ssl_comm_read_from_pkcs_file("dummy", &x509, &pkey, NULL); TEST_ASSERT_EQUAL_INT(0, ret); } === The complete unit test example with CMock === #include "unity.h" #include "system_monitoring/monitoring/uptime.c" #include "ocpp_logic/ocpp_hooks/mock_ocpp_on_ready_for_transaction_hook.h" #include "meter/mock_meter.h" #include "error_handling/mock_ebee_error_handling.h" #include "ocpp_logic/ocpp_connector/mock_ocpp_connector.h" #include "eevt/mock_eevt.h" void test_monitoring_uptime_timer_expired_return_1_if_expired(void) // arrange uptime_timer_ev = 0; // act int ret = monitoring_uptime_timer_expired(); // assert TEST_ASSERT_EQUAL(1, ret); } void test_monitoring_uptime_timer_expired_return_0_if_not_expired(void) { // arrange uptime_timer_ev = 123; // act int ret = monitoring_uptime_timer_expired(); // assert TEST_ASSERT_EQUAL(0, ret); } int (**eevt_trigger_stub(eevt_type_t eevt_type, const void *data, int num_calls))() { TEST_ASSERT_EQUAL(EEVT_OCPP_READY_FOR_TRANSACTION, eevt_type); bool data_as_bool = *((bool *)data); TEST_ASSERT_TRUE(data_as_bool); return 0; }; void test_uptime_timer_cb_calls_correct_eevt_trigger(void) { // arrange uptime_timer_ev = 1; ocpp_is_ready_for_transaction_notified_ExpectAndReturn(false); meter_is_ready_for_billing_ExpectAndReturn(1); ocpp_connector_is_unavailable_or_faulted_ExpectAndReturn(OCPP_LOCAL_CONNECTOR, 0); error_is_charging_allowed_ExpectAndReturn(OCPP_LOCAL_CONNECTOR, 1); eevt_trigger_StubWithCallback(eevt_trigger_stub); // act uptime_timer_cb(NULL); // assert TEST_ASSERT_EQUAL(0, uptime_timer_ev); } === Unit test modules that uses functions from cpp modules === If you want to create a unit test for an module that uses functions from an Cpp module, you have to create an mock header for the Cpp module, extended with the postfix mock. Like in dlm_charts_db_mock.h. #ifndef DB_DLM_CHARTS_DB_MOCK_H_ #define DB_DLM_CHARTS_DB_MOCK_H_ void dlm_charts_db_upgrade(int version) {(void)version;} void dlm_charts_db_reduce_table_size(void) {} void dlm_charts_add_slave_entry(int slave_id) {(void)slave_id;} int dlm_charts_db_get_count(void) {return 0;} #endif /* DB_DLM_CHARTS_DB_MOCK_H_ */ That header you have to include instead of the prefix mock header. #include "unity.h" #define HOSTSIMULATION #include "db.c" #include "sqlite3.h" #include "mock_ebee_version.h" #include "mock_transaction_db.h" #include "mock_dlm_slaves_db.h" // #include "mock_dlm_charts_db.h" #include "db/dlm_charts_db_mock.h" #include "mock_key_value_store_db.h" #include "config/parameters/mock_database_parameters.h"