Skip to content

Latest commit

 

History

History
229 lines (178 loc) · 6.69 KB

File metadata and controls

229 lines (178 loc) · 6.69 KB

Testing

Shigoto provides helpers for testing workers and job workflows without running background queue processes. The perform_job/2,3 runner and the inline/manual testing modes need no database at all; drain_queue/1 runs against a real pool for integration tests.

perform_job — run a worker with no database

shigoto:perform_job/2,3 runs a worker through the same perform path production uses (the middleware chain and deps_results injection) and returns the raw worker result. No database, no queue, no retry, and no resilience gating (rate limits, circuit breakers, concurrency caps are skipped). It is a pure in-process unit-test runner.

ok = shigoto:perform_job(email_worker, #{~"to" => ~"user@test.com"}),

{ok, #{~"produced" := 42}} =
    shigoto:perform_job(sum_worker, #{~"action" => ~"produce", ~"value" => 42}),

{error, _} = shigoto:perform_job(flaky_worker, #{~"action" => ~"fail"}).

Pass predecessors' results (exactly as a real dependent would receive them) via deps_results in the third-argument options:

{ok, _} = shigoto:perform_job(
    rollup_worker, #{~"action" => ~"consume"},
    #{deps_results => #{41 => #{~"n" => 7}}}
).

Other options: attempt, max_attempts, queue, id, meta, tags, priority, timeout.

Testing modes and enqueue assertions

Arm a testing mode so insert/1,2 and insert_all/1,2 stop persisting. Because these modes change persistence, arming them requires two keys — a mode value that leaks into production without the confirmation key falls back to the real persisted path and logs an error, so it can never silently drop jobs:

application:set_env(shigoto, testing, manual),
application:set_env(shigoto, testing_confirm_no_persistence, true).
  • manual captures inserted jobs in a process-local buffer without running or persisting them.
  • inline runs each inserted job synchronously and keeps no row; a job that returns {error, _} raises {shigoto_inline_job_failed, Worker, Reason}.

Assert on what your code enqueued with m:shigoto_testing:

enqueue_signup_jobs(User),
shigoto_testing:assert_enqueued(#{worker => welcome_email_worker}),
shigoto_testing:assert_enqueued(#{args => #{~"user_id" => 5}}),
shigoto_testing:refute_enqueued(#{queue => ~"sms"}),
[_ | _] = shigoto_testing:all_enqueued().

Filters: worker, queue, args (containment), tags (containment), state. Call shigoto_testing:reset/0 in init_per_testcase to clear the buffer.

manual capture is per-process — it sees enqueues made by the calling process, not by other processes it spawns. For cross-process expectations, use a real test pool: outside manual mode the assertions query the pool via find_jobs, so the same assertions work against a live database.

Note: batch, depends_on, and unique are no-ops under inline/manual (shigoto logs a warning). Exercise those against a real pool with drain_queue/1.

drain_queue

The primary testing tool is shigoto:drain_queue/1, which synchronously claims and executes all available jobs in a queue:

-include_lib("stdlib/include/assert.hrl").

my_test(_Config) ->
    %% Insert a job
    shigoto:insert(#{
        worker => my_email_worker,
        args => #{<<"to">> => <<"test@example.com">>}
    }),

    %% Execute all jobs synchronously
    ok = shigoto:drain_queue(<<"default">>),

    %% Assert the side effect happened
    ?assert(email_was_sent(<<"test@example.com">>)).

With a timeout:

ok = shigoto:drain_queue(<<"emails">>, #{timeout => 10000}).

Test Setup

A typical CT suite for shigoto tests:

-module(my_jobs_SUITE).
-behaviour(ct_suite).
-include_lib("stdlib/include/assert.hrl").

-export([all/0, init_per_suite/1, end_per_suite/1,
         init_per_testcase/2, end_per_testcase/2]).
-export([test_email_job/1]).

-define(POOL, my_test_pool).

all() -> [test_email_job].

init_per_suite(Config) ->
    {ok, _} = application:ensure_all_started(pgo),
    {ok, _} = pgo:start_pool(?POOL, #{
        host => "localhost",
        port => 5432,
        database => "my_app_test",
        user => "postgres",
        password => "postgres",
        pool_size => 5
    }),
    application:set_env(shigoto, pool, ?POOL),
    ok = shigoto_migration:up(?POOL),
    Config.

end_per_suite(_Config) ->
    ok.

init_per_testcase(_TestCase, Config) ->
    Config.

end_per_testcase(_TestCase, _Config) ->
    pgo:query(<<"DELETE FROM shigoto_jobs">>, [], #{pool => ?POOL}),
    ok.

test_email_job(_Config) ->
    {ok, Job} = shigoto:insert(#{
        worker => my_email_worker,
        args => #{<<"to">> => <<"user@test.com">>}
    }),
    ?assertEqual(<<"available">>, maps:get(state, Job)),
    ok = shigoto:drain_queue(<<"default">>).

Testing Workers Directly

For unit testing worker logic without the job queue:

test_worker_logic(_Config) ->
    Args = #{<<"to">> => <<"test@example.com">>, <<"subject">> => <<"Test">>},
    ?assertEqual(ok, my_email_worker:perform(Args)).

Testing with Dependencies

test_pipeline(_Config) ->
    {ok, Step1} = shigoto:insert(#{
        worker => extract_worker, args => #{}
    }),
    {ok, _Step2} = shigoto:insert(#{
        worker => transform_worker, args => #{},
        depends_on => [maps:get(id, Step1)]
    }),
    %% First drain executes Step1
    ok = shigoto:drain_queue(<<"default">>),
    %% Second drain executes Step2 (deps resolved)
    ok = shigoto:drain_queue(<<"default">>).

Testing Batches

test_batch_completion(_Config) ->
    {ok, Batch} = shigoto:new_batch(#{
        callback_worker => my_callback,
        callback_args => #{<<"report">> => 1}
    }),
    BatchId = maps:get(id, Batch),
    shigoto:insert(#{worker => my_worker, args => #{}, batch => BatchId}),
    shigoto:insert(#{worker => my_worker, args => #{}, batch => BatchId}),

    %% Drain executes both jobs + the callback
    ok = shigoto:drain_queue(<<"default">>),

    {ok, Final} = shigoto:get_batch(BatchId),
    ?assertEqual(<<"finished">>, maps:get(state, Final)).

Testing Unique Jobs

test_unique_prevents_duplicate(_Config) ->
    Opts = #{unique => #{keys => [worker, args]}},
    {ok, Job1} = shigoto:insert(
        #{worker => my_worker, args => #{<<"x">> => 1}}, Opts
    ),
    {ok, {conflict, Job2}} = shigoto:insert(
        #{worker => my_worker, args => #{<<"x">> => 1}}, Opts
    ),
    ?assertEqual(maps:get(id, Job1), maps:get(id, Job2)).

Docker Compose for Tests

Use Docker Compose for PostgreSQL in CI and local testing:

# docker-compose.yml
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: root
      POSTGRES_DB: shigoto_test
    ports:
      - "5556:5432"