Getting Started with Unit Testing
It was alluded to in the last section that there are multiple kinds of software testing which you may utilize in a development lifecycle. Naturally, this might lead to the question of where to start with testing. We think a good place to start is with unit testing. By learning how to test software at a granular level, you will begin to recognize techniques and tools that you can use to make your software more robust and less buggy :)!
When looking at more complex projects, we can observe that they are comprised of multiple subsystems, libraries, and components each with own set of functions. Unit tests would be targeting the indivudal functions for each of those components.
What is Unit Testing
Unit testing in a general sense is the act of testing a small pieces or functions of code (agianst a requirement). That definition may seem a bit ambiguous or vague but it is surprisingly accurate.
In simpler terms, the intent of unit testing is checking to see if a singular piece of code matches the expected behavior.
Note
Keep the idea about expected behavior in your memory. It will come up again later.
Why use Unit Testing
Advantages/Disadvantages of Unit Testing
Note
Sometimes unit testing gets confused with another paradigm called module testing which is extremely similar. For the most part, module tests are a series/collection of unit tests for a particularl subsystem or library.
Terminology
Let's start getting into the technical details. To start with we are going to be introuducing some terminology.
| Term | Definition |
|---|---|
| Test case | Code that evaluates the feature or module to be tested under certain conditions and settings. |
| Test harness | A software package that allows for the creation and evaluation of test cases as well as produce corresponding artifacts such as success/failure reports. |
| Test fixture | Code that establishes the isolated environment that allows for safe and fast execution of test cases. |
Test Frameworks
There is no shortage of testing frameworks. For the sake of breiviety, we shall only list a few but feel free to refer this list if you want to explore options not presented here.
| Test Framework | Description | Homepage/Documentation |
|---|---|---|
| Google Test (GTest) | Google's C++ testing framework designed to be portable with usage across the three major operating systems and multiple emebedded target architectures. GTest also is packaged with GMock which gives GTest built-in ability to work with mock objects. | GitHub |
| Unity | Some description | Unity Test Framework Homepage |
| CMocka | Some description | Cmocka Homepage |
Note About Integrating w/ Build Systems
Part of the resistance or deterrant to testing is simply the inconvience of it. To some extent that is unavoidable but having good integration with your build system or development environment can make creating, running, and evaluating test cases/suites almost seamless. In essence the goal is to automate the process of testing to where running a test suite is as simple as pressing a button or entering a short command into your terminal environment.
How easy a tool is to use somewhat subject and will change between developers but **the point is that you should strive to get your testing environment to a place where it's easy & quick to build, run, and get test results back in your desired format. **
Deriving Test Cases
There are multiple methodologies when coming up with test cases. An easy approach however is to define or evaluate requirements or constraints for your project and then base your test cases off them.
You may wonder why we need to worry about requirements before writing our test cases. Having the requirements will explicitly define the critieria needed for our test case. In other words how do we know if a test passes if we don't define the conditions in which it would pass or fail. While doing this ultimatetly takes more time, it makes the process of writing test cases much easier.
To paint the picture let's consider a very simple function. Regardless of the language/implementation let's say you wanted to implement a function that takes some string as an input and returns the length of the string.
1. Example Requirements
We do not need an extensive list but each requirement defined should be a testable item. The point of this step is to define as much of the behavior of your function or system as possible! The expected behavior is exactly what we are looking for to test against.
Note
Ideally, each requirement or constraint should focus on essential features/functions of your function or system.
| Req ID | Description |
|---|---|
| REQ-001 | The function shall return the length of the string specified by the input argument |
| REQ-002 | The function shall only accept alpha numeric characters (i.e a-z & 0-9) |
| REQ-003 | If an invalid character is detected, the function shall return a -1 indidicating an error |
| REQ-004 | If an empty string is passed into the function, the function shall return 0. |
2. Writing the Test Case
Once you have your requirements and or expected behavior figured out, all that is left to do is to write the test cases. Typically for each requirement there will be at least a single test case. So if you had defined 10 requirements, you would expect at least 10 test cases.
Using requirement REQ-002 as an example, "The function shall only accept alpha numeric characters", there are two initial test cases that comes to mind:
-
Testing the function with a string that only contains alphanumeric characters
-
Testing the function with a string that has special characters
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
/* Function to be tested */
int string_length(const char *str) {
if (!str) return 0;
for (size_t i = 0; str[i] != '\0'; i++) {
if (!isalnum((unsigned char)str[i])) {
return -1;
}
}
return strlen(str);
}
/* Test for a valid alphanumeric string */
static void test_string_length_valid(void **state) {
(void) state; /* Unused parameter */
assert_int_equal(string_length("hello123"), 8);
}
/* Test for a string with special characters */
static void test_string_length_special_chars(void **state) {
(void) state; /* Unused parameter */
assert_int_equal(string_length("hello@123"), -1);
}
/* Main function to run tests */
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_string_length_valid),
cmocka_unit_test(test_string_length_special_chars),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
We will talk about the structure of unit test anmd some good practices in the next guide.