zeebe_bpmn_rspec
This gem provides support for testing BPMN files using RSpec with the Zeebe workflow engine.
The gem adds RSpec helpers that are used to interact with Zeebe and a running workflow instance.
Installation
Add this line to the test group in your application's Gemfile:
group :test do
gem "zeebe_bpmn_rspec"
end
And then execute:
$ bundle install
Or install it yourself as:
$ gem install zeebe_bpmn_rspec
Configuration
Either the address for the Zeebe workflow engine or a Zeebe client must be configured.
ZEEBE_ADDRESS
if used from the environment if this is not configured.
ZeebeBpmnRspec.configure do |config|
config.zeebe_address = "localhost:26500"
# -OR-
config.client = #<Zeebe::Client::GatewayProtocol::Gateway::Stub instance>
end
Usage
The gem adds the following helper methods to RSpec.
The gem also defines Custom Matchers.
Deploy Process
The deploy_process
(previously deploy_workflow
) method requires a path to a BPMN file and deploys it to Zeebe. There is no support for
removing a BPMN file once deployed, so this can be done once before the examples that use it.
before(:all) { deploy_process(filepath) }
A custom name can also be specified for the process:
before(:all) { deploy_process(filepath, "custom_name") }
With Process Instance
The with_process_instance
(previously with_workflow_instance
) method is used to create an instance for the specified process
and then yields a block that can interact with the instance.
This method ensures that an active instance is cancelled at the end of the block.
For testing BPMN files it is expected that most of the test definition will be wrapped in a call to this method.
with_process_instance("file_basename") do
...
end
Processing Jobs
A single job can be processed for a process by calling activate_job
(previously process_job
).
activate_job
is called with a job type:
activate_job("my_job")
The call to activate_job
returns a ActivatedJob
object that provides a fluent interface to chain
additional expectations and responses onto the job.
Expect Input
To check the input variables that are passed to the job add .expect_input
:
activate_job("my_job").
expect_input(user_id: 123)
Expect input uses RSpec expectations so it supports other RSpec helpers. For example, to perform a partial match on the input:
activate_job("my_job").
expect_input(hash_including("user_id" => 123))
Note: that when using methods like hash_including
string keys must be used to match the parsed JSON
coming from Zeebe.
Expect Headers
Similar to expect_input
, expectations can be set on headers for the job using .expect_headers
:
activate_job("my_job").
expect_headers(content_type: "CREATE")
# Combined with expect_input
activate_job("my_job").
expect_input(user_id: 123).
expect_headers(content_type: "CREATE")
Complete Job
Jobs can be completed by calling and_complete
(also aliased as complete
). Variables can optionally be returned with the
completed job.
# Completing a job can be changed with expectations
activate_job("my_job").
expect_input(user_id: 123).
and_complete
# Jobs can be completed with data that is merged with variables in the process
activate_job("my_job").
and_complete(status: "ACTIVATED")
Fail Job
Jobs can be failed by calling and_fail
(also aliased as fail
). An optional message can be specified when failing a job.
# Failing a job can be chanined with expectations
activate_job("my_job").
expect_headers(id_type: "user").
and_fail
# Jobs can be failed with a message
activate_job("my_job").
and_fail("something didn't go right")
By default retries are set to zero when a job is failed but the remaining retries can optionally be specified:
job = activate_job("my_job")
job.fail(retries: 1)
Update Retries
The retries for a job can also be modified using the update_retries
method:
job = activate_job("my_job")
job.update_retries(3)
Throw Error
The and_throw_error
(also aliased as throw_error
) method can be used to throw an error for a job. The error code is required and an
optional message may be specified:
activate_job("my_job").
expect_input(foo: "bar").
and_throw_error("NOT_FOUND")
# with message
activate_job("my_job").
expect_input(foo: "bar").
and_throw_error("NOT_FOUND", "couldn't find a bar")
Activating Multiple Jobs
Multiple jobs can be activated using the activate_jobs
method.
activate_jobs("my_job")
The call to activate_jobs
returns an Enumerator that returns ActivatedJob
instance.
The maximum number of jobs to return can be specified:
jobs = activate_jobs("my_job", max_jobs: 2).to_a
Process Complete
The process_complete!
(previously workflow_complete!
) method can be used to assert that the current process is complete at the end of a
test. This is implemented by cancelling the process and checking for an error that it is already
complete.
with_process_instance("file_basename") do
...
process_complete!
end
Publish Message
The publish_message
method is used to send a message to Zeebe.
The message name and correlation key are required:
publish_message("message_name", correlation_key: expected_value)
Variables can also be sent with a message:
publish_message("message_name", correlation_key: expected_value,
variables: { foo: "bar" })
The time-to-live (in milliseconds) cna also be specified for a message. It defaults to 5000 milliseconds if unspecified.
publish_message("message_name", correlation_key: expected_value, ttl_ms: 1000)
Set Variables
The set_variables
method can be used to set variables for a specified
scope in Zeebe:
# process_instance_key is a method that returns the key for the current process instance
set_variables(process_instance_key, { foo: "bar" })
An activated job can be used to determine the key for the task that it is associated with:
job = job_with_type("my_type")
set_variables(job.task_key, { foo: "baz"})
Variables default to being local to the scope on which they are set. This
can be overridden by specifying the :local
option:
set_variables(job.task_key, { foo: "baz"}, local: false)
Custom Matchers
In addition to the helpers documented above, this gem defines custom RSpec matchers to provide a more typical experience of expectations and matchers.
expect_job_of_type
The expect_job_of_type
helper is a convenient wrapper to activate a job and set an expectation target.
expect_job_of_type("my_type")
Similar to the activate_job
helper, it activates a job and wraps the result in an ActivatedJob
object.
That object is then passed to expect()
. Unlike activate_job
, this helper does not raise if there is no job activated.
This is equivalent to expect(job_with_type("my_type")
or expect(activate_job("my_type", validate: false))
.
expect_job_of_type
is expected to be used with the matchers below.
have_activated
The have_activated
matcher checks that the target represents an activated job. It will raise an error if no job
was activated.
expect_job_of_type("my_type").to have_activated
Various additional methods can be chained on the have_activated
matcher.
The with_variables
method can be used to check the input variables that the job was activated with:
expect_job_of_type("my_type").to have_activated.with_variables(user_id: 123)
The with_headers
method can be used to check the headers that the job was activated with:
expect_job_of_type("my_type").to have_activated.with_headers(id_type: "user")
The with_variables
and with_headers
methods can be chained on the same expectation:
expect_job_of_type("my_type").to have_activated.
with_variables(user_id: 123).
with_headers(id_type: "user")
The matcher also supports methods to complete, fail, or throw an error for a job:
# Complete
expect_job_of_type("my_type").to have_activated.and_complete
# Complete with new variables
expect_job_of_type("my_type").to have_activated.and_complete(result_code: 456)
# Fail (sets retries to 0 by default)
expect_job_of_type("my_type").to have_activated.and_fail
# Fail and specify retries
expect_job_of_type("my_type").to have_activated.and_fail(retries: 1)
# Fail with an error message
expect_job_of_type("my_type").to have_activated.and_fail("boom!")
# Fail with an error message and specify retries
expect_job_of_type("my_type").to have_activated.and_fail("boom!", retries: 2)
# Throw an error (error code is required)
expect_job_of_type("my_type").to have_activated.and_throw_error("MY_ERROR")
# Throw an error with an error message
expect_job_of_type("my_type").to have_activated.and_throw_error("MY_ERROR", "went horribly wrong")
Only one of and_complete
, and_fail
, or and_throw_error
can be specified for a single expectation.
have_variables and have_headers
In addition to the with_variables
and with_headers
methods that can be chained onto the have_activated
matcher, there are matchers that can be used directly to set expectations on the variables or
headers for an ActivatedJob
.
job = activate_job("my_type")
expect(job).to have_variables(user: 123)
expect(job).to have_headers(id_type: "user")
Tips & Tricks
Enumerator for Multiple Jobs
When activating multiple jobs, call to_a
on the result of activate_jobs
to get
an array of activated jobs objects.
Timer Duration
Specify timer durations using a variable so that tests can easily set the variable to specify a short duration.
Limitations
The current gem and approach have some limitations:
- You can interact with only one process at a time.
Development
This repo contains a docker-compose file that starts Zeebe and can be used for local development. Docker and Docker Compose most be installed as prerequisites.
Run the following to start a bash session. Gems will automatically be bundled and the environment will have access to a running Zeebe broker:
docker-compose run --rm console bash
To run specs using docker-compose run the following command:
docker-compose run --rm console rspec
Install Locally
To install this gem onto your local machine, run bundle exec rake install
.
Create a Release
To release a new version, update the version number in version.rb
, and then
run bundle exec rake release
, which will create a git tag for the version,
push git commits and tags, and push the .gem
file to
rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/ezcater/zeebe_bpmn_rspec.
License
The gem is available as open source under the terms of the MIT License.