How To: Unit Testing Asterisk

So you’ve written some code for Asterisk and now you need to test it. The Asterisk project has support for both integration and unit testing. In this post we’ll talk about the latter, how to write a unit test. Luckily, for someone already modifying, or adding to, the Asterisk source writing a unit test has a fairly low entry point since the test itself is written in ‘C’ and should be calling the code, which it covers, directly.

Many unit tests are self contained in their own modules, and are located in the test directory within the main Asterisk source. However, if needed they can be directly embedded within another module’s source file, which you may need to do if the code you need to test is not externally accessible.

Getting Started with Unit Testing

First a bit of setup. We need some code to test. For the sake of simplicity we’ll write a small function and include it in our unit test file that we’ll create:

enum FABULOUS_RESULT {
	F_TERRIFIC,
	F_SURREAL,
	F_WILD
};

static enum FABULOUS_RESULT fabulous_fun(const char *s)
{
	if (!strcmp(s, "terrific")) {
		return F_TERRIFIC;
	} else if (!strcmp(s, "surreal")) {
		return F_SURREAL;
	}

	return F_WILD;
}

Create a file beneath the test directory and prefix the name with test_ (an Asterisk convention used by other tests). For the purposes of this post we’ll use “test_example.c”. To start, at a minimum we’ll include the following definitions and headers near or at the top of the file:

/*** MODULEINFO
<depend>TEST_FRAMEWORK</depend>
<support_level>core</support_level>
***/

#include "asterisk.h"

ASTERISK_REGISTER_FILE()

#include "asterisk/test.h"
#include "asterisk/module.h"
#include "asterisk/strings.h"

You’ll also want to include the file header for the code you are testing. “test.h” contains functions and definitions for the unit test framework. “strings.h” contains some useful string utility functions and is shown here since “test_example.c” needs it. However, depending the test your file may or may not need it. Since these particular tests are going to be contained within their very own module, “module.h” is included as well. While we are on the topic, let’s go ahead and declare the Asterisk module definition and load/unload function handlers. We’ll put these at the bottom of the file:

static int load_module(void)
{
    return AST_MODULE_LOAD_SUCCESS;
}

static int unload_module(void)
{
    return 0;
}

AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Fabulous test example");

Creating the Unit Test

We are now ready to start creating/defining our unit test(s). Luckily there are some handy macros and functions to help with this (this is a framework right?). To start, we’ll declare the unit test (above the module load/unload functions) and we’ll call it fabulous_fun_results using the following macro:

AST_TEST_DEFINE(fabulous_fun_results)
{
}

The macro expands to the following function definition:

static enum ast_test_result_state hdr(struct ast_test_info *info, enum ast_test_command cmd, struct ast_test *test)

That’s somewhat good to know since each unit test will be making use of those parameters. For instance ast_test_info will contain the required initialization and test description information and the cmd parameter is used to determine the test’s execution context (Meaning, when the test is called is it being called in order to initialize the test and test information or is it being called to execute the test itself).

Initialization

It is crucial that each test be initialized with appropriate test information. This is used by Asterisk to categorize and describe the test at runtime. This needs to be done at the start of the test function and usually look something like the following:

switch (cmd) {
    case TEST_INIT:
        info->name = __func__;
        info->category = “/main/fabulous”;
        info->summary = "Fabulous functionality test";
        info->description = “A nice description of my fabulous test and what it is doing”;
        return AST_TEST_NOT_RUN;
    case TEST_EXECUTE:
        break;
}

The info attributes are fairly self explanatory, but to add just a bit of flavor to them:

  • name – The name of this unit test (usually the same as the function name, but can be any string value)
  • category – The group of tests this test falls under (this is how we’ll find/run the test from the CLI)
  • summary A short description of the test
  • description – A longer description describing what the test does.

Since in this case, TEST_INIT, the test is not being run, but initialized it needs to always return AST_TEST_NOT_RUN . That being said the only other option is TEST_EXECUTE. For that we simply continue on, and what follows is the test execution block itself.

Execution Block

Yay we have finally arrived. Time to write some actual test code. Some tests of course can become quite complicated in and of themselves, however we’ll keep it simple for now and use the test framework once again to help us. Usually for a test we’ll want to check a condition and then either pass (return AST_TEST_SUCCESS ) the test, continue testing more conditions, or fail (return AST_TEST_FAIL ) the test.

Our function under test has several return values, so we’ll check its return conditions in this one test (Note, for each condition we could write a separate unit test and sometimes that makes sense to do. Use your discretion to decide when creating tests). There is a handy macro we can use to check our condition(s) and fail the test if the condition is not met:

ast_test_validate(test, condition, …)

test is most always the test object passed into our unit test. condition is the predicate that’s checked and if it evaluates to false then a failure message is printed and the test automatically fails, stopping execution. DON’T FORGET this is ‘C’! The ast_test_validate macro returns from the current test. If any memory has been previously allocated we’ll have a memory leak. For this reason I suggest using a RAII_VAR for memory allocated variables within the unit test. Alright, didn’t I say something about writing some test code? Okay this time I mean it. Let’s write some validation code to test our function’s return values for various inputs.

RAII_VAR(char *, str, NULL, ast_free);

if (!(str = ast_malloc(16))) {
    return AST_TEST_FAIL;
}

ast_copy_string(str, "surreal", 8);
ast_test_validate(test, fabulous_fun("terrific") == F_TERRIFIC);
ast_test_validate(test, fabulous_fun(str) == F_SURREAL);
ast_test_validate(test, fabulous_fun("wild") == F_WILD);

return AST_TEST_PASS;

Again if any of those test conditionals fail the test will fail. We allocated some memory (silly for this, but hey it’s an example), but we used a RAII_VAR, so we don’t have to worry about freeing the memory ourselves. When the function exits ast_free is automatically called on the variable.

Register/Unregister the Test

The test itself needs to be registered at module load time. Like any good code it needs to also be cleaned up and unregistered. This can be done at unload time. Remember the load and unload functions we defined earlier? Now we want to add to those. Add a test register to the load_module function:

static int load_module(void)
{
	AST_TEST_REGISTER(fabulous_fun_results);
	return AST_MODULE_LOAD_SUCCESS;
}

And add a test unregister to the unload_module function:

static int unload_module(void)
{
	AST_TEST_UNREGISTER(fabulous_fun_results);
	return 0;
}

We’re done! Well not quite. We need to compile and run the test of course and make sure it works and passes.

Running the Test

Compile and install Asterisk if it is not already. Make sure Asterisk has been configured with dev-mode enabled. We’ll also need to have selected the TEST_FRAMEWORK  option within menuselect that’s found beneath the “Compiler Flags – Development” section. Still in menuselect, make sure our test is selected under the ‘Test Modules’ section too.

asterisk $ ./configure –enable-dev-mode
asterisk $ make menuselect/menuselect menuselect-tree menuselect.makeopt
asterisk $ menuselect/menuselect --enable TEST_FRAMEWORK --enable test_example menuselect.makeopts
asterisk $ make && make install

Now run Asterisk (we’ll use the ‘-c’ option to run Asterisk in console mode):

asterisk $ asterisk -c

We can now execute the test from the Asterisk CLI (remember the category?):

*CLI> test execute category /main/fabulous

After hitting enter we should see something like the following:

Running all available tests matching category /main/fabulous
START /main/fabulous - fabulous_fun_results
[test_example.c:fabulous_fun_results:143]: Condition failed: fabulous_fun(str) == F_SURREAL
END /main/fabulous - fabulous_fun_results Time: <1ms Result: FAIL

1 Test(s) Executed 0 Passed 1 Failed

Ugh our test failed. That means either there is a bug in the code under test (yay the test is doing its job), or the unit test itself has a bug. We’re too fancy to have bugs in our actual code right? So let’s assume the test is as fault. After going back and looking we can see the test does indeed have a problem. While ‘surreal’ is ‘7’ characters long we forgot to figure in the NULL terminator. Changing it to an ‘8’ should fix the problem. Recompile, start Asterisk and execute the test again. This time we should see the following:

*CLI> test execute category /main/fabulous
Running all available tests matching category /main/fabulous
START /main/fabulous - fabulous_fun_results
END /main/fabulous - fabulous_fun_results Time: <1ms Result: PASS

1 Test(s) Executed 1 Passed 0 Failed

Yay success! My stuff works. My code is tested. Ship it, we’re done!

What else?

Well yes and no. Indeed the test has passed, but at this point we can ask ourselves a few questions. For example: Does my unit test cover all that it needs to? Are there other test cases I could write. Have I covered all paths (nominal and off nominal) in my code? The answers to these and other questions can be complicated of course. It may be too hard or even impossible to test all paths for instance.

Another thing to consider, while this unit test is meant to test the code directly, and apart from the rest of the system, how will this code work in an integrated environment? That however is outside the scope of unit testing and gets into integration testing. Don’t fret too much though because Asterisk also has an integration testing framework too that you can use to test your changes/features with regards to the overall system or subsytem. That however is a topic for another time.

For the most part that is the basics for writing a unit test in Asterisk. If you haven’t already I’d suggest checking out ‘test.h’ found in the Asterisk source under the ‘includes/asterisk/’ directory for more information and other framework options. Also check out some of the other tests that can be found under the ‘tests’ directory as well as experiment with other CLI options with regards to unit testing (all associated commands start with ‘test’). Lastly, If you see something in the unit test framework that needs to be fixed or can be improved upon well we are in luck as this is open source!

 

Leave a Reply

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


The reCAPTCHA verification period has expired. Please reload the page.

About the Author

What can we help you find?