# 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:

```bash
# 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
<?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](https://github.com/phpspec/prophecy) (included with Drupal's testing infrastructure) to mock the parameter bag:

```php
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:

```php
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:

```php
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:

```php
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:

```php
// 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:

```bash
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:

```php
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:

```bash
ddev phpstan analyse modules/my_module/tests/
```

## Next Steps

- **[Create a Node Processor](quick-reference/define-node.md)** — Build the processor you want to test
- **[Node Processor Plugin System](flowdrop-node-processor.md)** — Deep dive into processor internals
