Skip to content

Commit 4fa4ebc

Browse files
authored
Merge pull request #13 from KyleKincer/codex/implement-table-driven-tests-for-4d
Add subtest support for table-driven tests
2 parents ccc2432 + bcd264c commit 4fa4ebc

File tree

5 files changed

+162
-17
lines changed

5 files changed

+162
-17
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ A comprehensive unit testing framework for the 4D platform with test tagging, fi
1010
- **Multiple output formats** - Human-readable and JSON output
1111
- **CI/CD ready** - Structured JSON output for automated testing
1212
- **Transaction management** - Automatic test isolation with rollback
13+
- **Subtests** - Run table-driven tests with `t.run`
1314

1415
## Quick Example
1516

@@ -65,6 +66,24 @@ tool4d --project YourProject.4DProject --startup-method "test" --user-param "tag
6566
tool4d --project YourProject.4DProject --startup-method "test" --user-param "tags=unit excludeTags=slow"
6667
```
6768

69+
## Table-Driven Tests
70+
71+
Use subtests to build table-driven tests. Each call to `t.run` executes the provided function with a fresh testing context. If a subtest fails, the parent test is marked as failed. Subtests run with the same `This` object as the parent test, so helper methods and state remain accessible. Pass optional data as the third argument when the test logic lives in a separate method.
72+
73+
```4d
74+
Function test_math($t : cs.Testing)
75+
var $cases : Collection
76+
$cases:=[New object("name"; "1+1"; "in"; 1; "want"; 2)]
77+
78+
var $case : Object
79+
For each ($case; $cases)
80+
$t.run($case.name; This._checkMathCase; $case)
81+
End for each
82+
83+
Function _checkMathCase($t : cs.Testing; $case : Object)
84+
$t.assert.areEqual($t; $case.want; $case.in+1; "math works")
85+
```
86+
6887
## Output
6988

7089
**Human format:**
@@ -94,4 +113,4 @@ tool4d --project YourProject.4DProject --startup-method "test" --user-param "tag
94113

95114
## License
96115

97-
MIT License
116+
MIT License

docs/guide.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A comprehensive unit testing framework for the 4D platform with enhanced reporti
1313
- [Test Filtering](#test-filtering)
1414
- [Test Tagging](#test-tagging)
1515
- [Test Lifecycle Methods](#test-lifecycle-methods)
16+
- [Table-Driven Tests](#table-driven-tests)
1617
- [Mocking and Test Utilities](#mocking-and-test-utilities)
1718
- [CI/CD Integration](#cicd-integration)
1819
- [Framework Architecture](#framework-architecture)
@@ -28,6 +29,7 @@ A comprehensive unit testing framework for the 4D platform with enhanced reporti
2829
- **Rich Assertions**: Built-in assertion library with helpful error messages
2930
- **Enhanced Reporting**: Detailed test results with execution times and pass rates
3031
- **JSON Output**: Structured output for CI/CD integration and automated processing
32+
- **Subtest Support**: Create table-driven tests using `t.run`
3133
- **Mock Support**: Built-in mocking utilities for isolated unit testing
3234
- **CI/CD Ready**: GitHub Actions integration for automated testing
3335

@@ -751,6 +753,24 @@ For each test class, the framework executes lifecycle methods in this order:
751753
- **Keep lifecycle methods lightweight** to minimize test execution overhead
752754
- **Handle errors gracefully** in cleanup methods to prevent test failures from masking real issues
753755

756+
## Table-Driven Tests
757+
758+
Use subtests to implement table-driven tests. The `t.run` method executes a named subtest with a fresh testing context. If a subtest fails, the parent test is marked as failed and the subtest's log messages are prefixed with its name. Subtests share the same `This` object as the parent test, so any instance methods or state remain available. Pass optional data as the third argument when using a helper method for the subtest logic.
759+
760+
```4d
761+
Function test_math_operations($t : cs.Testing.Testing)
762+
var $cases : Collection
763+
$cases:=[New object("name"; "1+1"; "in"; 1; "want"; 2)]
764+
765+
var $case : Object
766+
For each ($case; $cases)
767+
$t.run($case.name; This._checkMathCase; $case)
768+
End for each
769+
770+
Function _checkMathCase($t : cs.Testing.Testing; $case : Object)
771+
$t.assert.areEqual($t; $case.want; $case.in+1; "math works")
772+
```
773+
754774
## Transaction Management
755775

756776
The framework provides automatic transaction management for test isolation and manual transaction control for advanced scenarios.
@@ -843,4 +863,4 @@ The `$t` (Testing) context provides these transaction management methods:
843863

844864
## License
845865

846-
MIT License - see LICENSE file for details.
866+
MIT License - see LICENSE file for details.

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

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ property logMessages : Collection
66
property assert : cs:C1710.Assert
77
property stats : cs:C1710.UnitStatsTracker
88
property failureCallChain : Collection
9+
property classInstance : 4D:C1709.Object
910

1011
Class constructor()
1112
This:C1470.failed:=False:C215
@@ -36,8 +37,56 @@ Function resetForNewTest()
3637
This:C1470.stats.resetStatistics()
3738
This:C1470.failureCallChain:=Null
3839

39-
Function run($name : Text; $subtest : 4D:C1709.Function)
40-
// This will be implemented later
40+
Function run($name : Text; $subtest : 4D:C1709.Function; $data : Variant) : Boolean
41+
// Execute a named subtest with its own Testing context
42+
// Returns true if the subtest passed
43+
44+
var $subT : cs:C1710.Testing
45+
$subT:=cs:C1710.Testing.new()
46+
47+
// Share assertion and statistics objects with parent
48+
$subT.assert:=This:C1470.assert
49+
$subT.stats:=This:C1470.stats
50+
$subT.classInstance:=This:C1470.classInstance
51+
52+
var $result : Boolean
53+
$result:=True:C214
54+
55+
// Allow calling run with no subtest for compatibility
56+
If ($subtest=Null:C1517)
57+
return $result
58+
End if
59+
60+
// Execute the subtest with the parent test context
61+
var $context : 4D:C1709.Object
62+
$context:=This:C1470.classInstance
63+
If ($context=Null:C1517)
64+
$context:=This:C1470
65+
End if
66+
67+
var $args : Collection
68+
$args:=[$subT]
69+
If (Count parameters>=3)
70+
$args.push($data)
71+
End if
72+
$subtest.apply($context; $args)
73+
74+
// Propagate log messages with subtest name prefix
75+
var $message : Text
76+
For each ($message; $subT.logMessages)
77+
This:C1470.log($name+": "+$message)
78+
End for each
79+
80+
// If the subtest failed, mark parent as failed and capture call chain
81+
If ($subT.failed)
82+
This:C1470.fail()
83+
If ($subT.failureCallChain#Null)
84+
This:C1470.failureCallChain:=$subT.failureCallChain
85+
End if
86+
$result:=False:C215
87+
End if
88+
89+
return $result
4190

4291
// Transaction management methods for manual control
4392

@@ -163,4 +212,4 @@ Function formatCallChain() : Text
163212
End if
164213

165214
return $result
166-
215+

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Class constructor($class : 4D:C1709.Class; $classInstance : 4D:C1709.Object; $fu
1616
This:C1470.function:=$function
1717
This:C1470.functionName:=$name
1818
This:C1470.t:=cs:C1710.Testing.new()
19+
This:C1470.t.classInstance:=$classInstance
1920
This:C1470.runtimeErrors:=[]
2021
This:C1470.skipped:=False:C215
2122
This:C1470.tags:=This:C1470._parseTags($classCode || "")

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

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,71 @@ Function test_log_with_special_characters($t : cs:C1710.Testing)
113113
$t.assert.areEqual($t; "Message with\nnewlines"; $testing.logMessages[1]; "Should preserve newlines")
114114
$t.assert.areEqual($t; "Message with\ttabs"; $testing.logMessages[2]; "Should preserve tabs")
115115

116-
Function test_run_method_placeholder($t : cs:C1710.Testing)
117-
118-
var $testing : cs:C1710.Testing
119-
$testing:=cs:C1710.Testing.new()
120-
121-
// Test that run method exists (even if not implemented)
122-
// This is a placeholder method in the current implementation
123-
$t.assert.isNotNull($t; $testing.run; "Run method should exist")
124-
125-
// Call it to ensure it doesn't crash
126-
$testing.run("test"; Null:C1517)
127-
// Should not throw an error
116+
Function test_run_executes_subtest($t : cs:C1710.Testing)
117+
118+
var $testing : cs:C1710.Testing
119+
$testing:=cs:C1710.Testing.new()
120+
121+
var $result : Boolean
122+
$result:=$testing.run("sub"; Formula($1.log("ran")))
123+
124+
$t.assert.isTrue($t; $result; "Run should return true when subtest passes")
125+
$t.assert.areEqual($t; 1; $testing.logMessages.length; "Parent should capture subtest log")
126+
$t.assert.areEqual($t; "sub: ran"; $testing.logMessages[0]; "Log should be prefixed with subtest name")
127+
128+
Function test_run_propagates_failure($t : cs:C1710.Testing)
129+
130+
var $testing : cs:C1710.Testing
131+
$testing:=cs:C1710.Testing.new()
132+
133+
var $result : Boolean
134+
$result:=$testing.run("fail"; Formula($1.fail()))
135+
136+
$t.assert.isFalse($t; $result; "Run should return false when subtest fails")
137+
$t.assert.isTrue($t; $testing.failed; "Parent test should be marked as failed")
138+
139+
140+
Function test_run_uses_parent_context($t : cs:C1710.Testing)
141+
142+
// Verify that subtests execute with the same object context as the parent
143+
This.contextValue:="parent"
144+
145+
$t.run("ctx"; This._setContextChild)
146+
147+
$t.assert.areEqual($t; "child"; This.contextValue; "Subtest should run with parent This context")
148+
149+
Function _setContextChild($t : cs:C1710.Testing)
150+
151+
$t.log("running")
152+
This.contextValue:="child"
153+
154+
Function test_run_with_data_argument($t : cs:C1710.Testing)
155+
156+
var $testing : cs:C1710.Testing
157+
$testing:=cs:C1710.Testing.new()
158+
159+
var $cases : Collection
160+
$cases:=[\
161+
New object("name"; "ok"; "in"; 1; "want"; 2);\
162+
New object("name"; "bad"; "in"; 2; "want"; 4)\
163+
]
164+
165+
var $case : Object
166+
For each ($case; $cases)
167+
$testing.run($case.name; This._addOneCase; $case)
168+
End for each
169+
170+
$t.assert.areEqual($t; 2; $testing.logMessages.length; "Should log each case")
171+
$t.assert.areEqual($t; "ok: 2"; $testing.logMessages[0]; "Should prefix log with case name")
172+
$t.assert.areEqual($t; "bad: 3"; $testing.logMessages[1]; "Should prefix log with case name")
173+
$t.assert.isTrue($t; $testing.failed; "Parent should fail if any case fails")
174+
175+
Function _addOneCase($t : cs:C1710.Testing; $case : Object)
176+
177+
var $got : Integer
178+
$got:=$case.in+1
179+
$t.log(String($got))
180+
If ($got#$case.want)
181+
$t.fail()
182+
End if
183+

0 commit comments

Comments
 (0)