Skip to main content
This guide covers testing practices for Memos, including unit tests, integration tests, and end-to-end testing strategies.

Backend Testing

Memos backend uses Go’s built-in testing framework with additional libraries for assertions and test containers.

Running Tests

go test ./...

Test Structure

Memos uses the standard Go test pattern:
package test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/require"
    "github.com/usememos/memos/store"
)

func TestMemoCreation(t *testing.T) {
    t.Parallel() // Run tests in parallel
    ctx := context.Background()
    ts := NewTestingStore(ctx, t)
    defer ts.Close()

    // Create test user
    user, err := createTestingHostUser(ctx, ts)
    require.NoError(t, err)

    // Execute test
    memo, err := ts.CreateMemo(ctx, &store.Memo{
        CreatorID:  user.ID,
        Content:    "test content",
        Visibility: store.Public,
    })
    require.NoError(t, err)
    require.NotNil(t, memo)
    require.Equal(t, "test content", memo.Content)
}

Testing Utilities

Memos provides test utilities in store/test/:
Creates an isolated test database:
func TestExample(t *testing.T) {
    ctx := context.Background()
    ts := NewTestingStore(ctx, t)
    defer ts.Close()

    // Each test gets a fresh database
    // SQLite: temporary file
    // MySQL/Postgres: new database in shared container
}
Creates a test admin user:
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)

// Use user.ID for creating memos, etc.
Creates a store connected to a specific database:
ts := NewTestingStoreWithDSN(ctx, t, "mysql", dsn)
// Useful for migration testing

Writing Store Tests

Test pattern for store operations:
1

Set up test database

func TestMemoUpdate(t *testing.T) {
    t.Parallel()
    ctx := context.Background()
    ts := NewTestingStore(ctx, t)
    defer ts.Close()
2

Create test fixtures

user, _ := createTestingHostUser(ctx, ts)
memo, _ := ts.CreateMemo(ctx, &store.Memo{
    CreatorID:  user.ID,
    Content:    "original",
    Visibility: store.Public,
})
3

Execute operation

newContent := "updated"
err := ts.UpdateMemo(ctx, &store.UpdateMemo{
    ID:      memo.ID,
    Content: &newContent,
})
require.NoError(t, err)
4

Verify results

updated, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
require.NoError(t, err)
require.Equal(t, "updated", updated.Content)

Testing with Multiple Databases

Run tests against SQLite, MySQL, and PostgreSQL:
go test ./store/...
MySQL and PostgreSQL tests use testcontainers-go to spin up database containers automatically.

Testing API Endpoints

Test gRPC/Connect RPC services:
package v1_test

import (
    "context"
    "testing"

    "connectrpc.com/connect"
    "github.com/stretchr/testify/require"

    apiv1 "github.com/usememos/memos/proto/gen/api/v1"
)

func TestMemoService_CreateMemo(t *testing.T) {
    ctx := context.Background()
    
    // Setup test server
    server := newTestServer(t)
    defer server.Close()

    // Create memo via API
    req := &apiv1.CreateMemoRequest{
        Content:    "test memo",
        Visibility: "PUBLIC",
    }
    
    resp, err := server.MemoService.CreateMemo(ctx, connect.NewRequest(req))
    require.NoError(t, err)
    require.NotNil(t, resp.Msg.Memo)
    require.Equal(t, "test memo", resp.Msg.Memo.Content)
}

Frontend Testing

Memos frontend uses TypeScript type checking and linting instead of traditional unit tests.

Running Frontend Checks

cd web
pnpm lint

TypeScript Configuration

Strict type checking ensures code quality:
tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Biome Linting

Biome (ESLint replacement) catches common errors:
# Check for issues
pnpm lint

# Auto-fix issues
pnpm lint:fix
Configuration in web/biome.json:
{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noExplicitAny": "error"
      }
    }
  }
}

Manual Testing

Use React Query DevTools for state inspection:
1

Start dev server

cd web
pnpm dev
2

Open DevTools

Click the React Query icon in the bottom-left corner of the browser.
3

Inspect queries

  • View active queries and their state
  • Check cache entries and expiration
  • Manually refetch or invalidate queries
  • Inspect mutation state

Protocol Buffer Testing

Validate .proto files:
cd proto
buf lint

CI/CD Testing

GitHub Actions runs automated tests on every push and pull request.

Backend Test Workflow

.github/workflows/backend-tests.yml
name: Backend Tests

on:
  push:
    branches: [main]
  pull_request:
    paths:
      - "go.mod"
      - "go.sum"
      - "**.go"

jobs:
  static-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-go@v6
        with:
          go-version: '1.25.7'
      - name: Verify go.mod is tidy
        run: |
          go mod tidy
          git diff --exit-code
      - name: Run golangci-lint
        run: golangci-lint run --timeout=3m

  tests:
    strategy:
      matrix:
        test-group: [store, server, plugin]
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-go@v6
      - name: Run tests
        run: go test -v -race -coverprofile=coverage.out ./store/...

Frontend Test Workflow

.github/workflows/frontend-tests.yml
name: Frontend Tests

on:
  push:
    branches: [main]
  pull_request:
    paths:
      - "web/**"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
        working-directory: web
      - run: pnpm lint
        working-directory: web

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v6
      - run: pnpm install --frozen-lockfile
        working-directory: web
      - run: pnpm build
        working-directory: web

Test Coverage

Viewing Coverage Reports

1

Generate coverage

go test -coverprofile=coverage.out ./...
2

View in terminal

go tool cover -func=coverage.out
3

View in browser

go tool cover -html=coverage.out

Coverage Targets

Aim for these coverage levels:
  • Store layer: >80%
  • API services: >70%
  • Utilities: >60%
Coverage is a metric, not a goal. Focus on testing critical paths and edge cases rather than chasing 100% coverage.

Integration Testing

Test full workflows across multiple components:
func TestMemoWorkflow(t *testing.T) {
    ctx := context.Background()
    ts := NewTestingStore(ctx, t)
    defer ts.Close()

    // 1. Create user
    user, err := createTestingHostUser(ctx, ts)
    require.NoError(t, err)

    // 2. Create memo
    memo, err := ts.CreateMemo(ctx, &store.Memo{
        CreatorID:  user.ID,
        Content:    "#tag test memo",
        Visibility: store.Public,
    })
    require.NoError(t, err)

    // 3. Verify memo payload was processed (tags extracted)
    require.NotNil(t, memo.Payload)
    require.Contains(t, memo.Payload.Tags, "tag")

    // 4. Filter by tag
    memos, err := ts.ListMemos(ctx, &store.FindMemo{
        Filters: []string{`tag in ["tag"]`},
    })
    require.NoError(t, err)
    require.Len(t, memos, 1)

    // 5. Delete memo
    err = ts.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID})
    require.NoError(t, err)

    // 6. Verify deletion
    found, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})
    require.NoError(t, err)
    require.Nil(t, found)
}

Performance Testing

Benchmark Tests

Go supports benchmarking out of the box:
func BenchmarkMemoCreation(b *testing.B) {
    ctx := context.Background()
    ts := NewTestingStore(ctx, &testing.T{})
    defer ts.Close()

    user, _ := createTestingHostUser(ctx, ts)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = ts.CreateMemo(ctx, &store.Memo{
            CreatorID:  user.ID,
            Content:    "benchmark memo",
            Visibility: store.Public,
        })
    }
}
Run benchmarks:
go test -bench=. -benchmem ./store/...

Load Testing

Use tools like k6 or hey for HTTP load testing:
# Install hey
go install github.com/rakyll/hey@latest

# Load test memo creation endpoint
hey -n 1000 -c 10 -m POST \
  -H "Authorization: Bearer <token>" \
  -d '{"content":"test"}' \
  http://localhost:8081/api/v1/memos

Test Best Practices

Parallel Tests

Use t.Parallel() for independent tests:
func TestExample(t *testing.T) {
    t.Parallel()
    // ...
}

Cleanup

Always clean up resources:
defer ts.Close()
defer server.Stop()

Clear Assertions

Use descriptive error messages:
require.Equal(t, expected, actual,
  "memo content should match")

Isolated Tests

Each test should be independent:
// Good: fresh database per test
ts := NewTestingStore(ctx, t)

// Bad: shared state
var globalDB *sql.DB

Debugging Tests

Verbose Output

go test -v ./store/...

Run Specific Test

go test -v -run TestMemoCreation ./store/test/

Debug with Delve

# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest

# Debug test
dlv test ./store/test -- -test.run TestMemoCreation
func TestExample(t *testing.T) {
    memo, _ := ts.CreateMemo(ctx, &store.Memo{...})
    
    // Use t.Log for test output
    t.Logf("Created memo: %+v", memo)
    
    // Or spew for detailed output
    spew.Dump(memo)
}

Troubleshooting

Ensure Docker is running for testcontainers:
docker ps
If containers fail to start, check Docker Desktop is running.
Run with race detection to find data races:
go test -race ./...
Fix by using proper synchronization (mutexes, channels).
Common causes:
  • Timing dependencies (use time.Sleep sparingly)
  • Shared state between tests (use t.Parallel() and isolation)
  • External dependencies (mock when possible)
Fix by ensuring deterministic test execution.
Increase timeout:
go test -timeout 10m ./...
Or optimize slow tests by reducing test data size.

Next Steps

Contributing

Learn how to submit your changes

Building

Build production binaries

Build docs developers (and LLMs) love