TestPlan Expansion¶
How FHIR Frog expands TestPlans into executable JUnit tests.
Expansion Process¶
TestPlans are dynamically expanded at runtime:
- Load TestPlan - Read from server or classpath
- Resolve references - Load referenced TestScripts/TestPlans
- Build dependency graph - Analyze dependencies
- Detect cycles - Fail if circular dependencies found
- Topological sort - Determine execution order
- Generate JUnit tests - Create dynamic test methods
- Execute - Run tests in order
Simple Expansion¶
TestPlan¶
{
"resourceType": "TestPlan",
"id": "patient-suite",
"testCase": [
{
"testRun": [
{"script": {"reference": "TestScript/patient-create"}},
{"script": {"reference": "TestScript/patient-read"}},
{"script": {"reference": "TestScript/patient-delete"}}
]
}
]
}
Expanded JUnit Tests¶
@TestPlan("TestPlan/patient-suite")
class PatientSuite {
@Test
@DisplayName("patient-create")
void testPatientCreate() {
// Execute TestScript/patient-create
}
@Test
@DisplayName("patient-read")
void testPatientRead() {
// Execute TestScript/patient-read
}
@Test
@DisplayName("patient-delete")
void testPatientDelete() {
// Execute TestScript/patient-delete
}
}
Recursive Expansion¶
Nested TestPlans¶
{
"resourceType": "TestPlan",
"id": "au-core-suite",
"testCase": [
{
"testRun": [
{"script": {"reference": "TestPlan/patient-suite"}},
{"script": {"reference": "TestPlan/practitioner-suite"}}
]
}
]
}
Expanded Structure¶
AUCoreSuite
├── PatientSuite
│ ├── patient-create
│ ├── patient-read
│ └── patient-delete
└── PractitionerSuite
├── practitioner-create
├── practitioner-read
└── practitioner-delete
Dependency Expansion¶
With Dependencies¶
{
"testCase": [
{
"sequence": 1,
"testRun": [{"script": {"reference": "TestScript/setup"}}]
},
{
"sequence": 2,
"dependency": [{"predecessor": "TestScript/setup"}],
"testRun": [
{"script": {"reference": "TestScript/test1"}},
{"script": {"reference": "TestScript/test2"}}
]
}
]
}
Expanded with Order¶
@TestPlan("TestPlan/with-deps")
class WithDependencies {
@Test
@Order(1)
@DisplayName("setup")
void testSetup() {}
@Test
@Order(2)
@DisplayName("test1")
void testTest1() {}
@Test
@Order(2) // Same order - can run in parallel
@DisplayName("test2")
void testTest2() {}
}
Expansion Diagram¶
Cycle Detection¶
Detection Algorithm¶
public void detectCycles(TestPlan plan) {
Set<String> visiting = new HashSet<>();
Set<String> visited = new HashSet<>();
for (TestCase testCase : plan.getTestCase()) {
if (hasCycle(testCase, visiting, visited)) {
throw new CycleDetectedException();
}
}
}
Example Cycle¶
Error:
Execution Order¶
Topological Sort¶
Input Dependencies:
A: []
B: [A]
C: [A]
D: [B, C]
Topological Sort:
Level 1: A
Level 2: B, C (parallel)
Level 3: D
JUnit Execution¶
@TestPlan("TestPlan/sorted")
class SortedTests {
@Test
@Order(1)
void testA() {}
@Test
@Order(2)
void testB() {}
@Test
@Order(2) // Parallel with testB
void testC() {}
@Test
@Order(3)
void testD() {}
}
Dynamic Test Generation¶
JUnit 5 TestFactory¶
@TestFactory
Stream<DynamicTest> expandTestPlan() {
TestPlan plan = loadTestPlan("TestPlan/suite");
List<TestScript> scripts = expand(plan);
return scripts.stream()
.map(script -> DynamicTest.dynamicTest(
script.getName(),
() -> executeTestScript(script)
));
}
Nested Containers¶
@TestFactory
Stream<DynamicContainer> expandNestedTestPlan() {
TestPlan plan = loadTestPlan("TestPlan/nested");
return plan.getTestCase().stream()
.map(testCase -> {
List<DynamicTest> tests = expandTestCase(testCase);
return DynamicContainer.dynamicContainer(
testCase.getNarrative(),
tests.stream()
);
});
}
Expansion Limits¶
Maximum Depth¶
Prevent infinite recursion:
private static final int MAX_DEPTH = 10;
public List<TestScript> expand(TestPlan plan, int depth) {
if (depth > MAX_DEPTH) {
throw new MaxDepthExceededException();
}
// Expand...
}
Maximum Tests¶
Limit total test count:
private static final int MAX_TESTS = 1000;
public List<TestScript> expand(TestPlan plan) {
List<TestScript> scripts = new ArrayList<>();
expandRecursive(plan, scripts);
if (scripts.size() > MAX_TESTS) {
throw new TooManyTestsException();
}
return scripts;
}
Performance¶
Caching¶
Cache expanded TestPlans:
private Map<String, List<TestScript>> cache = new HashMap<>();
public List<TestScript> expand(TestPlan plan) {
String key = plan.getId();
if (cache.containsKey(key)) {
return cache.get(key);
}
List<TestScript> scripts = doExpand(plan);
cache.put(key, scripts);
return scripts;
}
Parallel Expansion¶
Expand independent branches in parallel:
public List<TestScript> expandParallel(TestPlan plan) {
return plan.getTestCase().parallelStream()
.flatMap(testCase -> expandTestCase(testCase).stream())
.collect(Collectors.toList());
}
Best Practices¶
Keep It Simple
Avoid deep nesting. Aim for 2-3 levels maximum.
Watch Performance
Large TestPlans with many nested references can be slow to expand.
- Limit nesting depth - Max 2-3 levels
- Cache expansions - Reuse expanded plans
- Test expansion - Verify no cycles before production
- Monitor performance - Track expansion time
- Document structure - Explain TestPlan hierarchy
Next Steps¶
- Structure - TestPlan structure
- Dependencies - Managing dependencies
- Overview - TestPlan basics