Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A comprehensive unit testing framework for the 4D platform with test tagging, fi
- **Multiple output formats** - Human-readable and JSON output
- **CI/CD ready** - Structured JSON output for automated testing
- **Transaction management** - Automatic test isolation with rollback
- **Trigger control** - Database triggers disabled by default with opt-in helpers
- **Subtests** - Run table-driven tests with `t.run`

## Quick Example
Expand Down
26 changes: 26 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,32 @@ Function _checkMathCase($t : cs.Testing.Testing; $case : Object)
$t.assert.areEqual($t; $case.want; $case.in+1; "math works")
```

## Trigger Management

Database triggers are disabled for every test context by default. The framework issues `ALTER DATABASE DISABLE TRIGGERS` whenever a `Testing` instance is created or reset so tests begin from a consistent state without trigger side effects. Any table-level trigger overrides are tracked and reverted automatically before the next test runs.

Use the following helpers on the `$t` context when a test needs trigger support:

- `$t.disableAllTriggers()` – Explicitly disable triggers for the current session and clear any table overrides
- `$t.enableAllTriggers()` – Re-enable triggers for all tables in the current session
- `$t.enableTableTriggers(tableName)` – Enable triggers for a single table while keeping others disabled
- `$t.disableTableTriggers(tableName)` – Disable triggers for a specific table and remove it from the override list
- `$t.restoreTriggerDefaults()` – Convenience wrapper that restores the default “all triggers disabled” state

Table names are quoted automatically, so you can enable triggers on tables with spaces or special characters without additional escaping. Any double quotes inside the name are doubled per SQL rules.

```4d
Function test_trigger_callbacks($t : cs.Testing.Testing)
// Enable triggers only for the tables needed by this scenario
$t.enableTableTriggers("AuditLog")

// Execute behavior that relies on table triggers
This._performOperation()

// Return to the default trigger state for the remainder of the test
$t.restoreTriggerDefaults()
```

## Transaction Management

The framework provides automatic transaction management for test isolation and manual transaction control for advanced scenarios.
Expand Down
119 changes: 115 additions & 4 deletions testing/Project/Sources/Classes/Testing.4dm
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ property assert : cs:C1710.Assert
property stats : cs:C1710.UnitStatsTracker
property failureCallChain : Collection
property classInstance : 4D:C1709.Object
property triggersGloballyEnabled : Boolean
property triggersEnabledTables : Collection

Class constructor()
This:C1470.failed:=False:C215
This:C1470.done:=False:C215
This:C1470.failed:=False:C215
This:C1470.done:=False:C215
This:C1470.logMessages:=[]
This:C1470.assertions:=[]
This:C1470.assert:=cs:C1710.Assert.new()
This:C1470.stats:=cs:C1710.UnitStatsTracker.new()
This:C1470.failureCallChain:=Null
This:C1470._resetTriggerTracking()
This:C1470.disableAllTriggers()

Function log($message : Text)
This:C1470.logMessages.push($message)
Expand Down Expand Up @@ -59,12 +63,119 @@ Function fatal()
This:C1470.failureCallChain:=Call chain:C1662

Function resetForNewTest()
This:C1470.failed:=False:C215
This:C1470.done:=False:C215
This:C1470.disableAllTriggers()
This:C1470.failed:=False:C215
This:C1470.done:=False:C215
This:C1470.logMessages:=[]
This:C1470.assertions:=[]
This:C1470.stats.resetStatistics()
This:C1470.failureCallChain:=Null

Function disableAllTriggers()
This:C1470._executeSQL("ALTER DATABASE DISABLE TRIGGERS")

If (This:C1470.triggersEnabledTables#Null:C1517)
var $tableName : Text
For each ($tableName; This:C1470.triggersEnabledTables)
This:C1470._disableTriggersForTable($tableName)
End for each
End if

This:C1470._resetTriggerTracking()

Function enableAllTriggers()
This:C1470._executeSQL("ALTER DATABASE ENABLE TRIGGERS")
This:C1470.triggersGloballyEnabled:=True:C214

Function enableTableTriggers($tableName : Text)
If ($tableName=Null:C1517)
return
End if

If ($tableName="")
return
End if

var $statement : Text
$statement:="ALTER TABLE "+This:C1470._quoteIdentifier($tableName)+" ENABLE TRIGGERS"
This:C1470._executeSQL($statement)

If (This:C1470.triggersEnabledTables=Null:C1517)
This:C1470.triggersEnabledTables:=[]
End if

If (This:C1470.triggersEnabledTables.indexOf($tableName)<0)
This:C1470.triggersEnabledTables.push($tableName)
End if

Function disableTableTriggers($tableName : Text)
If ($tableName=Null:C1517)
return
End if

If ($tableName="")
return
End if

This:C1470._disableTriggersForTable($tableName)

If (This:C1470.triggersEnabledTables#Null:C1517)
var $index : Integer
$index:=This:C1470.triggersEnabledTables.indexOf($tableName)
If ($index>=0)
This:C1470.triggersEnabledTables.remove($index)
End if
End if

Function restoreTriggerDefaults()
This:C1470.disableAllTriggers()

Function _disableTriggersForTable($tableName : Text)
If ($tableName=Null:C1517)
return
End if

If ($tableName="")
return
End if

var $statement : Text
$statement:="ALTER TABLE "+This:C1470._quoteIdentifier($tableName)+" DISABLE TRIGGERS"
This:C1470._executeSQL($statement)

Function _quoteIdentifier($identifier : Text) : Text
If ($identifier=Null:C1517)
return "\"\""
End if

var $escaped : Text
$escaped:=Replace string:C233($identifier; "\""; "\"\"")
return "\""+$escaped+"\""

Function _executeSQL($statement : Text)
If ($statement="")
return
End if

var $finalStatement : Text
$finalStatement:=$statement

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this final statement thing achieving anything? Feels like $statement could just be modified directly trhoughout the function.


If (Length:C16($finalStatement)>0)
var $lastChar : Text
$lastChar:=Substring:C12($finalStatement; Length:C16($finalStatement); 1)
If ($lastChar#";")
$finalStatement:=$finalStatement+";"
End if
End if

This:C1470._performSQLExecution($finalStatement)

Function _performSQLExecution($statement : Text)
SQL EXECUTE($statement)

Function _resetTriggerTracking()
This:C1470.triggersGloballyEnabled:=False:C215
This:C1470.triggersEnabledTables:=[]

Function run($name : Text; $subtest : 4D:C1709.Function; $data : Variant) : Boolean
// Execute a named subtest with its own Testing context
Expand Down
114 changes: 114 additions & 0 deletions testing/Project/Sources/Classes/_TestingTest.4dm
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,120 @@ Function test_run_subtest_stats_isolated($t : cs:C1710.Testing)
$parentStat:=$testing.stats.getStat("mocked")
$t.assert.areEqual($t; 0; $parentStat.getNumberOfCalls(); "Parent stats should remain unaffected")

Function test_triggers_disabled_by_default($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()

$t.assert.isTrue($t; ($mock.executedStatements.length>0); "Should execute SQL on initialization")
$t.assert.areEqual($t; "ALTER DATABASE DISABLE TRIGGERS;"; $mock.executedStatements[0]; "Should disable triggers by default")
$t.assert.isFalse($t; $mock.triggersGloballyEnabled; "Global trigger flag should be cleared")

Function test_enable_all_triggers_executes_sql($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()
$mock.executedStatements:=[]

$mock.enableAllTriggers()

$t.assert.areEqual($t; 1; $mock.executedStatements.length; "Should execute a single SQL statement")
$t.assert.areEqual($t; "ALTER DATABASE ENABLE TRIGGERS;"; $mock.executedStatements[0]; "Should enable triggers globally")
$t.assert.isTrue($t; $mock.triggersGloballyEnabled; "Global trigger flag should be set")

Function test_enable_table_triggers_tracks_tables($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()
$mock.executedStatements:=[]

$mock.enableTableTriggers("Orders")

$t.assert.areEqual($t; 1; $mock.executedStatements.length; "Should execute table enable SQL")
$t.assert.areEqual($t; "ALTER TABLE \"Orders\" ENABLE TRIGGERS;"; $mock.executedStatements[0]; "Should quote table name")
$t.assert.areEqual($t; 1; $mock.triggersEnabledTables.length; "Should track enabled table")
$t.assert.areEqual($t; "Orders"; $mock.triggersEnabledTables[0]; "Should store table identifier")

$mock.enableTableTriggers("Orders")
$t.assert.areEqual($t; 1; $mock.triggersEnabledTables.length; "Should avoid duplicate tracking entries")

Function test_enable_table_triggers_escapes_quotes($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()
$mock.executedStatements:=[]

$mock.enableTableTriggers("Order\"Detail")

$t.assert.areEqual($t; "ALTER TABLE \"Order\"\"Detail\" ENABLE TRIGGERS;"; $mock.executedStatements[0]; "Should escape embedded quotes")

Function test_disable_table_triggers_updates_tracking($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()
$mock.executedStatements:=[]

$mock.enableTableTriggers("Orders")
$mock.executedStatements:=[]

$mock.disableTableTriggers("Orders")

$t.assert.areEqual($t; 1; $mock.executedStatements.length; "Should execute table disable SQL")
$t.assert.areEqual($t; "ALTER TABLE \"Orders\" DISABLE TRIGGERS;"; $mock.executedStatements[0]; "Should disable triggers for table")
$t.assert.areEqual($t; 0; $mock.triggersEnabledTables.length; "Should remove table from tracking")

Function test_disable_all_triggers_resets_tracking($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()
$mock.executedStatements:=[]

$mock.enableTableTriggers("Orders")
$mock.enableTableTriggers("Customers")
$mock.executedStatements:=[]

$mock.disableAllTriggers()

$t.assert.areEqual($t; 3; $mock.executedStatements.length; "Should disable database and tracked tables")
$t.assert.areEqual($t; "ALTER DATABASE DISABLE TRIGGERS;"; $mock.executedStatements[0]; "Should disable globally")
$t.assert.areEqual($t; "ALTER TABLE \"Orders\" DISABLE TRIGGERS;"; $mock.executedStatements[1]; "Should disable first table")
$t.assert.areEqual($t; "ALTER TABLE \"Customers\" DISABLE TRIGGERS;"; $mock.executedStatements[2]; "Should disable second table")
$t.assert.areEqual($t; 0; $mock.triggersEnabledTables.length; "Should clear table tracking")
$t.assert.isFalse($t; $mock.triggersGloballyEnabled; "Should reset global flag")

Function test_restore_trigger_defaults_disables_triggers($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()
$mock.executedStatements:=[]

$mock.enableAllTriggers()
$mock.executedStatements:=[]

$mock.restoreTriggerDefaults()

$t.assert.areEqual($t; 1; $mock.executedStatements.length; "Should call disable when restoring defaults")
$t.assert.areEqual($t; "ALTER DATABASE DISABLE TRIGGERS;"; $mock.executedStatements[0]; "Should disable triggers when restoring")
$t.assert.isFalse($t; $mock.triggersGloballyEnabled; "Should clear global state on restore")

Function test_reset_for_new_test_restores_trigger_state($t : cs:C1710.Testing)

var $mock : cs:C1710._TestingTriggerMock
$mock:=cs:C1710._TestingTriggerMock.new()
$mock.executedStatements:=[]

$mock.enableTableTriggers("Orders")
$mock.enableAllTriggers()
$mock.executedStatements:=[]

$mock.resetForNewTest()

$t.assert.areEqual($t; 2; $mock.executedStatements.length; "Reset should disable database and tracked tables")
$t.assert.areEqual($t; "ALTER DATABASE DISABLE TRIGGERS;"; $mock.executedStatements[0]; "Reset should disable globally")
$t.assert.areEqual($t; "ALTER TABLE \"Orders\" DISABLE TRIGGERS;"; $mock.executedStatements[1]; "Reset should disable tracked table")
$t.assert.areEqual($t; 0; $mock.triggersEnabledTables.length; "Reset should clear tracking")
$t.assert.isFalse($t; $mock.triggersGloballyEnabled; "Reset should clear global trigger flag")

Function _addOneCase($t : cs:C1710.Testing; $case : Object)

var $got : Integer
Expand Down
17 changes: 17 additions & 0 deletions testing/Project/Sources/Classes/_TestingTriggerMock.4dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Class extends Testing

property executedStatements : Collection

Class constructor()
Super:C1705()

If (This:C1470.executedStatements=Null:C1517)
This:C1470.executedStatements:=[]
End if

Function _performSQLExecution($statement : Text)
If (This:C1470.executedStatements=Null:C1517)
This:C1470.executedStatements:=[]
End if

This:C1470.executedStatements.push($statement)