Testing Event-driven system
What is hard in testing?
Side Effects
- Preparing DB requirements
- Mocking HTTP calls
- Managing Redis
- Async jobs
- More...
"The Rails Way" controller
class UserApi::V2::RecipientsController < UserApi::V2::UserApiBaseController
def create
recipient = UserApi::V2::Recipient.build_tree created_by: current_contact, params: recipient_params
if recipient.save_tree
render json: UserApi::V2::RecipientSerializer.serialize(recipient), status: 200
else
render json: { message: recipient.errors_for_tree.details }, status: 422
end
end
end
class UserApi::V2::Recipient < ::Recipient
def self.build_tree(created_by:, params:)
information = RecipientOrganizationInformation.find_or_initialize_by(
name: params[:name],
legal_form: params[:legal_form]
)
information.tax_id = params[:tax_id]
information.build_organization unless information.organization
new created_by: created_by, information: information
end
def save_tree
transaction do
information.organization.save! unless information.organization.persisted?
information.save! unless information.persisted?
save!
end
rescue StandardError
false
end
end
What you need to know before testing?
- Who am I (as a contract)?
- Does recipient organization information already exist in DB (optionally create it before test)
- Does recipient organization already exist in DB (optionally create it before test)
- Are there any side-effects of creating one of above? If so, stub them
- Are there any validations for creating Recipient? If so, hardcode them in test or stub them
- Are there any side-effects of creating Recipient? If so, stub them
- Now handle happy path plus all failures in test case
"The Event Way" controller
class V3::RecipientsController < V3::BaseController
def create
recipient_id = SecureRandom.uuid
Fulfillment::Commands::CreateRecipient.call(params.merge id: recipient_id,
created_by: current_user.id
organization_id: current_user.organization_id)
render json: { recipient_id: recipient_id }, status: 200
end
end
class Fulfillment::Commands::CreateRecipient < ::DomainCommand
validations do
required(:id).filled(:str?)
required(:created_by).filled(:str?)
required(:organization_id).filled(:str?)
required(:name).filled(:str?)
# [...]
end
def call(params)
Fulfillment::Events::RecipientCreated.call(params)
end
end
class V3::BaseController < ApplicationController
rescue_from Domain::ParamsError, with: :bad_params
private
def bad_params(e)
render json: { errors: e.message }, status: 422
end
end
- Does user exists?
- NO: authentication guarantees it, so any value is correct
- Does organization exists?
- NO: authentication guarantees it, so any value is correct
- Are there any validations?
- YES: but they are explicit and stateless, so can be easily tested
- Are there any side-effects?
- YES: sending stateless message to Kafka, also simple to test
What you need to know before testing?
- Stub side-effects (Kafka)
- Provide input (API params), check output (Kafka message)
- Done
- Authentication errors are handled by IAM
- Params errors are handled by ApplicationController
- Verification and state transition will happen in Event handlers
Testing strategy:
- What are the inputs?
- Kafka, test it according to observed Event spec
- What are the dependencies?
- User - it was already authenticated, so you can trust it
- Organization - as above
- Current state of DB - there's no avoiding it sometimes
- HTTP - use VCR
- What are the side-effects?
- Kafka, easy to stub
- HTTP - also use VCR
Event Handler testing strategy:
Law of Demeter
- Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
- Why should Fulfillment know about internals of Payments?
- Each unit should only talk to its friends; don't talk to strangers.
- Use Commands and Events to communicate across domains
- Only talk to your immediate friends.
Testing Event-driven system
By Bernard Potocki
Testing Event-driven system
- 900