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

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