README.md
1 # DriftKit Workflow Test Framework 2 3 A comprehensive testing framework for DriftKit workflows that provides powerful mocking capabilities, execution tracking, and assertion utilities. 4 5 ## Table of Contents 6 7 - [Features](#features) 8 - [Quick Start](#quick-start) 9 - [Core Concepts](#core-concepts) 10 - [API Reference](#api-reference) 11 - [Examples](#examples) 12 - [Best Practices](#best-practices) 13 - [Troubleshooting](#troubleshooting) 14 - [Additional Resources](#additional-resources) 15 16 ## Features 17 18 ✅ **Powerful Mocking System** 19 - Mock any workflow step with custom behavior 20 - Conditional mocking based on input data 21 - Retry-aware mocking with failure simulation 22 - Async step mocking support 23 24 ✅ **Execution Tracking** 25 - Track all workflow and step executions 26 - Verify execution paths and counts 27 - Access execution history and context 28 29 ✅ **Fluent Assertions** 30 - AssertJ-style workflow assertions 31 - Step-specific verification 32 - Execution order validation 33 34 ✅ **Framework Integration** 35 - Mockito integration for external dependencies 36 - Spring Boot test support 37 - JUnit 5 compatible 38 39 ## Quick Start 40 41 ### 1. Add Dependency 42 43 ```xml 44 <dependency> 45 <groupId>ai.driftkit</groupId> 46 <artifactId>driftkit-workflow-test-framework</artifactId> 47 <version>0.8.8</version> 48 <scope>test</scope> 49 </dependency> 50 ``` 51 52 ### 2. Create Your First Test 53 54 ```java 55 public class MyWorkflowTest extends WorkflowTestBase { 56 57 @Test 58 void testSimpleWorkflow() throws Exception { 59 // Define workflow 60 WorkflowBuilder<String, String> builder = WorkflowBuilder 61 .define("greeting-workflow", String.class, String.class) 62 .then("greet", (name, ctx) -> StepResult.finish("Hello, " + name)); 63 64 engine.register(builder); 65 66 // Execute and assert 67 String result = executeWorkflow("greeting-workflow", "World"); 68 assertEquals("Hello, World", result); 69 } 70 } 71 ``` 72 73 ### 3. Mock Workflow Steps 74 75 ```java 76 @Test 77 void testWithMocking() throws Exception { 78 // Setup mock 79 orchestrator.mock() 80 .workflow("my-workflow") 81 .step("external-service") 82 .always() 83 .thenReturn(String.class, input -> 84 StepResult.continueWith("Mocked response for: " + input)); 85 86 // Execute workflow 87 String result = executeWorkflow("my-workflow", "test input"); 88 89 // Verify mock was called 90 assertions.assertStep("my-workflow", "external-service") 91 .wasExecuted() 92 .withInput("test input"); 93 } 94 ``` 95 96 ## Architecture Overview 97 98 ### Class and Object Relationships 99 100 ``` 101 ┌─────────────────────────────────────────────────────────────────────────────────────┐ 102 │ DriftKit Workflow Test Framework │ 103 └─────────────────────────────────────────────────────────────────────────────────────┘ 104 105 ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ 106 │ WorkflowTestBase │────────▶│WorkflowTestOrchest. │────────▶│ WorkflowEngine │ 107 │ │ │ │ │ (from workflow │ 108 │ - engine │ │ - engine │ │ framework) │ 109 │ - orchestrator │ │ - interceptor │ │ │ 110 │ - assertions │ │ - mockRegistry │ │ - register() │ 111 │ │ │ │ │ - execute() │ 112 │ + executeWorkflow() │ │ + mock() │ │ - addInterceptor() │ 113 │ + executeAsync() │ │ + getEngine() │ │ │ 114 └─────────────────────┘ │ + getInterceptor() │ └─────────────────────┘ 115 │ └─────────────────────┘ │ 116 │ │ │ 117 │ ▼ ▼ 118 │ ┌─────────────────────┐ ┌─────────────────────┐ 119 │ │WorkflowTestIntercep.│◀────────│ WorkflowInterceptor │ 120 │ │ │ │ (interface from │ 121 │ │ - mockRegistry │ │ workflow) │ 122 │ │ - executionTracker │ │ │ 123 │ │ │ │ + beforeStep() │ 124 │ │ + beforeStep() │ │ + afterStep() │ 125 │ │ + afterStep() │ │ │ 126 │ └─────────────────────┘ └─────────────────────┘ 127 │ │ 128 │ ▼ 129 │ ┌─────────────────────┐ 130 │ │ MockRegistry │ 131 │ │ │ 132 │ │ - mocks: Map │ 133 │ │ │ 134 │ │ + register() │ 135 │ │ + findMock() │ 136 │ │ + clear() │ 137 │ └─────────────────────┘ 138 │ │ 139 │ ▼ 140 │ ┌─────────────────────┐ ┌─────────────────────┐ 141 │ │ MockBuilder │────────▶│ WorkflowMock │ 142 │ │ │ creates │ │ 143 │ │ - workflowId │ │ - condition │ 144 │ │ - stepId │ │ - behavior │ 145 │ │ │ │ - executionLimit │ 146 │ │ + always() │ │ │ 147 │ │ + when() │ │ + matches() │ 148 │ │ + times() │ │ + execute() │ 149 │ │ + thenReturn() │ │ + canExecute() │ 150 │ │ + thenFail() │ │ │ 151 │ │ + thenSucceed() │ └─────────────────────┘ 152 │ └─────────────────────┘ 153 │ 154 ▼ 155 ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ 156 │WorkflowStepAssert. │────────▶│ ExecutionTracker │◀────────│ StepExecution │ 157 │ │ uses │ │ tracks │ │ 158 │ - tracker │ │ - executions: Map │ │ - stepId │ 159 │ - workflowId │ │ │ │ - input │ 160 │ - stepId │ │ + recordExecution() │ │ - output │ 161 │ │ │ + getExecutions() │ │ - timestamp │ 162 │ + wasExecuted() │ │ + getExecutionCount()│ │ - error │ 163 │ + wasNotExecuted() │ │ + clear() │ │ │ 164 │ + withInput() │ │ │ └─────────────────────┘ 165 │ + producedOutput() │ └─────────────────────┘ 166 └─────────────────────┘ 167 │ 168 ▼ 169 ┌─────────────────────┐ ┌─────────────────────────────────────────────────────┐ 170 │EnhancedWorkflowAss. │────────▶│ Workflow Framework Objects │ 171 │ │ uses ├─────────────────────┬─────────────────────┬─────────┤ 172 │ + assertThat() │ │ WorkflowBuilder │ WorkflowExecution │StepResult│ 173 │ + isCompleted() │ │ │ │ │ 174 │ + isSuspended() │ │ + define() │ + getStatus() │+ finish()│ 175 │ + isFailed() │ │ + then() │ + awaitResult() │+ fail() │ 176 │ + hasResult() │ │ + branch() │ + resume() │+ suspend│ 177 │ + completedWithin() │ │ + thenWithRetry() │ │ │ 178 └─────────────────────┘ └─────────────────────┴─────────────────────┴─────────┘ 179 180 Key Relationships: 181 ─────▶ Uses/Depends on 182 ◀────▶ Implements interface 183 ────── Creates/Manages 184 ``` 185 186 ### Component Interactions 187 188 ``` 189 ┌──────────────────────────────────────────────────────────────────────────────────────┐ 190 │ Test Execution Flow │ 191 └──────────────────────────────────────────────────────────────────────────────────────┘ 192 193 Your Test Class Test Framework Workflow Framework 194 │ │ │ 195 │ extends │ │ 196 ├───────────────────────────▶│ WorkflowTestBase │ 197 │ │ │ 198 │ executeWorkflow() │ │ 199 ├───────────────────────────▶│ │ 200 │ │ │ 201 │ │ engine.execute() │ 202 │ ├───────────────────────────────────▶│ 203 │ │ │ 204 │ │ │ WorkflowEngine 205 │ │◀───beforeStep()────────────────────│ (intercepts) 206 │ │ │ 207 │ │ MockRegistry.findMock() │ 208 │ ├─────────────▶│ │ 209 │ │ │ │ 210 │ │◀─────────────┤ (mock found) │ 211 │ │ │ │ 212 │ │ mock.execute() │ 213 │ ├─────────────▶│ │ 214 │ │ │ │ 215 │ │ ExecutionTracker.record() │ 216 │ ├─────────────▶│ │ 217 │ │ │ │ 218 │ │ return StepResult │ 219 │ ├───────────────────────────────────▶│ 220 │ │ │ 221 │ │◀───afterStep()─────────────────────│ 222 │ │ │ 223 │◀───────────────────────────┤ WorkflowExecution │ 224 │ │ │ 225 │ assertions.assertStep() │ │ 226 ├───────────────────────────▶│ │ 227 │ │ ExecutionTracker.getExecutions() │ 228 │ ├─────────────▶│ │ 229 │ │ │ │ 230 │◀───────────────────────────┤ assertions pass/fail │ 231 │ │ │ 232 ``` 233 234 ### Key Design Principles 235 236 1. **Separation of Concerns** 237 - Test framework components are cleanly separated from workflow framework 238 - Interceptor pattern allows non-invasive testing 239 - Mock registry is independent of workflow execution 240 241 2. **Extensibility** 242 - Custom assertions can be added by extending base assertion classes 243 - Mock behaviors can be composed using builder pattern 244 - New interceptors can be added without modifying core framework 245 246 3. **Type Safety** 247 - Generic types preserve type information throughout execution 248 - Mock builders enforce type consistency between input and output 249 - Assertions provide compile-time type checking 250 251 4. **Testability** 252 - All components are designed to be easily testable 253 - Clear interfaces between components 254 - Minimal coupling between test and production code 255 256 ## Core Concepts 257 258 ### Test Base Class 259 260 All workflow tests should extend `WorkflowTestBase`: 261 262 ```java 263 public class MyTest extends WorkflowTestBase { 264 @Test 265 void testMyWorkflow() throws Exception { 266 // Your test logic here 267 } 268 } 269 ``` 270 271 ### Mock Builder API 272 273 The mock builder provides a fluent API for creating mocks: 274 275 ```java 276 // Always mock 277 orchestrator.mock() 278 .workflow("workflow-id") 279 .step("step-id") 280 .always() 281 .thenReturn(InputType.class, input -> StepResult.continueWith(output)); 282 283 // Conditional mock 284 orchestrator.mock() 285 .workflow("workflow-id") 286 .step("step-id") 287 .when(Order.class, order -> order.getAmount() > 1000) 288 .thenReturn(Order.class, order -> StepResult.continueWith(processHighValue(order))); 289 290 // Failure simulation 291 orchestrator.mock() 292 .workflow("workflow-id") 293 .step("step-id") 294 .times(2).thenFail(new ServiceException("Service unavailable")) 295 .afterwards().thenSucceed("Success after retry"); 296 ``` 297 298 ### Assertion API 299 300 The framework provides comprehensive assertion capabilities: 301 302 ```java 303 // Basic assertions 304 assertions.assertStep("workflow-id", "step-id") 305 .wasExecuted() 306 .wasExecutedTimes(3) 307 .wasNotExecuted(); 308 309 // Execution order 310 assertions.assertExecutionOrder() 311 .step("step1") 312 .step("step2") 313 .step("step3"); 314 315 // Advanced workflow assertions 316 EnhancedWorkflowAssertions.assertThat(execution, tracker) 317 .isCompleted() 318 .hasResult(expectedResult) 319 .completedWithin(Duration.ofSeconds(5)); 320 ``` 321 322 ## API Reference 323 324 ### WorkflowTestBase 325 326 Base class for all workflow tests. Provides utilities for executing and testing workflows. 327 328 **Example Usage:** 329 330 ```java 331 // Execute a workflow synchronously 332 public class SimpleWorkflowTest extends WorkflowTestBase { 333 @Test 334 void testSync() throws Exception { 335 String result = executeWorkflow("my-workflow", "input data"); 336 assertEquals("expected output", result); 337 } 338 339 // Execute asynchronously 340 @Test 341 void testAsync() throws Exception { 342 CompletableFuture<String> future = executeWorkflowAsync("my-workflow", "input"); 343 String result = future.get(5, TimeUnit.SECONDS); 344 assertNotNull(result); 345 } 346 347 // Test workflow suspension 348 @Test 349 void testSuspension() throws Exception { 350 WorkflowExecution<?> execution = executeAndExpectSuspend( 351 "approval-workflow", 352 new ApprovalRequest("REQ-123"), 353 Duration.ofSeconds(2) 354 ); 355 assertions.assertThat(execution).isSuspended(); 356 } 357 } 358 ``` 359 360 ### WorkflowTestOrchestrator 361 362 Central orchestration point for test configuration and mocking. 363 364 **Example Usage:** 365 366 ```java 367 // Access the orchestrator from your test 368 @Test 369 void testWithOrchestrator() { 370 // Start creating a mock 371 orchestrator.mock() 372 .workflow("payment-workflow") 373 .step("charge-card") 374 .always() 375 .thenSucceed(new PaymentResult(true, "TXN-123")); 376 377 // Access the workflow engine directly 378 WorkflowEngine engine = orchestrator.getEngine(); 379 engine.register(myWorkflowBuilder); 380 381 // Access the test interceptor for advanced scenarios 382 WorkflowTestInterceptor interceptor = orchestrator.getInterceptor(); 383 ExecutionTracker tracker = interceptor.getExecutionTracker(); 384 } 385 ``` 386 387 ### MockBuilder 388 389 Fluent API for creating sophisticated mocks with various behaviors. 390 391 **Example Usage:** 392 393 ```java 394 // Always execute mock 395 orchestrator.mock() 396 .workflow("order-workflow") 397 .step("validate") 398 .always() 399 .thenReturn(Order.class, order -> { 400 if (order.isValid()) { 401 return StepResult.continueWith(order); 402 } 403 return StepResult.fail("Invalid order"); 404 }); 405 406 // Conditional mock based on input 407 orchestrator.mock() 408 .workflow("pricing-workflow") 409 .step("calculate-discount") 410 .when(Customer.class, customer -> customer.isPremium()) 411 .thenSucceed(new Discount(0.2)); // 20% for premium 412 413 // Limited execution mock (useful for retry testing) 414 orchestrator.mock() 415 .workflow("resilient-workflow") 416 .step("external-api") 417 .times(2).thenFail(new ServiceException("API down")) 418 .afterwards().thenSucceed("API recovered"); 419 420 // Chain multiple behaviors 421 orchestrator.mock() 422 .workflow("complex-workflow") 423 .step("process") 424 .times(1).thenReturn(String.class, s -> StepResult.continueWith(s + "-first")) 425 .times(1).thenReturn(String.class, s -> StepResult.continueWith(s + "-second")) 426 .afterwards().thenFail(new RuntimeException("No more attempts")); 427 ``` 428 429 ### StepAssertions 430 431 Comprehensive assertions for verifying workflow execution behavior. 432 433 **Example Usage:** 434 435 ```java 436 // Basic execution verification 437 assertions.assertStep("order-workflow", "validate") 438 .wasExecuted(); 439 440 // Verify non-execution 441 assertions.assertStep("order-workflow", "rollback") 442 .wasNotExecuted(); 443 444 // Verify exact execution count 445 assertions.assertStep("payment-workflow", "charge-card") 446 .wasExecutedTimes(3); // Retried 3 times 447 448 // Verify input and output 449 assertions.assertStep("calculation-workflow", "calculate") 450 .wasExecuted() 451 .withInput(new CalculationRequest(100, 0.15)) 452 .producedOutput(new CalculationResult(115)); 453 454 // Verify execution order 455 assertions.assertExecutionOrder() 456 .step("validate") 457 .step("process") 458 .step("notify"); 459 460 // Chain multiple assertions 461 assertions.assertStep("workflow-id", "step-id") 462 .wasExecuted() 463 .withInput(inputData) 464 .producedOutput(expectedOutput) 465 .wasExecutedTimes(1); 466 ``` 467 468 ### EnhancedWorkflowAssertions 469 470 Advanced assertions for workflow state and behavior using AssertJ-style API. 471 472 **Example Usage:** 473 474 ```java 475 // Basic workflow assertions 476 WorkflowExecution<?> execution = engine.execute("my-workflow", input); 477 478 EnhancedWorkflowAssertions.assertThat(execution, tracker) 479 .isCompleted() 480 .hasResult(expectedResult) 481 .completedWithin(Duration.ofSeconds(5)) 482 .hasNoErrors(); 483 484 // Suspended workflow assertions 485 EnhancedWorkflowAssertions.assertThat(suspendedExecution, tracker) 486 .isSuspended() 487 .hasSuspensionData(expectedPrompt) 488 .isWaitingForInput(UserApproval.class); 489 490 // Failed workflow assertions 491 EnhancedWorkflowAssertions.assertThat(failedExecution, tracker) 492 .isFailed() 493 .hasError() 494 .hasErrorMessage("Service unavailable") 495 .hasErrorType(ServiceException.class); 496 497 // Complex assertions 498 EnhancedWorkflowAssertions.assertThat(execution, tracker) 499 .hasExecutedSteps("validate", "process", "notify") 500 .hasExecutedStepsInOrder("validate", "process", "notify") 501 .hasExecutedStepsCount(3) 502 .completedWithin(Duration.ofMillis(500)); 503 ``` 504 505 ## Examples 506 507 ### Testing Retry Behavior 508 509 ```java 510 @Test 511 void testRetryMechanism() throws Exception { 512 // Create workflow with retry 513 WorkflowBuilder<String, String> builder = WorkflowBuilder 514 .define("retry-workflow", String.class, String.class) 515 .thenWithRetry("flaky-service", 516 (input, ctx) -> callFlakyService(input), 517 RetryPolicyBuilder.retry() 518 .withMaxAttempts(3) 519 .withDelay(100) 520 .withRetryOnFailResult(true) 521 .build() 522 ); 523 524 engine.register(builder); 525 526 // Mock to fail twice then succeed 527 orchestrator.mock() 528 .workflow("retry-workflow") 529 .step("flaky-service") 530 .times(2).thenFail(new RuntimeException("Temporary failure")) 531 .afterwards().thenSucceed("Success!"); 532 533 // Execute and verify 534 String result = executeWorkflow("retry-workflow", "input"); 535 assertEquals("Success!", result); 536 } 537 ``` 538 539 ### Testing Conditional Workflows 540 541 ```java 542 @Test 543 void testConditionalBranching() throws Exception { 544 // Mock different responses based on input 545 orchestrator.mock() 546 .workflow("order-workflow") 547 .step("process-payment") 548 .when(Order.class, order -> order.isVip()) 549 .thenReturn(Order.class, order -> 550 StepResult.continueWith(new Payment(order, 0.9))); // 10% discount 551 552 // Test VIP order 553 Order vipOrder = new Order("VIP-001", 1000, true); 554 OrderResult vipResult = executeWorkflow("order-workflow", vipOrder); 555 assertEquals(900, vipResult.getFinalAmount()); // Discount applied 556 557 // Test regular order 558 Order regularOrder = new Order("REG-001", 1000, false); 559 OrderResult regularResult = executeWorkflow("order-workflow", regularOrder); 560 assertEquals(1000, regularResult.getFinalAmount()); // No discount 561 } 562 ``` 563 564 ### Testing Async Workflows 565 566 ```java 567 @Test 568 void testAsyncWorkflow() throws Exception { 569 // Mock async step 570 orchestrator.mockAsync() 571 .workflow("async-workflow") 572 .step("long-operation") 573 .completeAfter(Duration.ofMillis(500)) 574 .withResult("Async result"); 575 576 // Execute asynchronously 577 CompletableFuture<String> future = executeWorkflowAsync("async-workflow", "input"); 578 579 // Verify not completed immediately 580 assertFalse(future.isDone()); 581 582 // Wait and verify result 583 String result = future.get(1, TimeUnit.SECONDS); 584 assertEquals("Async result", result); 585 } 586 ``` 587 588 ### Testing External Service Integration 589 590 ```java 591 public class PaymentWorkflowTest extends WorkflowTestBase { 592 593 @Mock 594 private PaymentService paymentService; 595 596 @BeforeEach 597 void setUp() { 598 MockitoAnnotations.openMocks(this); 599 600 // Register workflow with mocked service 601 WorkflowBuilder<PaymentRequest, PaymentResult> builder = WorkflowBuilder 602 .define("payment-workflow", PaymentRequest.class, PaymentResult.class) 603 .then("charge", (req, ctx) -> { 604 PaymentResponse response = paymentService.charge(req); 605 return StepResult.finish(new PaymentResult(response)); 606 }); 607 608 engine.register(builder); 609 } 610 611 @Test 612 void testPaymentProcessing() throws Exception { 613 // Setup Mockito mock 614 when(paymentService.charge(any())) 615 .thenReturn(new PaymentResponse("TXN-123", true)); 616 617 // Execute workflow 618 PaymentResult result = executeWorkflow("payment-workflow", 619 new PaymentRequest("CARD-123", 100.00)); 620 621 // Verify 622 assertTrue(result.isSuccessful()); 623 assertEquals("TXN-123", result.getTransactionId()); 624 verify(paymentService).charge(any()); 625 } 626 } 627 ``` 628 629 ### Testing Workflow Suspend and Resume 630 631 ```java 632 @Test 633 void testWorkflowSuspendResume() throws Exception { 634 // Create workflow that suspends for approval 635 WorkflowBuilder<OrderRequest, OrderResult> builder = WorkflowBuilder 636 .define("approval-workflow", OrderRequest.class, OrderResult.class) 637 .then("validate", (order, ctx) -> StepResult.continueWith(order)) 638 .then("request-approval", (order, ctx) -> { 639 if (order.amount > 10000) { 640 return StepResult.suspend( 641 new ApprovalPrompt("High value order requires approval"), 642 ApprovalResponse.class 643 ); 644 } 645 return StepResult.continueWith(new ApprovalResponse(true, "AUTO")); 646 }) 647 .then("complete", (approval, ctx) -> { 648 OrderRequest order = (OrderRequest) ctx.getTriggerData(); 649 return StepResult.finish(new OrderResult(order.id, approval.approved)); 650 }); 651 652 engine.register(builder); 653 654 // Test high-value order suspension 655 OrderRequest highValueOrder = new OrderRequest("ORD-001", 15000); 656 WorkflowExecution<?> execution = executeAndExpectSuspend( 657 "approval-workflow", 658 highValueOrder, 659 Duration.ofSeconds(2) 660 ); 661 662 // Verify workflow is suspended 663 EnhancedWorkflowAssertions.assertThat(execution, tracker) 664 .isSuspended() 665 .hasSuspensionData(new ApprovalPrompt("High value order requires approval")); 666 667 // Resume workflow with approval 668 execution.resume(new ApprovalResponse(true, "MANAGER-123")); 669 OrderResult result = (OrderResult) execution.awaitResult(Duration.ofSeconds(5)); 670 671 // Verify final result 672 assertTrue(result.approved); 673 assertEquals("ORD-001", result.orderId); 674 } 675 ``` 676 677 ### Testing Complex Branching Logic 678 679 ```java 680 @Test 681 void testComplexBranchingWorkflow() throws Exception { 682 // Mock different paths based on customer type 683 orchestrator.mock() 684 .workflow("customer-workflow") 685 .step("check-credit") 686 .when(Customer.class, c -> c.type == CustomerType.PREMIUM) 687 .thenSucceed(new CreditResult(50000, true)); 688 689 orchestrator.mock() 690 .workflow("customer-workflow") 691 .step("check-credit") 692 .when(Customer.class, c -> c.type == CustomerType.REGULAR) 693 .thenSucceed(new CreditResult(10000, true)); 694 695 orchestrator.mock() 696 .workflow("customer-workflow") 697 .step("check-credit") 698 .when(Customer.class, c -> c.type == CustomerType.NEW) 699 .thenSucceed(new CreditResult(0, false)); 700 701 // Test each customer type 702 Customer premium = new Customer("C1", CustomerType.PREMIUM); 703 ProcessResult premiumResult = executeWorkflow("customer-workflow", premium); 704 assertEquals(50000, premiumResult.creditLimit); 705 assertions.assertStep("customer-workflow", "premium-benefits").wasExecuted(); 706 assertions.assertStep("customer-workflow", "standard-processing").wasNotExecuted(); 707 708 Customer regular = new Customer("C2", CustomerType.REGULAR); 709 ProcessResult regularResult = executeWorkflow("customer-workflow", regular); 710 assertEquals(10000, regularResult.creditLimit); 711 assertions.assertStep("customer-workflow", "standard-processing").wasExecuted(); 712 assertions.assertStep("customer-workflow", "premium-benefits").wasNotExecuted(); 713 714 Customer newCustomer = new Customer("C3", CustomerType.NEW); 715 ProcessResult newResult = executeWorkflow("customer-workflow", newCustomer); 716 assertEquals(0, newResult.creditLimit); 717 assertions.assertStep("customer-workflow", "onboarding").wasExecuted(); 718 } 719 ``` 720 721 ## Best Practices 722 723 ### Test Organization and Structure 724 725 ### 1. Test Organization 726 727 ```java 728 public class OrderWorkflowTest extends WorkflowTestBase { 729 730 private OrderWorkflow orderWorkflow; 731 732 @BeforeEach 733 void setUp() { 734 // Initialize workflow once 735 orderWorkflow = new OrderWorkflow(); 736 engine.register(orderWorkflow); 737 } 738 739 @Test 740 void testHappyPath() { } 741 742 @Test 743 void testErrorHandling() { } 744 745 @Test 746 void testRetryScenarios() { } 747 } 748 ``` 749 750 ### 2. Mock Isolation 751 752 ```java 753 @Test 754 void testWithIsolatedMocks() throws Exception { 755 // Each test should set up its own mocks 756 orchestrator.mock() 757 .workflow("my-workflow") 758 .step("external-api") 759 .always() 760 .thenReturn(Data.class, data -> processData(data)); 761 762 // Test logic here 763 764 // Mocks are automatically cleared after each test 765 } 766 ``` 767 768 ### 3. Assertion Best Practices 769 770 ```java 771 @Test 772 void testComplexWorkflow() throws Exception { 773 // Execute workflow 774 OrderResult result = executeWorkflow("order-workflow", order); 775 776 // Use descriptive assertions 777 assertThat(result) 778 .as("Order should be processed successfully") 779 .isNotNull() 780 .extracting(OrderResult::getStatus) 781 .isEqualTo("COMPLETED"); 782 783 // Verify execution path 784 assertions.assertExecutionOrder() 785 .as("Should follow happy path") 786 .step("validate") 787 .step("process-payment") 788 .step("send-confirmation"); 789 } 790 ``` 791 792 ### 4. Use Descriptive Test Names 793 794 ```java 795 @Test 796 void shouldRetryPaymentThreeTimesBeforeFailing() { } 797 798 @Test 799 void shouldRoutePremiumOrdersThroughExpressProcessing() { } 800 801 @Test 802 void shouldSuspendWorkflowWhenApprovalRequired() { } 803 ``` 804 805 ### 5. Retry Testing 806 807 When testing retries, use the mock builder's retry-specific features: 808 809 ```java 810 @Test 811 void testRetryBehavior() throws Exception { 812 // Simulate failures followed by success 813 orchestrator.mock() 814 .workflow("payment-workflow") 815 .step("charge-api") 816 .times(2).thenFail(new ServiceException("Temporary outage")) 817 .afterwards().thenSucceed(new ChargeResult("TXN-123", true)); 818 819 // Configure retry policy 820 var retryPolicy = RetryPolicyBuilder.retry() 821 .withMaxAttempts(3) 822 .withDelay(100) 823 .withBackoffMultiplier(2.0) 824 .build(); 825 826 // Execute workflow with retry-enabled step 827 ChargeResult result = executeWorkflow("payment-workflow", 828 new ChargeRequest("CARD-123", 99.99)); 829 830 // Verify successful completion after retries 831 assertTrue(result.success()); 832 assertEquals("TXN-123", result.transactionId()); 833 } 834 835 @Test 836 void testRetryExhaustion() throws Exception { 837 // Always fail to test retry exhaustion 838 orchestrator.mock() 839 .workflow("unreliable-workflow") 840 .step("always-fails") 841 .always() 842 .thenFail(new PermanentException("Service decommissioned")); 843 844 // Expect workflow to fail after max attempts 845 assertThrows(WorkflowException.class, () -> { 846 executeWorkflow("unreliable-workflow", "input"); 847 }); 848 } 849 ``` 850 851 ## Troubleshooting 852 853 ### Common Issues and Solutions 854 855 **Mock Not Being Called** 856 857 ```java 858 // Problem: Mock not triggering 859 // Solution: Check exact IDs and branch prefixes 860 @Test 861 void debugMockIssues() { 862 // Enable debug logging first 863 Logger logger = LoggerFactory.getLogger("ai.driftkit.workflow.test"); 864 ((ch.qos.logback.classic.Logger) logger).setLevel(Level.DEBUG); 865 866 // Use tracker to inspect actual step IDs 867 executeWorkflow("my-workflow", input); 868 869 // Print all executed steps to see exact IDs 870 tracker.getAllExecutions().forEach((key, executions) -> { 871 System.out.println("Step executed: " + key); 872 }); 873 874 // Common issue: branch prefixes 875 // Instead of: orchestrator.mock().step("process") 876 // Use: orchestrator.mock().step("true_1_process") 877 } 878 ``` 879 880 **Retry Test Issues** 881 882 ```java 883 // Problem: Retries not working with StepResult.fail() 884 // Solution: Enable retryOnFailResult 885 @Test 886 void fixRetryIssues() { 887 var retryPolicy = RetryPolicyBuilder.retry() 888 .withMaxAttempts(3) 889 .withRetryOnFailResult(true) // Critical for StepResult.fail() 890 .build(); 891 892 // Mock configuration for retries 893 orchestrator.mock() 894 .workflow("retry-workflow") 895 .step("flaky-step") 896 .times(2).thenReturn(Input.class, i -> StepResult.fail("Temporary issue")) 897 .afterwards().thenReturn(Input.class, i -> StepResult.continueWith("Success")); 898 } 899 ``` 900 901 **Async Test Timing** 902 903 ```java 904 // Problem: Async tests timing out 905 // Solution: Adjust timeouts and verify mock delays 906 @Test 907 void handleAsyncTiming() { 908 // Configure async mock with realistic delay 909 orchestrator.mockAsync() 910 .workflow("async-workflow") 911 .step("slow-operation") 912 .completeAfter(Duration.ofMillis(200)) // Not too long 913 .withResult("Done"); 914 915 // Use appropriate timeout 916 CompletableFuture<String> future = executeWorkflowAsync("async-workflow", input); 917 String result = future.get(1, TimeUnit.SECONDS); // Generous timeout 918 } 919 ``` 920 921 ### Debug Utilities 922 923 ```java 924 // Utility method to debug workflow execution 925 private void debugWorkflow(String workflowId) { 926 System.out.println("=== Workflow Debug Info ==="); 927 928 // List all registered workflows 929 engine.getRegisteredWorkflows().forEach(id -> 930 System.out.println("Registered workflow: " + id) 931 ); 932 933 // List all mocked steps 934 orchestrator.getInterceptor().getMockRegistry().getAllMocks() 935 .forEach((key, mock) -> 936 System.out.println("Mocked step: " + key) 937 ); 938 939 // Show execution history 940 tracker.getAllExecutions().forEach((step, execs) -> { 941 System.out.println("Step " + step + " executed " + execs.size() + " times"); 942 execs.forEach(exec -> { 943 System.out.println(" Input: " + exec.getInput()); 944 System.out.println(" Output: " + exec.getOutput()); 945 }); 946 }); 947 } 948 ``` 949 950 ## Advanced Testing Patterns 951 952 ### Mock Design Patterns 953 954 **1. Use Builders for Complex Mocks** 955 956 ```java 957 @Test 958 void shouldHandleComplexPaymentScenarios() { 959 // Create a mock builder helper 960 MockBuilder paymentMock = orchestrator.mock() 961 .workflow("payment-workflow") 962 .step("charge-card"); 963 964 // Configure different behaviors for different inputs 965 paymentMock.when(PaymentRequest.class, req -> req.amount > 10000) 966 .thenReturn(PaymentRequest.class, req -> 967 StepResult.suspend(new FraudReviewPrompt(req), FraudDecision.class)); 968 969 paymentMock.when(PaymentRequest.class, req -> req.cardType == CardType.CORPORATE) 970 .thenReturn(PaymentRequest.class, req -> 971 StepResult.continueWith(new PaymentResult(req.amount * 0.98))); // 2% discount 972 973 paymentMock.when(PaymentRequest.class, req -> req.amount < 0) 974 .thenFail(new ValidationException("Invalid amount")); 975 } 976 ``` 977 978 **2. Create Reusable Mock Factories** 979 980 ```java 981 public class MockFactory { 982 983 public static void setupFlakeyServiceMock(WorkflowTestOrchestrator orchestrator, 984 String workflowId, 985 String stepId, 986 int failureCount) { 987 orchestrator.mock() 988 .workflow(workflowId) 989 .step(stepId) 990 .times(failureCount).thenFail(new ServiceUnavailableException("Service down")) 991 .afterwards().thenSucceed("Service recovered"); 992 } 993 } 994 ``` 995 996 ### Assertion Strategies 997 998 **1. Layer Your Assertions** 999 1000 ```java 1001 @Test 1002 void shouldProcessOrderCompleteFlow() throws Exception { 1003 // Given 1004 Order order = new Order("ORD-123", 150.00, CustomerType.PREMIUM); 1005 1006 // When 1007 OrderResult result = executeWorkflow("order-workflow", order); 1008 1009 // Then - Layer 1: Basic result assertions 1010 assertNotNull(result); 1011 assertTrue(result.isSuccessful()); 1012 assertEquals("ORD-123", result.orderId); 1013 1014 // Layer 2: Execution flow assertions 1015 assertions.assertExecutionOrder() 1016 .step("validate-order") 1017 .step("check-inventory") 1018 .step("calculate-pricing") 1019 .step("process-payment") 1020 .step("ship-order") 1021 .step("send-notification"); 1022 1023 // Layer 3: Step-specific assertions 1024 assertions.assertStep("order-workflow", "calculate-pricing") 1025 .producedOutput(new PricingResult(150.00, 0.10, 135.00)); // 10% discount 1026 1027 // Layer 4: Performance assertions 1028 EnhancedWorkflowAssertions.assertThat(execution, tracker) 1029 .completedWithin(Duration.ofSeconds(2)); 1030 } 1031 ``` 1032 1033 **2. Create Custom Assertions** 1034 1035 ```java 1036 public class OrderAssertions { 1037 1038 public static void assertOrderProcessedCorrectly( 1039 WorkflowStepAssertions assertions, 1040 Order order, 1041 OrderResult result) { 1042 1043 // Verify order basics 1044 assertEquals(order.orderId, result.orderId); 1045 1046 // Verify workflow execution based on order type 1047 if (order.amount > 1000) { 1048 assertions.assertStep("order-workflow", "fraud-check").wasExecuted(); 1049 } else { 1050 assertions.assertStep("order-workflow", "fraud-check").wasNotExecuted(); 1051 } 1052 1053 // Verify customer-specific routing 1054 if (order.customerType == CustomerType.PREMIUM) { 1055 assertions.assertStep("order-workflow", "premium-benefits").wasExecuted(); 1056 } 1057 } 1058 } 1059 ``` 1060 1061 ### Performance Testing 1062 1063 ```java 1064 @Test 1065 void shouldCompleteWithinSLA() throws Exception { 1066 // Arrange 1067 int concurrentRequests = 10; 1068 Duration maxExecutionTime = Duration.ofSeconds(5); 1069 1070 // Act 1071 List<CompletableFuture<OrderResult>> futures = IntStream.range(0, concurrentRequests) 1072 .mapToObj(i -> executeWorkflowAsync("order-workflow", 1073 new Order("ORD-" + i, 100.00 * i))) 1074 .collect(Collectors.toList()); 1075 1076 // Assert 1077 long startTime = System.currentTimeMillis(); 1078 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) 1079 .get(maxExecutionTime.toMillis(), TimeUnit.MILLISECONDS); 1080 long executionTime = System.currentTimeMillis() - startTime; 1081 1082 assertTrue(executionTime < maxExecutionTime.toMillis(), 1083 "Workflow execution took " + executionTime + "ms, exceeding SLA of " + maxExecutionTime); 1084 1085 // Verify all completed successfully 1086 futures.forEach(future -> { 1087 assertTrue(future.isDone()); 1088 assertDoesNotThrow(() -> future.get()); 1089 }); 1090 } 1091 ``` 1092 1093 ### Error Recovery Testing 1094 1095 ```java 1096 @Test 1097 void shouldRecoverFromTransientErrors() throws Exception { 1098 // Setup recovery scenario 1099 AtomicInteger attemptCount = new AtomicInteger(0); 1100 1101 orchestrator.mock() 1102 .workflow("resilient-workflow") 1103 .step("unstable-service") 1104 .always() 1105 .thenReturn(Request.class, req -> { 1106 int attempt = attemptCount.incrementAndGet(); 1107 if (attempt <= 2) { 1108 // Fail first two attempts 1109 throw new TransientException("Service temporarily unavailable"); 1110 } 1111 return StepResult.continueWith("Service call succeeded on attempt " + attempt); 1112 }); 1113 1114 // Execute with retry policy 1115 String result = executeWorkflow("resilient-workflow", new Request()); 1116 1117 // Verify recovery 1118 assertEquals(3, attemptCount.get()); 1119 assertThat(result).contains("attempt 3"); 1120 } 1121 ``` 1122 1123 ## Common Pitfalls to Avoid 1124 1125 ### 1. Avoid Over-Mocking 1126 1127 **❌ BAD: Mocking everything** 1128 ```java 1129 @Test 1130 void testOverMocked() { 1131 // Mocking internal logic - makes test brittle 1132 orchestrator.mock().workflow("wf").step("internal-calc").always().thenSucceed(42); 1133 orchestrator.mock().workflow("wf").step("format").always().thenSucceed("42"); 1134 orchestrator.mock().workflow("wf").step("validate").always().thenSucceed(true); 1135 1136 // Test doesn't actually test the workflow logic! 1137 } 1138 ``` 1139 1140 **✅ GOOD: Mock only external dependencies** 1141 ```java 1142 @Test 1143 void testProperMocking() { 1144 // Only mock external service calls 1145 orchestrator.mock() 1146 .workflow("calculation-workflow") 1147 .step("fetch-exchange-rate") 1148 .always() 1149 .thenSucceed(1.2); // Mock external API 1150 1151 // Let internal calculation logic run normally 1152 CalculationResult result = executeWorkflow("calculation-workflow", 1153 new CalculationInput(100, "USD", "EUR")); 1154 1155 // Verify actual calculation logic 1156 assertEquals(120.0, result.convertedAmount, 0.01); 1157 } 1158 ``` 1159 1160 ### 2. Handle Timing Issues Properly 1161 1162 **❌ BAD: Using fixed sleeps** 1163 ```java 1164 @Test 1165 void testBadTiming() throws Exception { 1166 CompletableFuture<Result> future = executeWorkflowAsync("async-workflow", input); 1167 Thread.sleep(1000); // Brittle! May fail on slow systems 1168 assertTrue(future.isDone()); 1169 } 1170 ``` 1171 1172 **✅ GOOD: Use proper waiting mechanisms** 1173 ```java 1174 @Test 1175 void testProperTiming() throws Exception { 1176 CompletableFuture<Result> future = executeWorkflowAsync("async-workflow", input); 1177 1178 // Wait with timeout 1179 Result result = future.get(5, TimeUnit.SECONDS); 1180 assertNotNull(result); 1181 1182 // Or use assertions that wait 1183 await().atMost(Duration.ofSeconds(5)) 1184 .untilAsserted(() -> { 1185 assertTrue(future.isDone()); 1186 assertEquals(expectedResult, future.get()); 1187 }); 1188 } 1189 ``` 1190 1191 ### 3. Clean Up Resources 1192 1193 ```java 1194 public class ResourceIntensiveTest extends WorkflowTestBase { 1195 1196 private ExecutorService executorService; 1197 1198 @BeforeEach 1199 void setUp() { 1200 executorService = Executors.newFixedThreadPool(10); 1201 } 1202 1203 @AfterEach 1204 void tearDown() { 1205 // Always clean up resources 1206 if (executorService != null) { 1207 executorService.shutdownNow(); 1208 } 1209 } 1210 } 1211 ``` 1212 1213 ## Additional Resources 1214 1215 ### Advanced Examples 1216 1217 For more complex testing scenarios, see [ADVANCED_EXAMPLES.md](ADVANCED_EXAMPLES.md) which covers: 1218 1219 - **Stateful Workflow Testing** - Managing complex state across workflow steps 1220 - **Multi-Stage Pipeline Testing** - Testing ETL and data processing workflows 1221 - **Event-Driven Workflow Testing** - Workflows that emit and react to events 1222 - **Saga Pattern Testing** - Distributed transactions with compensation 1223 - **Dynamic Workflow Testing** - Workflows that generate steps at runtime 1224 - **Integration Testing** - Testing with real external systems using TestContainers 1225 1226 ### API Documentation 1227 1228 For detailed API documentation and method signatures, refer to the Javadocs or explore the source code: 1229 1230 - [WorkflowTestBase](src/main/java/ai/driftkit/workflow/test/core/WorkflowTestBase.java) - Base test class 1231 - [MockBuilder](src/main/java/ai/driftkit/workflow/test/mocks/MockBuilder.java) - Mock configuration API 1232 - [WorkflowStepAssertions](src/main/java/ai/driftkit/workflow/test/assertions/WorkflowStepAssertions.java) - Assertion utilities 1233 - [EnhancedWorkflowAssertions](src/main/java/ai/driftkit/workflow/test/assertions/EnhancedWorkflowAssertions.java) - Advanced assertions 1234 1235 ### Getting Help 1236 1237 - **Issues**: Report bugs or request features on our [GitHub Issues](https://github.com/driftkit/workflow-test-framework/issues) 1238 - **Discussions**: Join our community discussions for questions and best practices 1239 - **Documentation**: Full documentation available at [docs.driftkit.ai](https://docs.driftkit.ai) 1240 1241 ## Contributing 1242 1243 Contributions are welcome! Please: 1244 1245 1. Fork the repository 1246 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 1247 3. Commit your changes (`git commit -m 'Add amazing feature'`) 1248 4. Push to the branch (`git push origin feature/amazing-feature`) 1249 5. Open a Pull Request 1250 1251 See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. 1252 1253 ## License 1254 1255 This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.