doc/tap_test_guide.md
This guide explains how to write tests for ProxySQL using the TAP (Test Anything Protocol) framework.
ProxySQL uses the TAP framework for both unit and integration tests. All tests reside in the test/tap/tests/ directory. Tests are distinguished by their naming convention:
unit- (e.g., unit-strip_schema_from_query-t.cpp)admin-listen_on_unix-t.cpp, test_firewall-t.cpp)This naming convention helps identify the test type at a glance and allows for easy filtering when running specific test suites.
TAP (Test Anything Protocol) is a simple text-based interface between testing modules and test harnesses. It provides a standardized way to communicate test results, making it language-agnostic and easy to integrate with various testing tools.
plan(int count)Declares how many tests you plan to run. This should be called once at the beginning of your test.
plan(15); // Planning to run 15 tests
ok(int pass, const char *fmt, ...)Reports a test result. The first argument is the pass/fail condition, followed by a printf-style message.
ok(result == expected, "Test description: %s", details);
diag(const char *fmt, ...)Prints diagnostic messages to stderr that don't count as tests. Useful for debugging and showing test progress.
diag("Debug info: value = %d", value);
skip(int how_many, const char *reason, ...)Skips a specified number of tests with a reason.
if (!feature_available) {
skip(3, "Feature not available in this build");
}
exit_status(void)Returns the appropriate exit code based on test results. Should be called at the end of your main function.
return exit_status();
1..15
ok 1 - Basic schema stripping: 'SELECT * FROM stats_mysql_query_digest'
ok 2 - Multiple schema references stripped
not ok 3 - Edge case handling
# Diagnostic message explaining failure
ok 4 - NULL input handled safely
...
ok 15 - Case insensitive schema match
1..N: Test plan (N tests expected)ok N - description: Test N passednot ok N - description: Test N failed#: Diagnostic messages0: All tests passed1: One or more tests failed255: Test suite bailed out (critical failure)Unit tests verify isolated functions without external dependencies. They test pure logic, data structures, and algorithms.
#include <stdlib.h>
#include "tap.h"
#include "unit_test.h" // Common unit test header
#include "gen_utils.h" // Header with function to test
// ... other project headers as needed
using std::string;
// ... other using declarations
int main(int argc, char** argv) {
plan(N); // N = number of tests
// Define and execute tests
return exit_status();
}
ok() callsHere's a complete example using table-driven testing:
#include <stdlib.h>
#include "tap.h"
#include "unit_test.h"
#include "gen_utils.h"
using std::string;
using std::vector;
struct Args {
const char* query;
const char* schema;
vector<string> tables;
bool ansi_quotes = false;
};
struct TestCase {
const char* name;
Args args;
const char* expected;
};
int main(int argc, char** argv) {
TestCase test_table[] = {
{
"Basic schema stripping",
{
"SELECT * FROM stats.stats_mysql_query_digest",
"stats",
},
"SELECT * FROM stats_mysql_query_digest",
},
{
"Quoted identifiers",
{
"SELECT * FROM `stats`.`table1`",
"stats",
},
"SELECT * FROM `table1`",
},
{
"NULL query handled safely",
{
nullptr,
"stats",
},
"",
},
{
"String literals preserved",
{
"SELECT * FROM stats.t WHERE x='stats.y'",
"stats",
},
"SELECT * FROM t WHERE x='stats.y'",
},
};
int num_tests = sizeof(test_table) / sizeof(test_table[0]);
plan(num_tests);
for (int i = 0; i < num_tests; i++) {
TestCase& tc = test_table[i];
string result = strip_schema_from_query(tc.args.query, tc.args.schema, tc.args.tables, tc.args.ansi_quotes);
ok(result == tc.expected, "%s: '%s'", tc.name, result.c_str());
}
return exit_status();
}
Integration tests verify ProxySQL's runtime behavior by interacting with running instances. They test features like query routing, connection pooling, firewall rules, and cluster synchronization.
A typical integration test file has the following structure:
Includes: Essential headers
"tap.h": The core TAP library for test reporting"command_line.h": Helper for reading connection parameters from environment variables"utils.h": Provides utility functions and macros like MYSQL_QUERY"mysql.h" for MySQL or "libpq-fe.h" for PostgreSQL tests<string>, <vector>, <chrono>, etc.)main() Function: The entry point for the test
plan(N): Declare how many tests you plan to run. N is the total number of ok() callsCommandLine cl;: Object to manage command-line and environment variables. cl.getEnv() reads the necessary configurationok()return exit_status();: Return the overall test statusA test should not depend on the state left behind by other tests.
// test_firewall-t.cpp
MYSQL_QUERY(mysqladmin, "delete from mysql_firewall_whitelist_users");
MYSQL_QUERY(mysqladmin, "delete from mysql_firewall_whitelist_rules");
MYSQL_QUERY(mysqladmin, "load mysql firewall to runtime");
// test_firewall-t.cpp
MYSQL_QUERY(mysqladmin, "load mysql variables from disk");
MYSQL_QUERY(mysqladmin, "load mysql variables to runtime");
The most reliable way to check ProxySQL's internal state is by querying the stats and runtime tables.
stats_mysql_connection_pool.
// test_connection_annotation-t.cpp
MYSQL_QUERY(proxysql_admin, "SELECT ConnUsed, ConnFree FROM stats.stats_mysql_connection_pool WHERE hostgroup=1");
// ... compare results before and after
stats_mysql_query_digest.
// test_query_cache_soft_ttl_pct-t.cpp
const string STATS_QUERY_DIGEST =
"SELECT hostgroup, SUM(count_star) FROM stats_mysql_query_digest "
"WHERE digest_text = 'SELECT SLEEP(?)' GROUP BY hostgroup";
Many operations in ProxySQL are asynchronous (e.g., connection killing, cluster synchronization). Your test must account for this.
sleep().
// test_cluster1-t.cpp
int module_in_sync(...) {
while (i < num_retries && rc != 1) {
// ... query stats_proxysql_servers_checksums and check if all nodes have the same checksum ...
sleep(1);
i++;
}
return (rc == 1 ? 0 : 1); // Return 0 on success
}
sleep() can be used.
// kill_connection-t.cpp
std::string s = "KILL CONNECTION " + std::to_string(mythreadid[j]);
MYSQL_QUERY(mysql, s.c_str());
sleep(1); // Give ProxySQL a moment to process the kill
int rc = run_q(other_mysql_conn, "DO 1");
ok(rc != 0, "Connection should be killed");
For complex or repetitive tasks, use helpers to make your test more readable and maintainable.
test_cluster1-t.cpp test defines trigger_sync_and_check to encapsulate the entire logic for testing one module's synchronization.pgsql-basic_tests-t.cpp test defines a PQEXEC macro to wrap PQexec and add error checking, reducing boilerplate.For features that require multiple steps or scenarios, break the test into smaller functions.
pgsql-basic_tests-t.cpp has separate functions like test_simple_query, test_insert_query, test_transaction_commit, etc. This makes it easy to see what is being tested and to debug failures.reg_test_3223-restapi_return_codes-t.cpp test defines its test cases in data structures (std::vector of structs) and then iterates over them. This table-driven approach is excellent for testing many variations of an input.Here is a basic template to get you started:
#include <string>
#include <vector>
#include <cstdio>
#include "mysql.h"
#include "tap.h"
#include "command_line.h"
#include "utils.h"
int main(int argc, char** argv) {
// 1. Declare the number of tests you will run.
plan(3);
CommandLine cl;
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return exit_status();
}
// 2. Establish connections.
MYSQL* admin = mysql_init(NULL);
if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) {
diag("Failed to connect to admin interface: %s", mysql_error(admin));
return exit_status();
}
MYSQL* client = mysql_init(NULL);
if (!mysql_real_connect(client, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) {
diag("Failed to connect to client interface: %s", mysql_error(client));
mysql_close(admin);
return exit_status();
}
// 3. Arrange: Set up the test environment.
diag("Setting up test: creating a new query rule.");
MYSQL_QUERY(admin, "DELETE FROM mysql_query_rules WHERE rule_id=999");
MYSQL_QUERY(admin, "INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup) VALUES (999, 1, '^SELECT 123', 1)");
MYSQL_QUERY(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");
// 4. Act & Assert: Run the test and verify the outcome.
diag("Running a query that should match the rule.");
int rc = mysql_query(client, "SELECT 123");
ok(rc == 0, "Query 'SELECT 123' should execute successfully.");
// Verify state via statistics
MYSQL_QUERY(admin, "SELECT hits FROM stats_mysql_query_rules WHERE rule_id=999");
MYSQL_RES* res = mysql_store_result(admin);
ok(res && mysql_num_rows(res) == 1, "Rule 999 should exist in stats.");
if (res && mysql_num_rows(res) == 1) {
MYSQL_ROW row = mysql_fetch_row(res);
ok(atoi(row[0]) == 1, "Rule 999 should have exactly 1 hit.");
}
mysql_free_result(res);
// 5. Cleanup
diag("Cleaning up test rule.");
MYSQL_QUERY(admin, "DELETE FROM mysql_query_rules WHERE rule_id=999");
MYSQL_QUERY(admin, "LOAD MYSQL QUERY RULES TO RUNTIME");
// 6. Close connections and exit.
mysql_close(admin);
mysql_close(client);
return exit_status();
}
ProxySQL provides make targets for building TAP tests:
For Release Builds:
make build_tap_tests
For Debug Builds:
make build_tap_tests_debug
The build process flows through multiple Makefiles:
Makefile (root)
└─> test/tap/Makefile
├─> test/tap/tap/Makefile (builds libtap.so)
└─> test/tap/tests/Makefile (compiles test executables)
build_tap_tests and build_tap_tests_debug targetsAfter building, run the test executable directly:
./test/tap/tests/unit-strip_schema_from_query-t
./test/tap/tests/test_firewall-t
All TAP tests must be registered in test/tap/groups/groups.json to be executed in CI. This requirement applies to both unit tests and integration tests.
The CI system uses groups.json to:
For example:
legacy-g1, legacy-g2, legacy-g3: Multiple instances running integration tests in parallelmysql84-g1, mysql90-g1, mysql-multiplexing=false-g1: Infrastructures with different MySQL versions and ProxySQL configuration.unit-tests-g1: Self-contained unit tests that don't require external infrastructureThe -g1, -g2, -g3 suffixes represent different parallel execution instances. They share the same base infrastructure configuration but run independently to speed up CI execution.
Important: If you add a new test but don't register it in groups.json, this will cause CI failure.
For unit tests that don't require external infrastructure:
{
"unit-your_function_name-t": ["unit-tests-g1"]
}
For integration tests that require a running ProxySQL instance and backend databases:
{
"test_your_feature-t": ["legacy-g1"]
}
If you are new to this project, you can assign tests to any of the default groups (legacy-g1, legacy-g2, legacy-g3, etc.). The distribution does not need to be perfectly balanced—the CI system will handle the workload. Additionally, ProxySQL maintainers periodically rearrange tests and improve group balance.
plan(): Always declare your test count. For table-driven tests, use plan(num_tests) where num_tests is the size of the test tableplan(N) matches actual number of ok() callsexit_status(): Always return exit_status() from maindiag() to add context when debugging test failures| Aspect | Unit Test | Integration Test |
|---|---|---|
| Purpose | Test isolated functions/logic | Test ProxySQL runtime behavior |
| Dependencies | None (pure function testing) | Requires running ProxySQL instance |
| Use Cases | String parsing, data structures, algorithms | Query routing, connection pooling, firewall rules |
| File naming | unit-*-t.cpp | *-t.cpp |
| Includes | unit_test.h, function headers | command_line.h, utils.h, mysql.h |
| Connections | No external connections | MySQL/PostgreSQL connections to ProxySQL |
| Setup | None or minimal | Delete/insert config, load to runtime |
| Test pattern | Table-driven | Arrange-Act-Assert |
| Verification | Direct return value comparison | Query stats tables via admin interface |
| Cleanup | Usually not needed | Restore config, reload from disk |
| Examples | unit-strip_schema_from_query-t.cpp | test_firewall-t.cpp, test_cluster1-t.cpp |
For more examples, examine existing tests in the test/tap/tests/ directory.