Test Units
Test Unit Packaging
Individual Tests
Individual test are implemented as test functions or methods which follow a four-phase testing pattern:
- Setting up a test fixture (optional)
- Exercising the System Under Test (SUT)
- Verifying that the expected outcome has occurred (typically using calls to Test Macros or Test Points)
- Tearing down the test fixture (optional)
Test Units
Individual functions or methods, which typically implement a single test case are grouped into one or more Test Units which are executed as atomic entities.
Grouping of individual tests into a Test Unit can be accomplished in any of three ways:
- A Test Unit can be comprised of the member functions of a C++ class,
- A Test Unit can be comprised of a set of C functions,
- A Test Unit can be comprised of C functions pointed to by members of a C struct
The following table compares the three packaging variants.
Test Unit Type | Advantages | Disadvantages | scl pragma |
---|---|---|---|
C++ class |
|
|
scl_test_class |
C class |
|
|
scl_test_cclass |
C functions |
|
|
scl_test_flist |
The best choice is usually the C++ class since it offers the best mix of features and ease-of-use. (You can test code written in C or C++ using the C++ class test units.) However, compiling C++ is not always possible, in this case one of the C-based test unit packaging options must be used.
You can freely mix different deployment methods across a project if desired, the format of the results is consistent across all test unit packaging options.
Recommendations
STRIDE supports a heterogeneous mix of test unit types, and it's not difficult to refactor the packaging style of a set of tests. However as you plan your testing strategy and tactics, keep these recommendations in mind:
- Where possible, use the C++ class packaging. This provides the most developer benefits and is easiest to use.
- If the C++ packaging is not possible, consider the C class packaging over the function list packaging. The C Class syntax is more involved (you have to explicitly carry around the "this" pointer to the structure), but the benefits of automatic fixturing and state encapsulation outweigh the heavier syntax.
All of the STRIDE pragmas for specifying test units are described here
Results Reporting Model
The STRIDE Framework organizes your testing activity in containers defined by Test Units made up of one or more Test Methods (or Functions). By default, each Test Method corresponds to a single Test Case, although test methods can also be used to create dynamic test cases using the STRIDE Runtime.
An understanding of these entities and the role that each plays is essential to creating and maintaining useful test assets.
Test Methods
Test methods are the test cases that comprise a Test Unit. When using Test Classes (c++), these are just all public functions that are not otherwise declared as fixture methods. For Test C-Class and Test FList units, the methods are similarly bound by membership in the structure (C-Class) or by explicit inclusion in the scl pragma declaration (FList).
Each Test Method by default maps to a single Test Case in the results. When relying on this default behavior of Test Methods, we refer to these methods as static Test Cases.
A less common technique is to create and add a test case dynamically at runtime to an existing Suite via the STRIDE API. We refer to these as dynamic Test Cases.
In the STRIDE Framework, every test case belongs to exactly one Test Unit. Less commonly, a Test Case may belong to a dynamically-created Suite if the Test Case is itself dynamically created. Further, each test case must have a Name and a Status, which are expressed as attributes of the test case. STRIDE assigns default values to these attributes, which may be changed at runtime. Other optional attributes can be set on the test case as well as shown in the table below.
Typically, you will set Test Case attributes via STRIDE Macros that simplify the interaction with the Test Case, the most common of these are Test Code Macros - to set the Test Case status.
Specifying Pass/Fail
There are several ways to indicate pass/fail status in a test method. The most common is to use a Stride test macro, but other options are available. See Indicating Pass/Fail in a Test Method for more details.
Test Units
A Test Unit is a set of Test Cases that is always run together. If fixturing is used, all Test Cases within a Test Unit also share the same fixturing methods. A Test Unit typically holds a set of logically-related tests that target the testing of a specific component or subsystem.
For a C++ class-based or C class-based Test Unit, the Test Unit maps directly to a source code entity. For the free function-based Test Unit, the Test Unit is synthesized by the STRIDE Runtime.
Following are the types of Test Units supported by STRIDE.
- C++ Class
- If a class is declared a scl_test_class all member functions of the class that meet certain criteria become member Test Cases of the Test Unit.
- C Class (using a structure)
- If a structure is declared a scl_test_cclass all function pointer members of the structure become member Test Cases of the Test Unit. (The structure must meet certain criteria to be a Test Unit).
- Free Function
- One or more free functions that meet certain criteria can be grouped into a synthesized Test Unit using the scl_test_flist pragma.
At runtime, STRIDE creates a Suite for each Test Unit that is run, thus providing a Suite context for each member Test Case.
For more on the internal reporting model, see here
Parameterized Test Units
You can pass parameters to Test Units using a command line option of the Runner. This allows you to provide input to a test during runtime.
For more on passing parameters to Test Units, see here
Simple Test Unit Examples
Following are a few short examples. In each example, a single test unit with the name "MyTest" is identified to the STRIDE compiler via a custom special test pragma.
Test Unit as C++ Class
MyTest.h
#include <srtest.h>
class MyTest : public stride::srTest
{
public:
void ExpectPass()
{
srNOTE_INFO("this test should pass");
srEXPECT_EQ(2 + 2, 4);
}
void ExpectFail()
{
srNOTE_INFO("this test should fail");
srEXPECT_GT(2 * 3, 7);
}
};
#ifdef _SCL
// this pragma identifies MyTest as a test class to the STRIDE compiler
#pragma scl_test_class(MyTest)
#endif
Test Unit as C Class
MyTest.h
#include <srtest.h>
typedef struct MyTest
{
void (*ExpectPass)(struct MyTest* self);
void (*ExpectFail)(struct MyTest* self);
} MyTest;
void MyTest_Init(MyTest* self);
#ifdef _SCL
// This pragma identifies MyTest as a test c class to the STRIDE compiler.
// Extra instrumentation code will be generated to call MyTest_Init() before
// tests are run.
#pragma scl_test_cclass(MyTest, MyTest_Init)
#endif
MyTest.c
#include "MyTest.h"
static void ExpectPass(MyTest* self)
{
srNOTE_INFO("this test should pass");
srEXPECT_EQ(2 + 2, 4);
}
static void ExpectFail(MyTest* self)
{
srNOTE_INFO("this test should fail");
srEXPECT_GT(2 * 3, 7);
}
void MyTest_Init(MyTest* self)
{
self->ExpectPass = ExpectPass;
self->ExpectFail = ExpectFail;
}
Test Unit as Group of Free Functions
MyTest.h
#include <srtest.h>
void ExpectPass();
void ExpectFail();
#ifdef _SCL
// this pragma identifies MyTest as a test unit to the STRIDE compiler, specifying
// the four functions as members of the test unit
#pragma scl_test_flist("MyTest", ExpectPass, ExpectFail, ChangeMyName, ChangeMyDescription)
#endif
MyTest.c
#include "MyTest.h"
void ExpectPass()
{
srNOTE_INFO("this test should pass");
srEXPECT_EQ(2 + 2, 4);
}
void ExpectFail()
{
srNOTE_INFO("this test should fail");
srEXPECT_GT(2 * 3, 7);
}
Integrating Test Units Into Your Target Build
STRIDE Test Units are easily integrated into your target build since all required test harnessing code is automatically generated based on header files that include STRIDE Test pragmas.
This harnessing code (referred to as Intercept Module, or IM code) is responsible for
- Communicating with the I/O portion of the STRIDE target runtime
- Instantiating each specified Test Unit
- Running each member test of the Test Unit
- Collecting test output
Harnessing code generation is the responsibility of the STRIDE Build Tools. The useful artifacts created by the build tools comprise the STRIDE database (xx.sidb), and the IM source (strideIM.c/cpp, strideIM.h, and strideIMEntry.h).
To build a fully-instrumented target:
- Several statements are added to your applications main() function (or equivalent) to start and stop the STRIDE I/O and IM threads
- The generated IM source files are compiled and linked with your target application
- The STRIDE library (which provides I/O and common services) is also linked with your application.
Running Target-Based Tests
After configuring TCP/IP or Serial port communication parameters, tests are controlled and run from a remote host computer. See Stride Runner for details.