Testing Standards for Meridian Services

VerifiedSafe

Defines testing standards including avoiding time.Sleep, using testcontainers, and table-driven tests with rationale.

Sby Skills Guide Bot
TestingIntermediate
7/23/2026
Claude CodeCursorCopilotCodex
#go#testcontainers#flaky-tests#table-driven-tests#integration-testing

Recommended for

Automated summary

Defines testing standards for Meridian Go services: replacing time.Sleep with the await package, PostgreSQL integration tests via testcontainers, and documented table-driven tests.

Strengths

  • Swaps time.Sleep for async assertions with configurable timeout and poll interval, removing a common source of flaky tests
  • Enforces per-container isolation through testdb/testhelpers with a mandatory defer Cleanup
  • Requires a rationale field on each test case, documenting intent and easing review
  • Calls out unhappy-path coverage (defensive testing, ADR-0008) alongside happy paths

Limitations

  • Tightly coupled to the Meridian ecosystem (shared/platform/await, testdb, ADR-0008) and not portable as-is
  • Assumes Go and testify; no guidance for other languages or test frameworks
  • The await package is described without source or details on its polling behavior on failure
When to use it

Use it when writing or fixing Go tests in a Meridian service, especially for flakiness, async tests, or database integration tests.

When not to use it

Avoid it on non-Meridian or non-Go projects, since the referenced packages and conventions do not exist there.

Security analysis

Safe
Quality score87/100

The skill contains only static testing guidance and code examples. It declares no tools and does not instruct any destructive, exfiltrating, or obfuscated actions. The content is purely informational for developers.

No concerns found

How this analysis is made

Examples

Fix a flaky async test
My TestOrderCompletion in the payments service uses time.Sleep(2 * time.Second) and fails intermittently in CI. Rewrite it using the Meridian await package with a proper timeout.
Add a PostgreSQL integration test
Write a repository integration test for the ledger service that spins up an isolated Postgres container with testhelpers.SetupTestContainer and verifies Create and FindByID.
Convert to table-driven with rationale
Convert TestDeposit into a table-driven test following Meridian standards, including a rationale field and both happy-path and unhappy-path cases.

name: skill-testing-standards description: Testing standards and best practices for Meridian services triggers:

  • Writing tests
  • Test flakiness or failures
  • Using time.Sleep in tests
  • Async or polling tests
  • Integration tests
  • Database tests
  • Test coverage

instructions: | Use await package instead of time.Sleep. Use testcontainers for database tests. Apply defensive testing (ADR-0008): test happy paths AND unhappy paths. Table-driven tests with rationale field. Use testify assert/require.

Testing Standards

This document defines testing standards for Meridian services.

Core Principles

  1. No time.Sleep - Use the await package for async assertions
  2. Defensive testing - Test happy paths AND unhappy paths (see ADR-0008)
  3. Table-driven tests - Use Go table-driven patterns with rationale
  4. Isolation - Tests must not depend on each other
  5. Fast feedback - Unit tests should complete in milliseconds

Async Testing: Use await Not time.Sleep

NEVER use time.Sleep in tests. It creates flaky tests - too short causes failures, too long wastes CI time.

Use shared/platform/await instead. It polls until conditions are met or timeout.

import "github.com/meridianhub/meridian/shared/platform/await"

// BAD - arbitrary sleep, flaky and slow
time.Sleep(2 * time.Second)
assert.Equal(t, "COMPLETED", order.Status)

// GOOD - polls until condition met or timeout
err := await.Until(func() bool {
    return order.Status == "COMPLETED"
})
require.NoError(t, err)

// With custom timeout and poll interval
err := await.New().
    AtMost(5 * time.Second).
    PollInterval(50 * time.Millisecond).
    Until(func() bool {
        return repo.FindByID(ctx, id) != nil
    })

// Wait for an operation to succeed (no error)
err := await.UntilNoError(func() error {
    return client.HealthCheck()
})

// With context cancellation
err := await.New().
    WithContext(ctx).
    AtMost(10 * time.Second).
    Until(condition)

Defaults: 10s timeout, 100ms poll interval.

For advanced matchers and more expressive assertions, consider gomega.Eventually().

Database Integration Tests: Use Testcontainers

Use shared/platform/testdb/ for PostgreSQL integration tests. Each test gets an isolated container.

import "github.com/meridianhub/meridian/internal/<service>/repository/testhelpers"

func TestRepository(t *testing.T) {
    tc := testhelpers.SetupTestContainer(t)
    defer tc.Cleanup(t)  // CRITICAL - always defer cleanup

    // Use tc.Repo for repository operations
    err := tc.Repo.Create(ctx, entity)
    require.NoError(t, err)

    // Or use tc.Pool for direct SQL
    var count int
    err = tc.Pool.QueryRow(ctx, "SELECT COUNT(*) FROM table").Scan(&count)
    require.NoError(t, err)
}

See Testcontainers Usage Guide for detailed patterns.

Table-Driven Tests with Rationale

Every test case must include a rationale field explaining WHY it matters:

func TestDeposit(t *testing.T) {
    tests := []struct {
        name      string
        amount    int64
        wantErr   bool
        rationale string  // WHY this test case matters
    }{
        // Happy path
        {
            name:      "valid deposit",
            amount:    100,
            wantErr:   false,
            rationale: "Standard valid deposit should succeed",
        },

        // Edge cases
        {
            name:      "minimum valid amount",
            amount:    1,
            wantErr:   false,
            rationale: "Even 1 cent is a valid deposit",
        },

        // Unhappy paths
        {
            name:      "zero amount rejected",
            amount:    0,
            wantErr:   true,
            rationale: "Zero deposits are meaningless operations",
        },
        {
            name:      "negative amount rejected",
            amount:    -100,
            wantErr:   true,
            rationale: "Negative deposits don't make sense (use withdraw)",
        },

        // Defensive: values that shouldn't happen but might
        {
            name:      "overflow detection",
            amount:    math.MaxInt64,
            wantErr:   true,
            rationale: "Must detect arithmetic overflow to prevent corruption",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := account.Deposit(tt.amount)
            if tt.wantErr {
                assert.Error(t, err, tt.rationale)
            } else {
                assert.NoError(t, err, tt.rationale)
            }
        })
    }
}

Defensive Testing Categories

Apply these categories to all tests (see ADR-0008):

For Numeric Inputs

  • Valid values (happy path)
  • Zero
  • Negative values
  • Maximum/minimum boundaries
  • Overflow conditions

For String Inputs

  • Valid formatted strings
  • Empty strings
  • Whitespace-only strings
  • Invalid formats
  • Very long strings

For State Transitions

  • Valid transitions
  • Invalid transitions (must be rejected)
  • Idempotent operations
  • Concurrent state changes

For Error Handling

  • Every error path has a test
  • State is not modified on error
  • Resources are cleaned up on error
  • Errors wrap correctly (errors.Is/errors.As work)

Test Assertions: Use testify

Use github.com/stretchr/testify for assertions:

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

// Use require for setup - fails fast
tc := testhelpers.SetupTestContainer(t)
require.NotNil(t, tc.Pool, "database pool required")

// Use assert for verifications - continues on failure
assert.Equal(t, expected, actual)
assert.NoError(t, err)
assert.ErrorIs(t, err, ErrNotFound)
assert.Contains(t, logs, "expected message")

require vs assert:

  • require - Stops test immediately on failure (use for setup/preconditions)
  • assert - Records failure but continues (use for verifications)

Test Naming

Use descriptive names that explain the scenario:

// GOOD - describes scenario and expected outcome
func TestDeposit_ZeroAmount_ReturnsError(t *testing.T)
func TestFindByID_NonExistent_ReturnsNotFoundError(t *testing.T)
func TestTransfer_InsufficientFunds_RollsBackTransaction(t *testing.T)

// BAD - vague names
func TestDeposit1(t *testing.T)
func TestError(t *testing.T)
func TestIt(t *testing.T)

Parallel Tests

Run independent tests in parallel for faster feedback:

func TestConcurrentOperations(t *testing.T) {
    t.Parallel()  // Mark test as safe for parallel execution

    tc := testhelpers.SetupTestContainer(t)
    defer tc.Cleanup(t)

    // Each parallel test gets its own container
}

When NOT to parallelize:

  • Tests sharing global state
  • Tests with fixed port bindings
  • Tests modifying environment variables

Mocking Guidelines

Minimize mocking. Prefer real implementations where practical:

  1. Use testcontainers for database tests (real PostgreSQL)
  2. Use in-memory implementations for simple interfaces
  3. Mock only external services that can't be containerized
  4. Never mock the thing you're testing

When mocking is necessary, use github.com/stretchr/testify/mock:

type MockClient struct {
    mock.Mock
}

func (m *MockClient) Send(ctx context.Context, msg Message) error {
    args := m.Called(ctx, msg)
    return args.Error(0)
}

// In test
client := new(MockClient)
client.On("Send", mock.Anything, expectedMsg).Return(nil)

Test Organization

service/
├── domain/
│   ├── account.go
│   └── account_test.go          # Unit tests (same package)
├── repository/
│   ├── postgres_repository.go
│   ├── postgres_repository_test.go      # Integration tests
│   └── testhelpers/
│       └── container.go         # Shared test setup
└── handler/
    ├── grpc_handler.go
    └── grpc_handler_test.go     # Handler tests

CI Considerations

  • Unit tests run on every commit
  • Integration tests (testcontainers) run on PR
  • Tests must pass before merge
  • Flaky tests must be fixed immediately (not skipped)

Related Documentation

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/testing-standards-for-meridian-services-mrx3xztb/download' --output '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md.part' && mv '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md.part' '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.

Claude Code
test -f '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md' && test -s '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/skill-testing-standards' && cp -R '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb' '.claude/skills/skill-testing-standards'

Cursor
test -f '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md' && test -s '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/skill-testing-standards' && cp -R '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb' '.cursor/skills/skill-testing-standards'

Codex
test -f '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md' && test -s '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md' && mkdir -p '.agents/skills' && test ! -e '.agents/skills/skill-testing-standards' && cp -R '.skill-downloads/testing-standards-for-meridian-services-mrx3xztb' '.agents/skills/skill-testing-standards'

OpenSkills

OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.

Official OpenSkills installation
openskills install './.skill-downloads/testing-standards-for-meridian-services-mrx3xztb'

openskills read 'testing-standards-for-meridian-services-mrx3xztb'

Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.

For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.

openskills sync

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/testing-standards-for-meridian-services-mrx3xztb/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills