Quality Assurance and Diagnostics
mORMot provides comprehensive testing and logging capabilities through mormot.core.test and mormot.core.log units. These tools are essential for building reliable, maintainable applications.
Testing ensures:
The recommended approach:
1. Write a void implementation (interface only) 2. Write a test 3. Run test - it must fail 4. Implement the feature 5. Run test - it must pass 6. Refactor and repeat
TSynTest (abstract)
├── TSynTestCase → Individual test case
└── TSynTests → Test suite (runs multiple cases)
Defines individual tests in published methods:
uses
mormot.core.test;
type
TTestMathOperations = class(TSynTestCase)
published
procedure TestAddition;
procedure TestMultiplication;
procedure TestDivision;
end;
procedure TTestMathOperations.TestAddition;
begin
Check(1 + 1 = 2, '1+1 should equal 2');
Check(Add(10, 20) = 30, 'Add function failed');
CheckEqual(Add(-5, 5), 0, 'Adding opposites');
end;
Runs a suite of test cases:
type
TMyTestSuite = class(TSynTests)
published
procedure AllTests;
end;
procedure TMyTestSuite.AllTests;
begin
AddCase([
TTestMathOperations,
TTestStringOperations,
TTestDatabaseOperations
]);
end;
// Main program
begin
with TMyTestSuite.Create('My Application Tests') do
try
Run;
Readln;
finally
Free;
end;
end.
// Boolean check
Check(Value = Expected, 'Error message');
// Equality checks
CheckEqual(Actual, Expected, 'Error message');
CheckNotEqual(Actual, Unexpected, 'Error message');
// Floating-point comparison (with tolerance)
CheckSame(FloatValue, ExpectedFloat, 'Floating point error');
// UTF-8 string comparison
CheckUtf8(ActualStr, ExpectedStr, 'String mismatch');
// Hash comparison
CheckHash(ActualHash, ExpectedHash, 'Hash mismatch');
procedure TTestErrors.TestExceptionRaised;
begin
// Verify exception is raised
CheckRaised(
procedure begin
raise EInvalidOperation.Create('Test');
end,
EInvalidOperation,
'Expected exception not raised'
);
end;
procedure TTestCustomer.TestOrderValidation;
var
Order: TOrder;
begin
Order := TOrder.Create;
try
// Multiple checks
Check(Order.Items.Count = 0, 'New order should be empty');
Order.AddItem(1, 2, 10.00);
CheckEqual(Order.Items.Count, 1, 'Should have one item');
CheckSame(Order.TotalAmount, 20.00, 'Total should be 20.00');
finally
Order.Free;
end;
end;
var
Suite: TMyTestSuite;
begin
Suite := TMyTestSuite.Create('Test Suite');
try
Suite.Run;
finally
Suite.Free;
end;
end;
Output example:
Test Suite
1. Math Operations
- Addition: 3 assertions passed 12.5 us
- Multiplication: 5 assertions passed 8.2 us
- Division: 4 assertions passed 6.1 us
2. String Operations
- Concatenation: 10 assertions passed 25.3 us
- Parsing: 8 assertions passed 18.7 us
Total: 30 assertions passed in 2 test cases
type
TSynTestOptions = set of (
tcoLogEachCheck, // Log each Check() call
tcoLogInSubFolder, // Put logs in ./log subfolder
tcoLogVerboseRotate, // Rotate large log files
tcoLogNotHighResolution // Use plain ISO-8601 timestamps
);
// Configure
Suite.Options := [tcoLogEachCheck, tcoLogInSubFolder];
Combines testing with logging:
type
TMyLoggedTests = class(TSynTestsLogged)
published
procedure AllTests;
end;
begin
with TMyLoggedTests.Create('Logged Tests') do
try
Run;
finally
Free;
end;
end;
procedure TTestWithLogging.TestDatabaseConnection;
begin
TSynLog.Enter(self, 'TestDatabaseConnection');
Log.Log(sllInfo, 'Connecting to database...');
// Test code...
Log.Log(sllDebug, 'Connection established');
Check(Connected, 'Should be connected');
end;
uses
mormot.core.interfaces;
type
ICalculator = interface(IInvokable)
['{...}']
function Add(A, B: Integer): Integer;
function Multiply(A, B: Integer): Integer;
end;
procedure TTestWithMocks.TestServiceWithMockedDependency;
var
Mock: TInterfaceMock;
Calculator: ICalculator;
begin
// Create mock
Mock := TInterfaceMock.Create(TypeInfo(ICalculator), Calculator, self);
// Define behavior
Mock.ExpectsCount('Add', qoEqualTo, 2); // Expect 2 calls
Mock.Returns('Add', [10, 20], 30); // Return 30 for Add(10,20)
Mock.Returns('Multiply', [], 100); // Return 100 for any Multiply
// Use mock
CheckEqual(Calculator.Add(10, 20), 30);
CheckEqual(Calculator.Multiply(5, 5), 100);
// Verify expectations
Mock.Verify;
end;
procedure TTestStubs.TestWithStub;
var
Stub: TInterfaceStub;
Service: IMyService;
begin
// Create stub (no verification)
Stub := TInterfaceStub.Create(TypeInfo(IMyService), Service);
// Define returns
Stub.Returns('GetValue', [], 'stubbed value');
// Use stub
CheckEqual(Service.GetValue, 'stubbed value');
end;
type
TInterfaceMockOptions = set of (
imoMockFailsWillPassTestCase, // Failures don't fail test
imoFakeInstanceCreation, // Create fake objects
imoLogMethodCallsAndResults // Log all calls
);
Mock.Options := [imoLogMethodCallsAndResults];
┌─────────────────────────────────────────────────────────────────┐
│ Logging Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ TSynLogFamily │ Configuration (levels, rotation, etc.) │
│ │ (per-class) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ TSynLog │ Logger instance (per-thread) │
│ │ (per-thread) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Log File │ │
│ │ • Automatic rotation │ │
│ │ • Stack traces on errors │ │
│ │ • Thread-safe writes │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
uses
mormot.core.log;
// Simple logging
TSynLog.Add.Log(sllInfo, 'Application started');
TSynLog.Add.Log(sllDebug, 'Processing item %', [ItemID]);
TSynLog.Add.Log(sllError, 'Failed to connect: %', [ErrorMessage]);
type
TSynLogLevel = (
sllNone, // No logging
sllInfo, // Informational messages
sllDebug, // Debug information
sllTrace, // Detailed tracing
sllWarning, // Warnings
sllError, // Errors
sllEnter, // Method entry
sllLeave, // Method exit
sllLastError, // OS last error
sllException, // Exception caught
sllExceptionOS, // OS exception
sllMemory, // Memory allocation
sllStackTrace, // Stack trace
sllFail, // Test failure
sllSQL, // SQL statements
sllCache, // Cache operations
sllResult, // Method results
sllDB, // Database operations
sllHTTP, // HTTP traffic
sllClient, // Client operations
sllServer, // Server operations
sllServiceCall, // Service invocations
sllServiceReturn, // Service returns
sllUserAuth, // User authentication
sllCustom1..4, // Custom levels
sllNewRun, // New run marker
sllDDDError, // DDD errors
sllDDDInfo, // DDD info
sllMonitoring // Monitoring data
);
var
LogFamily: TSynLogFamily;
begin
LogFamily := TSynLog.Family;
// Set log levels
LogFamily.Level := LOG_VERBOSE; // All levels
// Or specific levels
LogFamily.Level := [sllInfo, sllWarning, sllError, sllException];
// File settings
LogFamily.PerThreadLog := ptIdentifiedInOneFile; // One file, thread IDs
LogFamily.DestinationPath := 'C:\Logs\';
LogFamily.FileExistsAction := acAppend;
end;
// Rotate by size
LogFamily.RotateFileCount := 5; // Keep 5 files
LogFamily.RotateFileSizeKB := 10240; // 10MB per file
// Rotate by time
LogFamily.RotateFileDailyAtHour := 0; // Rotate at midnight
// Archive rotated logs
LogFamily.RotateFileArchiveCompression := acSynLz; // Compress with SynLZ
// Enable stack traces for errors
LogFamily.LevelStackTrace := [sllError, sllException, sllExceptionOS];
// Requires .map or .mab file for readable stack traces
// Generate .mab from .map:
// mormot2tests.map -> mormot2tests.mab (much smaller)
procedure TMyClass.ProcessData(const Data: TData);
begin
TSynLog.Enter(self, 'ProcessData'); // Logs entry with timestamp
// Processing...
TSynLog.Add.Log(sllDebug, 'Processing % bytes', [Length(Data)]);
// Automatic leave logging on scope exit
end;
Output:
20230615 14:32:15.123 + TMyClass.ProcessData
20230615 14:32:15.125 Processing 1024 bytes
20230615 14:32:15.130 - 00.007
// Log object as JSON
TSynLog.Add.Log(sllDebug, Customer); // Serializes to JSON
// Log with context
TSynLog.Add.Log(sllInfo, 'Customer loaded: %', [Customer], TypeInfo(TCustomer));
// Enable SQL logging
LogFamily.Level := LogFamily.Level + [sllSQL, sllDB];
// SQL statements are automatically logged by ORM
// Output:
// 20230615 14:35:22.456 SQL SELECT * FROM Customer WHERE ID=?
uses
mormot.core.log;
procedure ProcessWithLogging;
var
Log: ISynLog;
begin
Log := TSynLog.Enter(nil, 'ProcessWithLogging');
Log.Log(sllInfo, 'Starting process');
try
// Work...
Log.Log(sllDebug, 'Step 1 complete');
except
on E: Exception do
begin
Log.Log(sllException, E);
raise;
end;
end;
end; // Automatic leave logged
type
IMyService = interface
procedure DoWork;
end;
TMyService = class(TInterfacedObject, IMyService)
private
fLog: ISynLog;
public
constructor Create(const aLog: ISynLog);
procedure DoWork;
end;
procedure TMyService.DoWork;
begin
fLog.Log(sllInfo, 'Starting work');
// ...
end;
For readable stack traces, provide debug symbols:
// Delphi: Generate .map file (Project Options > Linker > Map File = Detailed)
// FPC: Compile with -gl flag, or use external .dbg file
// Convert .map to .mab (optimized format)
TDebugFile.Create('myapp.map', true); // Creates myapp.mab
| Format | Size (typical) | Load Time |
|---|---|---|
| .map | 4-15 MB | Slow |
| .dbg | 10-50 MB | Slow |
| .mab | 200-500 KB | Fast |
uses
mormot.core.log;
begin
// Install global exception handler
TSynLog.Family.Level := LOG_VERBOSE + [sllExceptionOS];
// All unhandled exceptions are logged with stack trace
end;
procedure SafeProcess;
begin
try
RiskyOperation;
except
on E: Exception do
begin
TSynLog.Add.Log(sllException, E);
// Or with additional context
TSynLog.Add.Log(sllException, '% during % processing',
[E.ClassName, OperationName], E);
raise;
end;
end;
end;
uses
mormot.core.log,
mormot.net.client;
var
LogFamily: TSynLogFamily;
begin
LogFamily := TSynLog.Family;
// Enable remote logging
LogFamily.EchoRemoteClient := THttpClientSocket.Create('logserver', '8080');
LogFamily.EchoRemoteClientOwned := True;
end;
// Send to SysLog server (RFC 5424)
LogFamily.EchoToSysLog := True;
LogFamily.SysLogFacility := sfLocal0;
Read and analyze log files:
uses
mormot.core.log;
var
LogFile: TSynLogFile;
i: Integer;
begin
LogFile := TSynLogFile.Create('app.log');
try
// Iterate events
for i := 0 to LogFile.EventCount - 1 do
begin
Writeln(LogFile.EventDateTime[i], ': ',
LogFile.EventLevel[i], ' - ',
LogFile.EventText[i]);
end;
// Get specific level events
Writeln('Errors: ', LogFile.EventCount(sllError));
finally
LogFile.Free;
end;
end;
mORMot provides a visual log viewer:
ex/logview/// Use conditional to avoid string formatting overhead
if sllDebug in TSynLog.Family.Level then
TSynLog.Add.Log(sllDebug, 'Complex: % + %', [ExpensiveCall1, ExpensiveCall2]);
// Enable async writes (background thread)
LogFamily.BufferSize := 32768; // 32KB buffer
LogFamily.NoFile := False;
// Production: minimal overhead
LogFamily.Level := [sllWarning, sllError, sllException];
LogFamily.LevelStackTrace := [sllException];
LogFamily.RotateFileCount := 10;
LogFamily.RotateFileSizeKB := 20480; // 20MB
// Development: verbose
LogFamily.Level := LOG_VERBOSE;
LogFamily.LevelStackTrace := [sllError, sllException, sllExceptionOS];
| Class | Purpose |
|---|---|
TSynTestCase |
Individual test case |
TSynTests |
Test suite runner |
TSynTestsLogged |
Suite with logging |
TInterfaceMock |
Interface mocking |
TInterfaceStub |
Interface stubbing |
| Method | Purpose |
|---|---|
Check() |
Boolean assertion |
CheckEqual() |
Equality assertion |
CheckSame() |
Float comparison |
CheckRaised() |
Exception testing |
| Class | Purpose |
|---|---|
TSynLog |
Logger instance |
TSynLogFamily |
Logger configuration |
ISynLog |
Logger interface |
TSynLogFile |
Log file reader |
TDebugFile |
Debug symbols |
| Level | Use For |
|---|---|
sllInfo |
Informational messages |
sllDebug |
Debug output |
sllWarning |
Warnings |
sllError |
Errors |
sllException |
Exceptions |
sllSQL |
SQL statements |
sllHTTP |
HTTP traffic |
| Unit | Purpose |
|---|---|
mormot.core.test |
Testing framework |
mormot.core.log |
Logging framework |
mormot.core.interfaces |
Mocking support |
This concludes the mORMot2 SAD Guide. For additional information, consult the source code documentation and the official mORMot forum.
| Previous | Index | Next |
|---|---|---|
| Chapter 24: Domain-Driven Design | Index | Chapter 26: Source Code |