Skip to content

Commit 49044a8

Browse files
authored
Merge pull request #23 from KyleKincer/feature/skip-triggers
feat: skip triggers in host project by default
2 parents c470c13 + a94f3b2 commit 49044a8

17 files changed

+1093
-358
lines changed

CLAUDE.md

Lines changed: 171 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ A comprehensive unit testing framework for the 4D 4GL platform with test tagging
66

77
This project provides a complete testing framework for 4D applications featuring:
88

9-
- **Auto test discovery** - Finds test classes ending with "Test"
9+
- **Auto test discovery** - Finds test classes ending with "Test"
1010
- **Comment-based tagging** - Organize tests with `// #tags: unit, integration, slow`
1111
- **Flexible filtering** - Run specific test subsets by name, pattern, or tags
1212
- **Multiple output formats** - Human-readable and JSON output with terse/verbose modes
1313
- **CI/CD ready** - Structured JSON output for automated testing pipelines
1414
- **Parallel test execution** - Run test suites concurrently for improved performance
1515
- **Automatic transaction management** - Test isolation with automatic rollback
1616
- **Manual transaction control** - Full transaction lifecycle management for advanced scenarios
17+
- **Trigger control** - Automatically skip database triggers during tests for true isolation
1718

1819
## Running Tests
1920

@@ -95,13 +96,18 @@ If you need more control or the Makefile doesn't meet your needs:
9596
--user-param "parallel=true"
9697
--user-param "parallel=true maxWorkers=4"
9798
--user-param "parallel=true format=json tags=unit"
99+
100+
# Trigger control
101+
--user-param "triggers=enabled" # Enable triggers for all tests
102+
--user-param "triggers=disabled" # Disable triggers (default)
103+
--user-param "triggers=enabled tags=integration" # Enable triggers for integration tests
98104
```
99105

100106
### Current Test Status
101107

102-
- **Total Tests**: 121 tests across 14 test suites
103-
- **Pass Rate**: 100%
104-
- **Key Test Classes**: TaggingSystemTest, TaggingExampleTest, TestRunnerTest, TestSuiteTest
108+
- **Total Tests**: 165 tests across 16 test suites
109+
- **Pass Rate**: 100% (164 passed, 1 skipped)
110+
- **Key Test Classes**: TaggingSystemTest, TriggerControlTest, TestRunnerTest, TransactionExampleTest
105111

106112
## Project Structure
107113

@@ -309,4 +315,164 @@ Function test_transactionWrapper($t : cs:C1710.Testing)
309315
| Comment | Effect |
310316
| ------------------------ | ---------------------------------------- |
311317
| `// #transaction: false` | Disables automatic transactions |
312-
| No comment | Enables automatic transactions (default) |
318+
| No comment | Enables automatic transactions (default) |
319+
320+
## Trigger Control During Tests
321+
322+
The framework automatically disables database triggers during test execution to ensure true unit test isolation. This prevents external dependencies and side effects from interfering with tests.
323+
324+
### How It Works
325+
326+
When tests run (either via `tool4d` or from a host project), the framework sets a flag in 4D's shared `Storage`:
327+
328+
```4d
329+
Storage.triggersDisabled.testMode = True
330+
```
331+
332+
This flag remains set for the duration of the test run, allowing triggers in the host project to check and skip execution during testing.
333+
334+
### Running Tests from a Host Project
335+
336+
When running tests from a host project (not standalone), you **must** pass the host project's Storage object to enable trigger control:
337+
338+
```4d
339+
// In your host project's method to run tests
340+
var $hostStorage : Object
341+
var $userParams : Object
342+
343+
$hostStorage:=Storage // Pass the host project's Storage
344+
$userParams:=New object // Optional parameters (e.g., "triggers"; "enabled")
345+
346+
// Call the testing component method with cs, Storage, and optional user params
347+
Testing_RunTestsWithCs(cs; $hostStorage; $userParams)
348+
```
349+
350+
**Important:** Components have separate Storage objects from their host projects. By passing the host's Storage, the test framework can set flags that your host project's triggers can check.
351+
352+
### Implementing Trigger Control in Host Projects
353+
354+
To make triggers skip execution during tests, add this check at the beginning of each trigger:
355+
356+
```4d
357+
// At the start of your trigger code
358+
If (Storage.triggersDisabled#Null) && (Storage.triggersDisabled.testMode=True)
359+
return // Skip trigger execution during tests
360+
End if
361+
362+
// Normal trigger logic continues here...
363+
```
364+
365+
### Example: Complete Trigger Implementation
366+
367+
```4d
368+
// Table trigger with test mode support
369+
If (Storage.triggersDisabled#Null) && (Storage.triggersDisabled.testMode=True)
370+
return
371+
End if
372+
373+
// Normal trigger logic
374+
Case of
375+
: (Trigger event=On Saving New Record Event)
376+
// Validate and set default values
377+
If ([MyTable]requiredField="")
378+
[MyTable]requiredField:="DefaultValue"
379+
End if
380+
381+
: (Trigger event=On Saving Existing Record Event)
382+
// Update modification timestamp
383+
[MyTable]modifiedAt:=Current date
384+
End case
385+
```
386+
387+
### Benefits of Trigger Control
388+
389+
- **True Unit Testing**: Tests can focus on business logic without trigger side effects
390+
- **Faster Tests**: Skipping triggers reduces test execution time
391+
- **Test Isolation**: Each test runs in a clean state without trigger interference
392+
- **Flexibility**: Easily enable triggers for integration tests when needed
393+
394+
### Configuring Trigger Behavior
395+
396+
The framework provides flexible control over when triggers execute:
397+
398+
#### Global Trigger Control via User Parameters
399+
400+
Control the default trigger behavior for all tests using the `triggers` parameter:
401+
402+
```bash
403+
# Enable triggers for all tests (default is disabled)
404+
make test triggers=enabled
405+
406+
# Explicitly disable triggers (default behavior)
407+
make test triggers=disabled
408+
409+
# With other parameters
410+
make test format=json triggers=enabled tags=integration
411+
```
412+
413+
**Default Behavior**: Triggers are **disabled by default** (`triggers=disabled`) to ensure true unit test isolation.
414+
415+
#### Per-Test Trigger Control via Comments
416+
417+
Individual tests can override the global setting using comment annotations:
418+
419+
```4d
420+
// #triggers: enabled
421+
Function test_withTriggersEnabled($t : cs.Testing)
422+
// This test will have triggers enabled regardless of global setting
423+
// Useful for integration tests that need to verify trigger behavior
424+
425+
// #triggers: disabled
426+
Function test_withTriggersDisabled($t : cs.Testing)
427+
// This test will have triggers disabled regardless of global setting
428+
// Useful for unit tests that need isolation
429+
430+
Function test_defaultBehavior($t : cs.Testing)
431+
// No annotation - uses global setting from triggers parameter
432+
```
433+
434+
#### Use Cases
435+
436+
**Unit Tests (triggers disabled):**
437+
```4d
438+
// #triggers: disabled
439+
Function test_calculateTotal($t : cs.Testing)
440+
// Test business logic without trigger side effects
441+
// Default behavior - no annotation needed
442+
```
443+
444+
**Integration Tests (triggers enabled):**
445+
```4d
446+
// #tags: integration
447+
// #triggers: enabled
448+
Function test_orderProcessingWithTriggers($t : cs.Testing)
449+
// Test complete flow including trigger execution
450+
```
451+
452+
**Hybrid Approach:**
453+
```bash
454+
# Run unit tests with triggers disabled (default)
455+
make test-unit
456+
457+
# Run integration tests with triggers enabled
458+
make test-integration triggers=enabled
459+
```
460+
461+
### When to Allow Triggers
462+
463+
For integration tests that specifically need to test trigger behavior:
464+
465+
1. **Use per-test annotations** with `// #triggers: enabled`
466+
2. **Enable globally** with `triggers=enabled` parameter for integration test suites
467+
3. **Tag appropriately** using `// #tags: integration` for filtering
468+
4. **Test trigger logic directly** by extracting it into testable functions
469+
5. **Mock trigger behavior** in unit tests using test doubles
470+
471+
### Implementation Notes
472+
473+
- The `Storage.triggersDisabled.testMode` flag is automatically managed by the test framework
474+
- Per-test trigger control automatically restores the default behavior after each test
475+
- No manual cleanup is required - flags persist only for the test process lifetime
476+
- Works in both interpreted and compiled modes
477+
- Compatible with parallel test execution - each worker process has its own Storage state
478+
- Test-level annotations take precedence over global `triggers` parameter

testing/Project/Sources/Classes/ParallelTestRunner.4dm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ property sharedResults : Object // Shared storage for collecting results
99
property completedSuites : Integer // Counter for completed suites
1010
property sequentialSuites : Collection // Suites that opted out of parallel execution
1111

12-
Class constructor($cs : 4D:C1709.Object)
13-
Super:C1705($cs)
12+
Class constructor($cs : 4D:C1709.Object; $hostStorage : 4D:C1709.Object; $userParams : 4D:C1709.Object)
13+
Super:C1705($cs; $hostStorage; $userParams)
1414
This:C1470.parallelMode:=False:C215
1515
This:C1470.maxWorkers:=This:C1470._getDefaultWorkerCount()
1616
This:C1470.workerProcesses:=[]

testing/Project/Sources/Classes/TestRunner.4dm

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
property classStore : 4D:C1709.Object // Class store from the calling project
2+
property hostStorage : 4D:C1709.Object // Host project's Storage for trigger control
23
property testSuites : Collection // Collection of cs._TestSuite
34
property results : Object // Test results summary
45
property outputFormat : Text // "human", "json", "junit"
@@ -7,23 +8,93 @@ property testPatterns : Collection // Collection of test patterns to match
78
property includeTags : Collection // Tags to include (OR logic)
89
property excludeTags : Collection // Tags to exclude
910
property requireAllTags : Collection // Tags that must all be present (AND logic)
11+
property userParams : Object // User parameters passed to the runner
12+
property disableTriggersByDefault : Boolean // Whether triggers are disabled by default
1013

11-
Class constructor($cs : 4D:C1709.Object)
14+
Class constructor($cs : 4D:C1709.Object; $hostStorage : 4D:C1709.Object; $userParams : Object)
1215
This:C1470.classStore:=$cs || cs:C1710
16+
This:C1470.hostStorage:=$hostStorage // Can be Null for component-only testing
17+
This:C1470.userParams:=$userParams || This:C1470._parseUserParams() // Use provided params or parse from command line
1318
This:C1470.testSuites:=[]
1419
This:C1470._initializeResults()
1520
This:C1470._determineOutputFormat()
1621
This:C1470._parseTestPatterns()
1722
This:C1470._parseTagFilters()
23+
This:C1470._determineTriggerDefaultBehavior()
1824

1925
Function run()
26+
This:C1470._initializeTriggerControl()
2027
This:C1470._prepareErrorHandlingStorage()
2128
var $handlerState : Object
2229
$handlerState:=This:C1470._installErrorHandler()
2330
This:C1470._runInternal()
2431
This:C1470._captureGlobalErrors()
2532
This:C1470._restoreErrorHandler($handlerState)
2633

34+
Function _determineTriggerDefaultBehavior()
35+
// Determine the default trigger behavior based on user parameters
36+
// Default is to disable triggers (testMode=true) unless explicitly enabled
37+
var $triggerParam : Text
38+
$triggerParam:=This:C1470.userParams.triggers || ""
39+
40+
// triggers=enabled means triggers are ON by default (testMode=false)
41+
// triggers=disabled (or omitted) means triggers are OFF by default (testMode=true)
42+
This:C1470.disableTriggersByDefault:=($triggerParam#"enabled")
43+
44+
Function _initializeTriggerControl()
45+
// Initialize trigger control flag in the appropriate Storage
46+
var $storageToUse : 4D:C1709.Object
47+
$storageToUse:=(This:C1470.hostStorage#Null:C1517) ? This:C1470.hostStorage : Storage:C1525
48+
49+
Use ($storageToUse)
50+
If ($storageToUse.triggersDisabled=Null:C1517)
51+
$storageToUse.triggersDisabled:=New shared object:C1526("testMode"; This:C1470.disableTriggersByDefault)
52+
Else
53+
Use ($storageToUse.triggersDisabled)
54+
$storageToUse.triggersDisabled.testMode:=This:C1470.disableTriggersByDefault
55+
End use
56+
End if
57+
End use
58+
59+
Function enableTriggersForTest()
60+
// Enable triggers for the current test (sets testMode=false)
61+
var $storageToUse : 4D:C1709.Object
62+
$storageToUse:=(This:C1470.hostStorage#Null:C1517) ? This:C1470.hostStorage : Storage:C1525
63+
64+
Use ($storageToUse)
65+
If ($storageToUse.triggersDisabled#Null:C1517)
66+
Use ($storageToUse.triggersDisabled)
67+
$storageToUse.triggersDisabled.testMode:=False:C215
68+
End use
69+
End if
70+
End use
71+
72+
Function disableTriggersForTest()
73+
// Disable triggers for the current test (sets testMode=true)
74+
var $storageToUse : 4D:C1709.Object
75+
$storageToUse:=(This:C1470.hostStorage#Null:C1517) ? This:C1470.hostStorage : Storage:C1525
76+
77+
Use ($storageToUse)
78+
If ($storageToUse.triggersDisabled#Null:C1517)
79+
Use ($storageToUse.triggersDisabled)
80+
$storageToUse.triggersDisabled.testMode:=True:C214
81+
End use
82+
End if
83+
End use
84+
85+
Function restoreDefaultTriggerBehavior()
86+
// Restore the default trigger behavior after a test
87+
var $storageToUse : 4D:C1709.Object
88+
$storageToUse:=(This:C1470.hostStorage#Null:C1517) ? This:C1470.hostStorage : Storage:C1525
89+
90+
Use ($storageToUse)
91+
If ($storageToUse.triggersDisabled#Null:C1517)
92+
Use ($storageToUse.triggersDisabled)
93+
$storageToUse.triggersDisabled.testMode:=This:C1470.disableTriggersByDefault
94+
End use
95+
End if
96+
End use
97+
2798
Function _prepareErrorHandlingStorage()
2899
Use (Storage:C1525)
29100
If (Storage:C1525.testErrors=Null:C1517)

0 commit comments

Comments
 (0)