Skip to content

Testing Node Processors

FlowDrop node processors are standard Drupal plugins and can be unit tested with PHPUnit. Because processors implement a simple process(ParameterBagInterface $params): array method, they are straightforward to test without a Drupal bootstrap.

Test Environment Setup

FlowDrop ships with a DDEV-based development environment. Run tests with:

# Run all FlowDrop unit tests
ddev phpunit modules/

# Run tests for a specific module
ddev phpunit modules/flowdrop_node_processor/tests/

# Run a specific test class
ddev phpunit modules/flowdrop_node_processor/tests/src/Unit/Plugin/FlowDropNodeProcessor/HttpRequestTest.php

Tests use Drupal's UnitTestCase base class, which provides mocking utilities without requiring a full Drupal install.

Basic Unit Test Structure

Place tests in tests/src/Unit/Plugin/FlowDropNodeProcessor/ within your module. The test class should extend Drupal\Tests\UnitTestCase.

<?php

declare(strict_types=1);

namespace Drupal\Tests\my_module\Unit\Plugin\FlowDropNodeProcessor;

use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\my_module\Plugin\FlowDropNodeProcessor\MyProcessor;
use Drupal\Tests\UnitTestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the MyProcessor node processor.
 *
 * @coversDefaultClass \Drupal\my_module\Plugin\FlowDropNodeProcessor\MyProcessor
 * @group my_module
 */
class MyProcessorTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * Creates a processor instance for testing.
   */
  private function createProcessor(): MyProcessor {
    return new MyProcessor(
      [],                    // $configuration
      'my_processor',        // $plugin_id
      ['id' => 'my_processor'],  // $plugin_definition
    );
  }

  /**
   * @covers ::process
   */
  public function testProcessReturnsExpectedOutput(): void {
    $processor = $this->createProcessor();

    // Mock the ParameterBag.
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->get('input_text')->willReturn('hello world');
    $params->getString('mode', 'upper')->willReturn('upper');

    $result = $processor->process($params->reveal());

    $this->assertArrayHasKey('output', $result);
    $this->assertSame('HELLO WORLD', $result['output']);
  }

}

Mocking ParameterBagInterface

Use Prophecy (included with Drupal's testing infrastructure) to mock the parameter bag:

use Prophecy\Argument;

// Stub a specific parameter value:
$params->get('url')->willReturn('https://example.com');

// Stub typed accessors:
$params->getString('mode', 'default')->willReturn('custom');
$params->getInt('count', 0)->willReturn(5);
$params->getBool('enabled', FALSE)->willReturn(TRUE);
$params->getArray('items', [])->willReturn(['a', 'b', 'c']);

// Stub with a wildcard (match any default):
$params->get('optional_key', Argument::any())->willReturn(NULL);

Testing with Service Dependencies

If your processor injects services via create(), mock them in the test:

use Drupal\Core\Logger\LoggerChannelInterface;

private function createProcessor(): MyProcessor {
  $logger = $this->prophesize(LoggerChannelInterface::class);
  // Expect a specific log call:
  $logger->info(Argument::containingString('processed'))->shouldBeCalled();

  return new MyProcessor(
    [],
    'my_processor',
    ['id' => 'my_processor'],
    $logger->reveal(),
  );
}

Testing Exception Cases

Processors should throw InvalidNodeConfigurationException for bad configuration and NodeProcessingException for runtime failures:

use Drupal\flowdrop\Exception\InvalidNodeConfigurationException;
use Drupal\flowdrop\Exception\NodeProcessingException;

public function testEmptyUrlThrowsConfigurationException(): void {
  $processor = $this->createProcessor();
  $params = $this->prophesize(ParameterBagInterface::class);
  $params->get('url')->willReturn('');

  $this->expectException(InvalidNodeConfigurationException::class);
  $this->expectExceptionMessageMatches('/url.*required/i');

  $processor->process($params->reveal());
}

public function testExternalServiceFailureThrowsProcessingException(): void {
  // ... test that a failed API call throws NodeProcessingException
}

Testing with an HTTP Client

For processors that make HTTP requests (like a custom integration node), mock the Guzzle client:

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Drupal\Core\Http\ClientFactory;

private function createProcessor(int $statusCode = 200, string $body = '{}'): MyApiProcessor {
  $response = new Response($statusCode, [], $body);

  $client = $this->prophesize(Client::class);
  $client->request(Argument::cetera())->willReturn($response);

  $factory = $this->prophesize(ClientFactory::class);
  $factory->fromOptions(Argument::any())->willReturn($client->reveal());

  return new MyApiProcessor(
    [], 'my_api_processor', ['id' => 'my_api_processor'],
    $factory->reveal(),
  );
}

public function testSuccessfulApiCallReturnsData(): void {
  $processor = $this->createProcessor(200, '{"name": "test"}');
  $params = $this->prophesize(ParameterBagInterface::class);
  $params->get('endpoint')->willReturn('https://api.example.com/resource');

  $result = $processor->process($params->reveal());

  $this->assertSame('test', $result['name']);
}

public function testApiErrorReturnsFailureStatus(): void {
  $processor = $this->createProcessor(500, '{"error": "Internal Server Error"}');
  // ...
}

Subclassing to Stub External Calls

When a processor makes calls that are hard to mock (DNS lookups, file system access), create a testable subclass that overrides the specific method:

// In your test file:
class TestableDnsProcessor extends MyDnsProcessor {
  public ?string $mockedIp = '1.2.3.4';

  protected function resolveHostname(string $hostname): ?string {
    return $this->mockedIp;
  }
}

This pattern is used in HttpRequestTest.php to stub DNS resolution without real network calls.

Test File Location Convention

modules/my_module/
  tests/
    src/
      Unit/
        Plugin/
          FlowDropNodeProcessor/
            MyProcessorTest.php

Declare the test group with @group my_module to run your module's tests in isolation:

ddev phpunit --group my_module

Integration Testing with WorkflowExecutionTestBase

Unit tests cannot observe orchestration semantics — job scheduling, branch gating, tool wiring, state propagation. To pin how your node behaves inside a running workflow, extend the public kernel base Drupal\Tests\flowdrop\Kernel\WorkflowExecutionTestBase. It ships the full module closure and a small fixture/assertion helper set for the load-fixture → launch → assert-job-trail loop:

use Drupal\Tests\flowdrop\Kernel\WorkflowExecutionTestBase;

class MyProcessorIntegrationTest extends WorkflowExecutionTestBase {

  // Merged with the base module list — declare only your additions.
  protected static $modules = ['my_module'];

  protected function setUp(): void {
    parent::setUp();
    $this->installConfig(['my_module']);
  }

  public function testMyNodeRunsInAWorkflow(): void {
    $workflow = $this->createStateGraphWorkflow('my_fixture', [
      $this->node('start', 'my_node_type', ['option' => 'value']),
      $this->loggerNode('sink', 'done'),
    ], [
      $this->edge('start', 'result', 'sink', 'message'),
    ]);

    $result = $this->launchAndWait($workflow);

    $pipeline = $this->loadPipeline($result->pipelineId);
    $this->assertSame('completed', $result->status, $this->jobTrail($pipeline));
    $this->assertSame('completed', $this->jobStatusForNode($pipeline, 'start'));
    $this->assertSame('value', $this->jobOutputForNode($pipeline, 'start')['echoed'] ?? NULL);
  }

}

Helpers: createWorkflow() / createStateGraphWorkflow() (fixtures), node() / edge() plus convenience builders (loggerNode(), gatewayNode(), repeatNode(), forEachNode(), confirmationNode()), launchAndWait(), and the assertions loadPipeline(), jobStatusForNode(), jobOutputForNode(), jobTrail().

The in-tree orchestration suites (StateGraphBranchSemanticsTest, StateGraphExecutorParityTest, …) are built on the same base and double as usage examples.

Running PHPStan on Your Tests

FlowDrop enforces PHPStan level 6. After writing tests, verify they pass static analysis:

ddev phpstan analyse modules/my_module/tests/

Next Steps