- Silmused Overview
A comprehensive guide to the Silmused PostgreSQL testing framework.
Silmused is a Python-based testing framework designed to automatically validate PostgreSQL databases and SQL queries. It provides a comprehensive suite of test classes that can verify database structures, data integrity, constraints, functions, procedures, triggers, views, indexes, and query results.
Silmused supports two main testing modes:
-
Database Tests - Validates the structure and content of a PostgreSQL database:
- Table and view structure (columns, types, constraints)
- Data content and correctness
- Constraints (primary keys, foreign keys, unique, check)
- Functions and procedures
- Triggers
- Indexes
-
Query Tests - Validates SQL query results:
- Query structure (columns present/absent)
- Query data (row counts, specific values, value ranges)
- Result ordering (using
test_idcolumn)
- Python: 3.12
- psycopg2: 2.9.9
- Silmused: 1.4.6
Silmused is built around three main components:
-
Runner - The orchestration engine that:
- Creates temporary test databases
- Executes test suites
- Formats results into JSON output
- Manages database connections
-
TestDefinition - The base class for all test types that:
- Provides common parameters and behavior
- Handles error catching and feedback generation
- Manages test execution lifecycle
-
Translator - The internationalization system that:
- Provides multi-language feedback messages
- Supports template-based message formatting
- Currently supports English and Estonian
These test classes inherit from TestDefinition and validate database structures:
StructureTest- Tests table/view structureDataTest- Tests table/view data contentConstraintTest- Tests table/column constraintsFunctionTest- Tests database functionsProcedureTest- Tests stored proceduresViewTest- Tests views and materialized viewsIndexTest- Tests database indexesTriggerTest- Tests database triggers
These test classes validate SQL query results:
QueryStructureTest- Tests query result structure (columns)QueryDataTest- Tests query result data
Important: Query tests use a special workflow where the input SQL query is executed and results are stored in a temporary table named query_test with an additional test_id column for row ordering.
Three supporting classes provide organizational and execution capabilities:
- ChecksLayer - Groups related tests together with a shared title
- ExecuteLayer - Executes SQL queries between tests (useful for trigger testing)
- TitleLayer - Adds section titles to test output
These classes do not inherit from TestDefinition as they serve organizational purposes rather than testing functionality.
The Runner class is the primary interface for executing tests. It handles database creation, test execution, and result formatting.
Runner(
backup_file_path, # Required: Path to SQL dump or SQL script
tests, # Required: List of test objects
lang='en', # Language for feedback ('en' or 'et')
test_name='', # Optional name for test database
db_user='postgres', # PostgreSQL username
db_host='localhost', # Database host
db_password='postgres', # Database password
db_port='5432', # Database port
test_query='test', # 'test' for database tests, 'query' for query tests
query_sql='', # SQL query string (for query tests)
encoding=None # File encoding (e.g., 'UTF-8')
)The Runner automatically:
- Creates a randomly-named database (format:
db_{test_name}_{filename}_{uuid}) - Accepts both
pg_dumpbinary files and SQL scripts (INSERT statements) - Validates file format before processing
- Handles encoding issues (including PostgreSQL 17.6+
\restrictcommands)
get_results()- Returns JSON-formatted test results in OK_V3 format_run_tests()- Executes all tests in sequence_create_db_from_psql_dump()- Creates database from pg_dump file_create_db_from_psql_insert()- Creates database from SQL script_create_query_view()- Creates temporary query_test table for query tests
The base class for all test types (except Layers). Provides common functionality:
run(cursor)- Main entry point that executes the test and handles errorsexecute(cursor)- Abstract method implemented by each test classresponse(is_success, message_success, message_failure, points, is_sys_fail)- Formats test response
TestDefinition automatically handles common SQL errors:
UndefinedColumn- Column doesn't existUndefinedTable- Table doesn't existAmbiguousColumn- Column reference is ambiguousUndefinedFunction- Function doesn't existIndexError- No result found
These errors are caught and formatted into user-friendly feedback messages.
Tests table/view structure using information_schema.columns.
Key Features:
- Tests table/view existence
- Tests column existence
- Tests column data types
- Tests column maximum length (for varchar)
Supported Types:
'integer'- Matches: tinyint, smallint, mediumint, int, bigint, integer'float'- Matches: float, double, decimal'varchar'- Matches: character varying'text'- Matches: text'boolean'- Matches: boolean
Tests table/view data content using direct SQL queries.
Key Features:
- Tests data existence
- Tests exact values
- Tests value ranges (for numbers)
- Tests value lists (for strings)
- Tests NULL values
- Supports WHERE clauses
- Supports JOIN clauses (INNER JOIN)
Tests table/column constraints using information_schema.table_constraints and information_schema.key_column_usage.
Key Features:
- Tests constraint existence
- Tests constraint types (PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK)
- Tests constraint names
- Tests multi-column constraints
Tests database functions using pg_catalog.pg_proc and information_schema.routines.
Key Features:
- Tests function existence
- Tests function type (FUNCTION vs PROCEDURE)
- Tests function parameter count
- Tests function return values
- Tests function result count
- Supports function arguments
Tests stored procedures using similar approach to FunctionTest.
Key Features:
- Tests procedure existence
- Tests procedure type
- Tests procedure parameter count
- Tests procedure result count via
after_query - Supports
pre_queryfor setup - Requires
after_queryto verify results
Tests views using information_schema.columns and pg_matviews.
Key Features:
- Tests view existence
- Tests view columns
- Tests materialized views (via
isMaterialized=True)
Tests database indexes using pg_indexes.
Key Features:
- Tests index existence by name
Tests database triggers using information_schema.triggers.
Key Features:
- Tests trigger existence
- Tests trigger event manipulation (INSERT, UPDATE, DELETE)
- Tests trigger action timing (BEFORE, AFTER)
- Often used with
ExecuteLayerfor setup
Tests query result structure (columns) using information_schema.columns on the query_test table.
Key Features:
- Tests column existence in query results
- Tests column absence (via
should_exist=False)
Tests query result data using direct SQL queries on the query_test table.
Key Features:
- Tests row counts
- Tests specific values (using
test_idfor row ordering) - Tests value ranges
- Tests value lists
- Supports WHERE clauses
Groups related tests together with a shared title. Results are nested in the output JSON.
Parameters:
title- Title for the test grouptests- List of test objects
Example:
ChecksLayer(
title='Users table validation',
tests=[
StructureTest(name='users', points=10),
DataTest(name='users', column_name='COUNT(*)', expected_value=100, points=20)
]
)Executes SQL queries between tests. Useful for preparing data before trigger tests.
Parameters:
query- SQL query to execute
Example:
ExecuteLayer("INSERT INTO users (email) VALUES ('test@example.com')")Adds section titles to test output. Appears as a message in results.
Parameters:
title- Title string
Example:
TitleLayer('Database Structure Tests')The Translator class provides internationalization support for feedback messages.
Features:
- Loads translation files from
silmused/locale/ - Supports JSON-based translation files
- Template-based message formatting with parameter substitution
- Currently supports: English (
en.json) and Estonian (et.json)
Translation Structure:
- Organized by test type (e.g.,
structure_test,data_test) - Each test type has test keys (e.g.,
table_should_exist_positive_feedback) - Messages support up to 5 parameters (
$param1through$param5)
All test classes inherit common parameters from TestDefinition. The following table describes all available parameters:
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
name |
string | - | Yes | Table/view/function/procedure/trigger/index name (must be lowercase, cannot be empty) |
points |
int/float | 0 |
No | Points awarded for this test |
title |
string | None |
No | Test description shown in feedback |
column_name |
string/list | None |
No | Column name(s) to test (must be lowercase; can be string or list for multiple columns) |
should_exist |
boolean | True |
No | Whether result should exist (True) or not exist (False) |
expected_value |
any | None |
No | Expected value; can be: single value, 'NULL', or list [min, max] for numeric range, or list of strings for value matching |
where |
string | None |
No | WHERE clause for filtering (SQL values use single quotes inside Python double quotes) |
join |
string | None |
No | JOIN clause (currently only INNER JOIN supported) |
description |
string | None |
No | Internal description (not displayed in feedback, only visible to test writer) |
arguments |
list | None |
No | Function/procedure arguments, or info_schema column selection |
expected_type |
string | None |
No | Expected column type ('varchar', 'integer', 'decimal', 'float', 'text', 'boolean') |
expected_character_maximum_length |
int | None |
No | Expected column maximum length (for varchar types) |
expected_count |
int/list | None |
No | Expected row count (for functions/procedures); can be integer or list for range |
pre_query |
string | None |
No | SQL to run before test (procedure tests only) |
after_query |
string | None |
No | SQL to run after test (required for procedure tests) |
custom_feedback |
string | None |
No | Custom feedback message (overwrites default translated feedback) |
query |
string | None |
No | Should not be used - automatically generated by test classes |
constraint_name |
string | None |
No | Constraint name (ConstraintTest only) |
constraint_type |
string | None |
No | Constraint type: 'PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK' (ConstraintTest only) |
number_of_parameters |
int | None |
No | Expected number of function/procedure parameters (FunctionTest/ProcedureTest only) |
isMaterialized |
boolean | False |
No | Whether view is materialized (ViewTest only) |
action_timing |
string | None |
No | Trigger action timing: 'BEFORE' or 'AFTER' (TriggerTest only) |
elements |
str/list | None |
No | String or list of elements in the query, which are required or not allowed to be used in the query (ie. LIMIT) (QueryStructure and ViewTest only) |
When writing WHERE clauses, remember:
- SQL values must be in single quotes:
'value' - Python strings use double quotes:
"..."
Example:
DataTest(
name='users',
where="email='admin@example.com'", # SQL uses ' inside Python "
points=15
)For columns with spaces or special characters:
QueryDataTest(
name='query_test',
where='"Title beginning"' + "='Dance'", # Column name with spaces
points=20
)Single Value:
expected_value=100
expected_value='admin@example.com'
expected_value='NULL'Numeric Range:
expected_value=[100, 200] # Value must be between 100 and 200String List:
expected_value=['active', 'pending', 'inactive'] # Value must be in this listSingle Column:
column_name='email'Multiple Columns:
column_name=['email', 'username'] # Tests for multiple columns-
Prepare Database:
pg_dump -U postgres mydatabase > mydb.sql -
Write Tests:
from silmused.ChecksLayer import ChecksLayer from silmused import StructureTest, DataTest, ConstraintTest, TitleLayer tests = [ TitleLayer('Homework 3'), ChecksLayer( title='Table Persons tests ', tests=[ StructureTest(title='Does table Persons exists?',name='persons',points=30), DataTest(title='Does table Persons have correct rows of data?',name='persons',expected_value=17,points=20), ConstraintTest(title='Does the primary key exist?',name='persons',constraint_type='PRIMARY KEY',points=20), ConstraintTest(title='Does the unique constraint exist?',name='persons',constraint_type='UNIQUE',points=10), ] ), TitleLayer('Practical 3'), ChecksLayer( title='Table Clubs tests', tests=[ StructureTest(title='Does column location exists?',name='clubs',column_name='location',points=30), DataTest(title='Is column location filled with data?',name='clubs',column_name='location',points=20), ] ) ]
-
Run Tests:
runner = Runner( backup_file_path='mydb.sql', tests=tests, lang='en' ) results = runner.get_results() print(results)
-
Prepare Database and Query:
- Database SQL file (for query execution context)
- SQL query file (the query to test)
-
Write Query Tests:
from silmused import Runner, QueryStructureTest, QueryDataTest tests = [ QueryStructureTest( name='query_test', # Always 'query_test' for query tests column_name='Title', title='Query has Title column', points=20 ), QueryDataTest( name='query_test', column_name='COUNT(*)', expected_value=10, title='Query returns 10 rows', points=30 ), QueryDataTest( name='query_test', column_name='pealkiri', where="test_id=1", # test_id is added automatically for row ordering expected_value="Madness of Love", title='First row is correct', points=30 ) ]
-
Run Query Tests:
with open('query.sql', 'r') as f: query_sql = f.read() runner = Runner( backup_file_path='database.sql', tests=tests, test_query='query', # Enable query test mode query_sql=query_sql, lang='en' ) results = runner.get_results() print(results)
Silmused can be run from the command line:
Database Tests:
silmused <database_dump_file> <tests_file> <db_user> <hostname> <port> <db_password> <test_language> test '' <encoding>Example:
silmused lahendus.sql tests.py postgres localhost 5432 postgresql en test '' UTF-8Query Tests:
silmused <query_file> <tests_file> <db_user> <hostname> <port> <db_password> <test_language> query <query_test_database> <encoding>Example:
silmused query.sql euro_kodu_3_2.py postgres localhost 5432 postgresql et query eurovisioon.sql UTF-8Note: The tests file must include an array with the key "tests":
tests = [
StructureTest(...),
DataTest(...)
]from silmused import (
Runner, ChecksLayer, StructureTest, DataTest,
ConstraintTest, FunctionTest, TitleLayer
)
tests = [
TitleLayer('Database Structure'),
ChecksLayer(
title='Users table validation',
tests=[
StructureTest(name='users', title='Table exists', points=10),
StructureTest(name='users', column_name='email', expected_type='varchar', expected_character_maximum_length=255, points=15),
StructureTest(name='users', column_name='created_at', expected_type='timestamp', points=10),
ConstraintTest(name='users', constraint_type='PRIMARY KEY', points=20),
ConstraintTest(name='users', constraint_type='UNIQUE', column_name='email', points=15),
DataTest(name='users', column_name='COUNT(*)', expected_value=100, points=20),
DataTest(name='users', where="email='admin@example.com'", expected_value='admin@example.com', column_name='email', points=10)
]
),
TitleLayer('Functions'),
FunctionTest(
name='calculate_total',
arguments=[100, 0.2],
expected_value=120,
number_of_parameters=2,
title='Calculate total function works correctly',
points=30
)
]
runner = Runner(
backup_file_path='database.sql',
tests=tests,
lang='en',
encoding='UTF-8'
)
results = runner.get_results()
print(results)Results are returned as JSON in OK_V3 format:
{
"result_type": "OK_V3",
"points": 85,
"producer": "silmused 1.4.5",
"finished_at": "2024-01-15T10:30:00Z",
"tests": [
{
"title": "Users table exists",
"status": "PASS",
"feedback": ""
},
{
"title": "Email column exists",
"status": "FAIL",
"feedback": "Wrong, expected to find column email in table users"
},
{
"title": "Users table validation",
"status": "FAIL",
"checks": [
{
"title": "Table exists",
"status": "PASS",
"feedback": ""
},
{
"title": "Email column exists",
"status": "FAIL",
"feedback": "Wrong, expected to find column email in table users"
}
]
}
]
}- Points are calculated as:
(earned_points / total_points) * 100 - If all tests have 0 points, the system treats it as a pass/fail test (100% if all pass, 0% if any fail)
- Final score is rounded to nearest integer
Feedback messages are generated through the Translator system:
- Test execution determines success/failure
- Test type and test key are determined
- Translator looks up message in locale file
- Parameters are substituted into message template
- Custom feedback (if provided) overrides default messages
-
Root Level:
result_type: Always"OK_V3"points: Percentage score (0-100)producer: Version string (e.g.,"silmused 1.4.5")finished_at: ISO 8601 timestamptests: Array of test results
-
Test Result:
title: Test titlestatus:"PASS"or"FAIL"feedback: Human-readable feedback message (empty for passing tests)checks: (Optional) Nested results for ChecksLayer groups
-
System Failures:
exception_message: Raw exception message for system failuresstatus:"FAIL"for system failures
Silmused uses a JSON-based translation system located in silmused/locale/. Translation files are organized by test type and test key.
{
"test_type": {
"test_key": "Message template with $param1, $param2, etc.",
...
},
...
}- English (
en) - Default, located insilmused/locale/en.json - Estonian (
et) - Located insilmused/locale/et.json
Translation messages are organized by test type:
structure_test- StructureTest messagesdata_test- DataTest messagesconstraint_test- ConstraintTest messagesfunction_test- FunctionTest messagesprocedure_test- ProcedureTest messagesview_test- ViewTest messagesindex_test- IndexTest messagestrigger_test- TriggerTest messagesquery_structure_test- QueryStructureTest messagesquery_data_test- QueryDataTest messagessys_fail- System error messages
Messages support parameter substitution using $param1 through $param5:
Example:
{
"structure_test": {
"table_should_exist_positive_feedback": "Correct, table $param1 was found",
"table_should_exist_negative_feedback": "Wrong, expected to find table $param1, but none were found"
}
}Set the language when creating the Runner:
runner = Runner(
backup_file_path='database.sql',
tests=tests,
lang='et' # Estonian feedback
)Every test supports custom_feedback parameter that overrides the default translated messages:
DataTest(
name='users',
column_name='COUNT(*)',
expected_value=100,
custom_feedback='Expected exactly 100 users, but found a different number',
points=20
)When custom_feedback is provided, it replaces both positive and negative feedback messages.
- Always use lowercase for table/column names - Silmused expects lowercase names
- Use descriptive titles - They appear in test results and help debugging
- Group related tests - Use
ChecksLayerto organize tests logically - Set appropriate points - Total points typically sum to 100 for percentage scoring
- Use
descriptionfor internal notes - Not shown in feedback, useful for documentation - Test incrementally - Start with structure tests, then data tests
- Handle edge cases - Test both positive (
should_exist=True) and negative (should_exist=False) cases - Use string concatenation for complex WHERE clauses - When dealing with column names containing spaces or special characters
- For query tests, always use
name='query_test'- This is the automatically created table name - Use
test_idfor row ordering in query tests - Thetest_idcolumn is automatically added for row ordering
Problem: Tests fail with "table not found"
- Solution: Ensure table names are lowercase in tests
Problem: Query tests not working
- Solution: Make sure
test_query='query'andquery_sqlis set, and usename='query_test'for all query tests
Problem: Encoding errors
- Solution: Specify
encoding='UTF-8'when loading SQL files with special characters
Problem: Database connection fails
- Solution: Check database credentials, host, port, and ensure PostgreSQL is running
Problem: Expected value not matching
- Solution: Check data types - use string conversion for comparisons if needed
Problem: Procedure tests fail
- Solution: Ensure
after_queryis provided (required for procedure tests)
Problem: Constraint tests not finding constraints
- Solution: Check constraint names and types - they must match exactly (case-sensitive)
- Check
silmused/test_cases/for example test files - Review
demo.pyandquery_demo.pyfor complete working examples