Testing & Debugging Workflows

Comprehensive guide to testing and debugging workflows in Elsa Workflows, covering unit testing, integration testing, debugging techniques, test data management, CI/CD integration, and best practices.

Testing and debugging workflows is crucial for building reliable, production-ready workflow systems. This guide covers comprehensive strategies for testing workflows with xUnit and Elsa.Testing, integration testing patterns, debugging techniques, and best practices for workflow testing in Elsa V3.

Why Test Workflows?

Workflows often contain critical business logic that needs to be reliable and maintainable. Testing workflows provides:

  • Confidence: Ensure workflows behave correctly before deployment

  • Regression Prevention: Catch breaking changes early

  • Documentation: Tests serve as executable documentation

  • Refactoring Safety: Make changes without fear of breaking functionality

  • Quality Assurance: Validate business rules and edge cases

Unit Testing Workflows

Unit testing workflows involves testing individual workflows or activities in isolation. Elsa provides the Elsa.Testing package to make this process straightforward.

Setting Up Your Test Project

1

Create Test Project

Create a new xUnit test project and add the necessary packages:

dotnet new xunit -n "MyWorkflows.Tests"
cd MyWorkflows.Tests
dotnet add package Elsa
dotnet add package Elsa.Testing.Shared
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
2

Configure Test Infrastructure

Create a base test class to set up the Elsa service container:

WorkflowTestBase.cs
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Contracts;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace MyWorkflows.Tests;

public abstract class WorkflowTestBase : IAsyncLifetime
{
    protected IServiceProvider Services { get; private set; } = default!;
    
    public virtual async Task InitializeAsync()
    {
        var services = new ServiceCollection();
        
        // Add Elsa services
        services.AddElsa();
        
        // Add custom activities or services
        ConfigureServices(services);
        
        // Build the service provider
        Services = services.BuildServiceProvider();
        
        // Populate registries (required for non-hosted scenarios)
        await Services.PopulateRegistriesAsync();
    }
    
    protected virtual void ConfigureServices(IServiceCollection services)
    {
        // Override in derived classes to add custom services
    }
    
    public virtual Task DisposeAsync()
    {
        if (Services is IDisposable disposable)
            disposable.Dispose();
        
        return Task.CompletedTask;
    }
    
    protected async Task<WorkflowState> RunWorkflowAsync(Workflow workflow, IDictionary<string, object>? input = null, CancellationToken cancellationToken = default)
    {
        var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
        var result = await workflowRunner.RunAsync(workflow, input, cancellationToken);
        return result;
    }
}

Testing a Simple Workflow

Here's an example of testing a workflow that validates and processes user input:

Testing Custom Activities

When testing custom activities, focus on testing the activity's logic in isolation:

Testing Workflow Inputs and Outputs

Test workflows with various input combinations and verify outputs:

Using Elsa's Official Testing Helpers

Elsa provides official testing helper packages that simplify test setup and execution. These are the recommended approaches used in the elsa-core repository.

ActivityTestFixture for Unit Testing Activities

The ActivityTestFixture from Elsa.Testing.Shared package is the recommended way to unit test individual activities:

WorkflowTestFixture for Integration Testing

The WorkflowTestFixture from Elsa.Testing.Shared.Integration provides a complete test infrastructure with proper service setup:

Creating Execution Contexts

WorkflowTestFixture provides methods to create execution contexts at different levels:

Testing Async Workflows

For workflows that complete asynchronously (with timers, external triggers, etc.), use AsyncWorkflowRunner:

AsyncWorkflowRunner tracks activity execution records and properly awaits workflow completion signals, making it ideal for deterministic testing of asynchronous workflow behavior.

Integration Testing

Integration tests verify that workflows work correctly with external dependencies like databases, message queues, and HTTP services.

Testing with TestContainers

TestContainers allows you to run real dependencies in Docker containers during tests:

1

Install TestContainers

2

Create Integration Test Base

3

Write Integration Tests

Testing HTTP Workflows

Use ASP.NET Core's test server for testing HTTP-triggered workflows:

Testing with In-Memory Databases

For faster tests, use Entity Framework Core's in-memory database:

Debugging Workflow Execution

Debugging workflows requires understanding execution flow, state, and activity behavior.

Using the Execution Journal

The execution journal records every activity execution, providing a complete audit trail:

Logging Workflow Execution

Configure structured logging to debug workflows:

Using WriteLine Activity for Debugging

Insert WriteLine activities to trace execution flow:

Debugging with Breakpoints

Create a custom breakpoint activity for debugging:

Inspecting Workflow State

Access and inspect workflow state during execution:

Debug Workflow Failures

Handle and debug faulted workflows:

Testing Faulted Workflows

When testing error scenarios, verify that workflows and activities fault correctly:

When testing fault scenarios, prefer using _fixture.GetActivityStatus(result, activity) to check if a specific activity faulted, rather than only checking the workflow-level status. This provides more granular test assertions.

Test Data Management

Effective test data management ensures reliable and maintainable tests.

Test Data Builders

Use the builder pattern to create test data:

Test Fixtures

Use xUnit class fixtures for shared test data:

Parameterized Tests

Use Theory and InlineData for data-driven tests:

CI/CD Integration

Integrate workflow tests into your CI/CD pipeline for automated testing.

GitHub Actions

Azure DevOps

Test Categories

Organize tests with categories for selective execution:

Common Testing Pitfalls & Solutions

Pitfall 1: Not Populating Registries

Problem: Workflows fail with "Activity type not found" errors.

Solution: Always call PopulateRegistriesAsync() after building the service provider:

Pitfall 2: Shared State Between Tests

Problem: Tests fail intermittently due to shared state.

Solution: Use IAsyncLifetime to ensure clean state for each test:

Pitfall 3: Testing Async Workflows Synchronously

Problem: Workflows with delays or blocking activities don't complete in tests.

Solution: Use proper async/await and consider timeout strategies:

Pitfall 4: Not Testing Edge Cases

Problem: Workflows fail in production with unexpected input.

Solution: Test boundary conditions and invalid inputs:

Pitfall 5: Ignoring Disposal

Problem: Resource leaks and test failures due to undisposed resources.

Solution: Implement proper disposal patterns:

Pitfall 6: Hardcoded Wait Times

Problem: Tests are flaky due to race conditions or unnecessarily slow.

Solution: Use polling or workflow completion events instead of fixed delays:

Best Practices for Workflow Testing

1. Test Pyramid

Follow the testing pyramid principle:

  • Many Unit Tests: Fast, isolated tests for individual activities and simple workflows

  • Some Integration Tests: Test workflow persistence, external dependencies

  • Few End-to-End Tests: Full system tests including UI and APIs

2. Arrange-Act-Assert Pattern

Structure tests clearly:

3. Use Descriptive Test Names

Test names should describe what is being tested and expected outcome:

4. Test One Thing Per Test

Each test should verify a single behavior:

5. Mock External Dependencies

Isolate workflows from external systems in unit tests:

6. Use Test Helpers and Utilities

Create reusable test utilities:

7. Test Error Handling

Explicitly test failure scenarios:

8. Keep Tests Fast

Optimize test execution time:

  • Use in-memory databases for unit tests

  • Parallelize test execution where possible

  • Mock slow dependencies

  • Use test containers only for integration tests

9. Maintain Test Data

Keep test data close to tests and version controlled:

10. Document Complex Test Scenarios

Add comments for complex test logic:

Summary

Testing and debugging workflows is essential for building reliable workflow-based applications. This guide covered:

  • Unit Testing: Testing workflows and activities in isolation with xUnit and Elsa.Testing, including ActivityTestFixture for activity unit tests

  • Integration Testing: Using WorkflowTestFixture, TestContainers, and in-memory databases for integration tests

  • Async Testing: Using AsyncWorkflowRunner for testing workflows with timers and external triggers

  • Debugging: Techniques including execution journals, logging, breakpoints, and state inspection

  • Test Data Management: Builders, fixtures, and parameterized tests

  • CI/CD Integration: Automating tests in GitHub Actions and Azure DevOps

  • Common Pitfalls: Solutions to frequent testing challenges

  • Best Practices: Proven patterns for maintainable and effective workflow tests

By following these practices and patterns, along with the official Elsa testing helpers (ActivityTestFixture, WorkflowTestFixture, AsyncWorkflowRunner), you'll build a robust test suite that gives you confidence in your workflows and enables rapid, safe iteration on your workflow-based applications.

Additional Resources

Elsa Core Repository Examples

Testing Frameworks & Tools

Last updated