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¶
- Create a Node Processor — Build the processor you want to test
- Node Processor Plugin System — Deep dive into processor internals