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

  • 784