# Testing workflow definitions
# Rationale
You might wonder why testing workflows definitions is necessary at all. Aren't they simply configuration? Let's have a look at an abbreviated example from the docs: publishing a podcast.
class PublishPodcastWorkflow extends AbstractWorkflow
{
private Podcast $podcast;
public function __construct(Podcast $podcast)
{
$this->podcast = $podcast;
}
public function definition(): WorkflowDefinition
{
return Workflow::define('Publish Podcast')
->addJob(new ReleaseOnTransistorFM($this->podcast))
->addJob(new ReleaseOnApplePodcasts($this->podcast));
}
}
This is a pretty straight forward workflow without any branches or conditions. Not having tests for this is probably fine.
Now imagine that you're writing a podcasting platform that allows users to configure on which platforms they want to publish their podcasts. Now your definition might look something like this.
public function definition(): WorkflowDefinition
{
$workflow = Workflow::define('Publish Podcast')
->addJob(new ProcessPodcast($this->podcast));
if ($this->podcast->publish_on_apple_podcasts) {
$workflow->addJob(new ReleaseOnApplePodcasts($this->podcast));
}
if ($this->podcast->publish_on_transistor_fm) {
$workflow->addJob(new ReleaseOnTransistorFM($this->podcast));
}
return $workflow;
}
Now, there are 4 paths through this function:
- Publish only on Apple Podcasts
- Publish only on Transistor
- Publish to both Apple Podcasts and Transistor
- Don't publish to either platform
This is something you should probably test.
Another example could be to schedule the release of the podcast ahead of time, but still perform all the processing and optimizing as soon as possible.
public function definition(): WorkflowDefinition
{
return Workflow::define('Publish Podcast')
->addJob(new ProcessPodcast($this->podcast))
->addJob(new OptimizePodcast($this->podcast), [ProcessPodcast::class])
->addJobWithDelay(
new ReleaseOnApplePodcasts($this->podcast),
$podcast->release_date,
[OptimizePodcast::class]
)
->addJobWithDelay(
new ReleaseOnTransistorFM($this->podcast),
$podcast->release_date,
[OptimizePodcast::class]
);
}
In this example, you might want to test that ReleaseOnTransistorFM
and ReleaseOnApplePocasts
get scheduled with the correct delay. It gets even more complicated if you combine these two features.
public function definition(): WorkflowDefinition
{
$workflow = Workflow::define('Publish Podcast')
->addJob(new ProcessPodcast($this->podcast))
->addJob(new OptimizePodcast($this->podcast), [ProcessPodcast::class]);
if ($this->podcast->release_on_apple_podcasts) {
$workflow->addJobWithDelay(
new ReleaseOnApplePodcasts($this->podcast),
$podcast->release_date,
[OptimizePodcast::class]
);
}
if ($this->podcast->release_on_transistor) {
$workflow->addJobWithDelay(
new ReleaseOnTransistorFM($this->podcast),
$podcast->release_date,
[OptimizePodcast::class]
);
}
return $workflow;
}
I think you get the point by now but let's take it one step further to really drive the point home.
Let's assume that podcast optimization is a paid feature on your platform. As such, you give users an option to enable or disable it on a per-podcast basis.
This is a really interessting example because the dependency graph of your workflow actually changes depending on what the user selects. If podcast optimization is turned off, the ReleaseOnTransistorFM
and ReleaseOnApplePodcast
jobs should not depend on the OptimizePodcast
job (otherwise the workflow will throw an exception about an unresolvable dependency). In this case, they should depend on the ProcessPodcast
job instead.
public function definition(): WorkflowDefinition
{
$workflow = Workflow::define('Publish Podcast')
->addJob(new ProcessPodcast($this->podcast));
if ($this->podcast->optimization_enabled) {
$workflow->addJob(new OptimizePodcast($this->podcast), [
ProcessPodcast::class,
]);
}
if ($this->podcast->release_on_apple_podcasts) {
$workflow->addJobWithDelay(
new ReleaseOnApplePodcasts($this->podcast),
$this->podcast->release_date,
// Depend on different steps based on what was selected...
$this->podcast->optimization_enabled
? [OptimizePodcast::class]
: [ProcessPodcast::class]
);
}
if ($this->podcast->release_on_transistor) {
$workflow->addJobWithDelay(
new ReleaseOnTransitorFM($this->podcast),
$this->podcast->release_date,
// Depend on different steps based on what was selected...
$this->podcast->optimization_enabled
? [OptimizePodcast::class]
: [ProcessPodcast::class]
);
}
return $workflow;
}
This single workflow can now take on very different shapes depending on its input. For cases like these, Venture provides you with a few helper methods that allow you to check your workflow definitions for correctness.
# Inspecting Definitions
# Job exists
To check that a workflow contains a specific job, you can call the hasJob
method on your workflow definition. This will return a boolean to indicate if the given job is part of the workflow.
$podcast = new Podcast(['optimization_enabled' => true]);
$workflowDefinition = (new PublishPodcast($podcast))->definition();
$actual = $workflowDefinition->hasJob(OptimizePodcast::class);
$this->assertTrue($actual);
# Job exists with dependencies
You can also check if a job is part of the workflow and has the correct dependencies. To do this, you can use the hasJobWithDependencies
method on the definition.
$podcast = new Podcast([
'optimization_enabled' => true
'release_on_apple_podcasts' => true,
]);
$workflowDefinition = (new PublishPodcast($podcast))->definition();
$hasJob = $workflowDefinition->hasJobWithDependencies(
ReleaseOnApplePodcasts::class,
[OptimizePodcast::class]
);
$this->assertTrue($hasJob);
Note
hasJobWithDependencies
checks for an exact match of the job's dependencies so be sure to provide all dependencies the job should have.
# Job exists with delay
To verify that a job will be queued with the correct delay, you can use the hasJobWithDelay
method on the workflow definition.
Carbon::setTestNow(now());
$podcast = new Podcast([
'release_date' => now()->addDay(),
'release_on_apple_podcasts' => true,
]);
$workflowDefinition = (new PublishPodcast($podcast))->definition();
$hasJobWithDelay = $workflowDefinition->hasJobWithDelay(
ReleaseOnApplePodcasts::class,
now()->addDay()
);
$this->assertTrue($hasJobWithDelay);
# Checking for dependencies and delay
There are two ways to check if a job has both the correct dependencies and the right delay. You can either write two separate assertions using hasJobWithDependencies
and hasJobWithDelay
, respectively. Or you can pass all three parameters to the hasJob
method.
// Two separate assertions
$this->assertTrue($workflowDefinition->hasJobWithDependencies(
ReleaseOnApplePodcasts::class,
[OptimizePodcast::class]
));
$this->assertTrue($workflowDefinition->hasJobWithDelay(
ReleaseOnApplePodcasts::class,
$delay
));
// One assertion
$this->assertTrue($workflowDefinition->hasJob(
ReleaseOnApplePodcasts::class,
[OptimizePodcast::class],
$delay
));
# Workflow exists
To check that a workflow contains a nested workflow, you can use the hasWorkflow
method on the workflow definition object.
$this->assertTrue($workflowDefinition->hasWorkflow(
EncodePodcastWorkflow::class,
[ProcessPodcast::class]
));
This would check that the workflow definition contains a nested EncodePodcastWorkflow
that depends on the ProcessPodcast
job. If you don't care about the dependencies, you can leave out the second paramater (or pass null
).
// Just want to know that EncodePodcastWorkflow exists.
// We don't care about its dependencies.
$this->assertTrue($workflowDefinition->hasWorkflow(
EncodePodcastWorkflow::class,
));
Note
hasWorkflow
does not work recursively, meaning it will always return false
when checking for a workflow that is part of another nested workflow. You shouldn't test the internals of your dependencies. Instead, write another test for EncodePodcastWorkflow
and check for the nested workflow there.