agentsclimarketplace

Unit testing

Skill MARUCIE/openclaw-foundry/web/public/packs/spellbook-test-engineer/skills/unit-testing

Use when writing unit tests for a function or module, mocking external dependencies, practising TDD on new business logic, or enforcing a coverage target across a codebase in Python, TypeScript, or Go.From its SKILL.md

Install
npx -y skills add MARUCIE/openclaw-foundry --skill unit-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

21.7 KB, ~5.5k tokens by cl100k_base, as published. Nobody here has run it

是什么

这是一份单元测试规范,覆盖 Arrange-Act-Assert(准备-执行-断言)测试结构、依赖打桩、参数化测试、TDD(测试驱动开发)流程、覆盖率策略、用例组织方式,让团队在 Python、TypeScript、Go 三种语言下都能写出快、稳、清晰的单测。

怎么用

  1. 写新函数或模块时按 AAA 三段式编排测试,让每个用例的输入、行为、断言一眼能看清。
  2. 涉及外部依赖(数据库、HTTP、文件系统)时按文档的打桩与 mock 模板隔离,让单测保持毫秒级。
  3. 同一逻辑多组输入用参数化测试代替复制粘贴,按规范的命名约定让失败时看得出是哪组输入挂了。
  4. 新业务实践 TDD 时按红绿重构三阶段循环走,先写失败测试再写实现,让设计跟着测试演进。
  5. 配覆盖率目标时按业务等级分档执行,关键模块卡 80% 以上行覆盖加分支覆盖双指标。

架构图

flowchart LR
    A[需求或缺陷] --> B[写失败测试]
    B --> C[最小实现]
    C --> D[测试通过]
    D --> E[重构优化]
    E --> F[覆盖率验证]

Unit Testing

Structured guidance for writing reliable, maintainable unit tests — covering test anatomy, mocking strategies, parameterized tests, TDD workflow, coverage strategy, and test organization across Python, TypeScript, and Go.

When to Activate

  • Writing tests for a function, class, or module
  • Setting up a test framework for a new project
  • Mocking or stubbing external dependencies in tests
  • Deciding what to unit test vs integration test
  • Practising TDD (test-driven development) on a new feature
  • Achieving or enforcing a coverage target
  • Debugging a flaky or unclear test failure

Test Anatomy

Arrange / Act / Assert (AAA)

Every test has three distinct phases:

  • Arrange — set up the inputs, dependencies, and state needed for the test
  • Act — call the unit under test (exactly once)
  • Assert — verify the outcome matches expectations

One assertion per logical concern means each test validates one observable behaviour. Multiple assert statements are fine as long as they all verify the same thing (e.g., both fields of a returned object).

Test Naming

LanguageConventionExample
Pythontest_<unit>_<scenario>_<expected_result>test_divide_by_zero_raises_value_error
TypeScriptdescribe('Unit') / it('should ...')it('should throw when dividing by zero')
GoTestXxx with t.Run("SubtestName", ...)TestDivide/ByZero

Well-Structured Test Examples

# Python — pytest
def test_calculate_total_with_discount_returns_reduced_price():
    # Arrange
    cart = Cart(items=[Item(price=100)])
    discount = PercentageDiscount(rate=0.10)

    # Act
    total = calculate_total(cart, discount)

    # Assert
    assert total == 90.0
// TypeScript — Jest / Vitest
describe('calculateTotal', () => {
  it('should return reduced price when discount is applied', () => {
    // Arrange
    const cart = { items: [{ price: 100 }] };
    const discount = { rate: 0.10 };

    // Act
    const total = calculateTotal(cart, discount);

    // Assert
    expect(total).toBe(90.0);
  });
});
// Go — testing package
func TestCalculateTotal_WithDiscount_ReturnsReducedPrice(t *testing.T) {
    // Arrange
    cart := Cart{Items: []Item{{Price: 100}}}
    discount := Discount{Rate: 0.10}

    // Act
    total := CalculateTotal(cart, discount)

    // Assert
    if total != 90.0 {
        t.Errorf("expected 90.0, got %f", total)
    }
}

What to Unit Test

Decision Table

Test ThisDon't Test This
Public functions and methodsPrivate implementation details
Business logic and domain rulesFramework internals (ORM, router)
Edge cases: nil/None/null, empty collectionsTrivial getters and setters
Boundary values (off-by-one, max/min)Auto-generated code (protobuf, ORM models)
Error paths and exception handlingConfiguration loading (use integration tests)
Conditional branchesmain() entrypoints
Data transformations and calculationsThird-party library behaviour

Principles

Test public interfaces. If you must reach into private methods to test behaviour, that is a signal the logic should be extracted into its own testable unit.

Test edge cases explicitly. The most common bugs live at the boundaries:

# BAD — only tests the happy path
def test_get_first_item():
    result = get_first([1, 2, 3])
    assert result == 1

# GOOD — also tests the edge case
def test_get_first_item_returns_none_for_empty_list():
    result = get_first([])
    assert result is None

Test error paths. If your function raises, throws, or returns an error value, test that explicitly:

// TypeScript
it('should throw InvalidInputError when name is empty', () => {
  expect(() => createUser({ name: '' })).toThrow(InvalidInputError);
});
// Go
func TestCreateUser_EmptyName_ReturnsError(t *testing.T) {
    _, err := CreateUser(UserInput{Name: ""})
    if err == nil {
        t.Fatal("expected error for empty name, got nil")
    }
}

Mocking, Stubbing, and Spying

Terminology

TermDefinitionUse When
StubReturns a fixed value; does not record callsReplacing a dependency that returns data
MockRecords calls; assertions on how it was usedVerifying a side-effect was triggered
SpyWraps real implementation; records callsChecking interactions without replacing logic
FakeLightweight working implementation (e.g., in-memory DB)Stateful dependencies like repositories

Python — unittest.mock and pytest-mock

from unittest.mock import MagicMock, patch
import pytest

# Stub with MagicMock
def test_send_welcome_email_calls_mailer():
    mailer = MagicMock()
    service = UserService(mailer=mailer)

    service.register(email="[email protected]")

    # Mock assertion — verify interaction
    mailer.send.assert_called_once_with(
        to="[email protected]",
        subject="Welcome"
    )

# Patch with pytest-mock
def test_get_current_time_uses_utcnow(mocker):
    mock_now = mocker.patch("mymodule.datetime")
    mock_now.utcnow.return_value = datetime(2024, 1, 1)

    result = get_current_time()

    assert result == datetime(2024, 1, 1)

TypeScript — Jest and Vitest

// jest.fn() — stub / mock
const mockSend = jest.fn().mockResolvedValue({ status: 200 });
const mailer = { send: mockSend };
const service = new UserService(mailer);

await service.register('[email protected]');

expect(mockSend).toHaveBeenCalledWith({
  to: '[email protected]',
  subject: 'Welcome',
});

// jest.spyOn() — spy on real method
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
triggerDeprecatedFeature();
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();

// Vitest equivalents (same API)
import { vi } from 'vitest';
const mockFn = vi.fn();
const spyFn = vi.spyOn(obj, 'method');

Go — Interface-Based Mocking

Go's idiomatic approach is to define an interface for the dependency and provide a test struct that implements it.

// Production interface
type Mailer interface {
    Send(to, subject string) error
}

// Test double — implement the interface manually
type MockMailer struct {
    CalledWith []struct{ To, Subject string }
}

func (m *MockMailer) Send(to, subject string) error {
    m.CalledWith = append(m.CalledWith, struct{ To, Subject string }{to, subject})
    return nil
}

// Test
func TestUserService_Register_SendsWelcomeEmail(t *testing.T) {
    mailer := &MockMailer{}
    svc := NewUserService(mailer)

    svc.Register("[email protected]")

    if len(mailer.CalledWith) != 1 {
        t.Fatalf("expected 1 call, got %d", len(mailer.CalledWith))
    }
    if mailer.CalledWith[0].To != "[email protected]" {
        t.Errorf("unexpected recipient: %s", mailer.CalledWith[0].To)
    }
}

When Mocking Is a Design Smell

If every test requires five mocks to run a single function, the function has too many dependencies. Excessive mocking signals tight coupling — consider refactoring toward pure functions, dependency injection, or smaller units.

# BAD — mocking the system under test
def test_process_order():
    order_service = MagicMock()          # mocking the thing being tested
    order_service.process.return_value = True
    assert order_service.process(order) is True  # tests nothing real

# GOOD — mock only the external boundary
def test_process_order_persists_to_repository():
    repo = MagicMock()
    service = OrderService(repo=repo)    # inject the dependency
    service.process(order)
    repo.save.assert_called_once_with(order)

Parameterized Tests

Use parameterized tests when the same logic needs to be verified against many input/output pairs. This avoids copy-pasting test bodies and makes the full test matrix visible at a glance.

Example: validate_email() across 5 cases

# Python — pytest.mark.parametrize with IDs
import pytest

@pytest.mark.parametrize("email,expected", [
    ("[email protected]",   True),
    ("[email protected]",   True),
    ("missing-at-sign",    False),
    ("@nodomain.com",      False),
    ("",                   False),
], ids=[
    "valid_standard",
    "valid_uppercase",
    "invalid_no_at",
    "invalid_no_local_part",
    "invalid_empty",
])
def test_validate_email(email, expected):
    assert validate_email(email) == expected
// TypeScript — Jest test.each / Vitest it.each
describe('validateEmail', () => {
  it.each([
    ['[email protected]',  true,  'valid standard'],
    ['[email protected]',  true,  'valid uppercase'],
    ['missing-at-sign',   false, 'invalid no at'],
    ['@nodomain.com',     false, 'invalid no local part'],
    ['',                  false, 'invalid empty'],
  ])('should return %s for "%s" (%s)', (email, expected) => {
    expect(validateEmail(email)).toBe(expected);
  });
});
// Go — table-driven tests with t.Run
func TestValidateEmail(t *testing.T) {
    cases := []struct {
        name     string
        email    string
        expected bool
    }{
        {"valid standard",        "[email protected]", true},
        {"valid uppercase",       "[email protected]", true},
        {"invalid no at",         "missing-at-sign",  false},
        {"invalid no local part", "@nodomain.com",    false},
        {"invalid empty",         "",                 false},
    }

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            result := ValidateEmail(tc.email)
            if result != tc.expected {
                t.Errorf("ValidateEmail(%q): got %v, want %v", tc.email, result, tc.expected)
            }
        })
    }
}

Test Doubles for External I/O

Fakes vs Mocks

ApproachBest ForExample
FakeStateful dependencies with real behaviourIn-memory repository
MockFire-and-forget calls where you verify interactionEmail sender, event bus
HTTP stubOutbound HTTP clientsresponses, msw, httptest

Use fakes for repositories and other stateful collaborators — they give you real insert/find behaviour without a live database. Use mocks for fire-and-forget side-effects (emails, queues) where the key assertion is that the call was made at all.

In-Memory Repository (Fake)

# Python — fake repository for stateful DB calls
class InMemoryUserRepository:
    def __init__(self):
        self._store = {}

    def save(self, user):
        self._store[user.id] = user

    def find_by_id(self, user_id):
        return self._store.get(user_id)
// TypeScript — fake repository
class InMemoryUserRepository implements UserRepository {
  private store = new Map<string, User>();

  async save(user: User): Promise<void> {
    this.store.set(user.id, user);
  }

  async findById(id: string): Promise<User | undefined> {
    return this.store.get(id);
  }
}
// Go — fake repository struct
type InMemoryUserRepo struct {
    store map[string]User
}

func NewInMemoryUserRepo() *InMemoryUserRepo {
    return &InMemoryUserRepo{store: make(map[string]User)}
}

func (r *InMemoryUserRepo) Save(u User) error {
    r.store[u.ID] = u
    return nil
}

func (r *InMemoryUserRepo) FindByID(id string) (User, bool) {
    u, ok := r.store[id]
    return u, ok
}

Mocking HTTP Clients

# Python — responses library
import responses as rsps
import requests

@rsps.activate
def test_fetch_user_returns_parsed_data():
    rsps.add(rsps.GET, "https://api.example.com/users/1",
             json={"id": 1, "name": "Alice"}, status=200)

    user = fetch_user(1)

    assert user.name == "Alice"
// TypeScript — msw (Mock Service Worker)
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(
  http.get('https://api.example.com/users/:id', () =>
    HttpResponse.json({ id: 1, name: 'Alice' })
  )
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

it('should return parsed user data', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});
// Go — httptest.NewServer
func TestFetchUser_ReturnsUser(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintln(w, `{"id":1,"name":"Alice"}`)
    }))
    defer server.Close()

    client := NewAPIClient(server.URL)
    user, err := client.FetchUser(1)

    if err != nil {
        t.Fatal(err)
    }
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}

TDD Workflow

Red → Green → Refactor

  • Red — write a failing test that specifies the behaviour you want before writing any implementation
  • Green — write the minimum code required to make the test pass, even if the code is ugly
  • Refactor — clean up duplication and improve design with the safety net of a passing test

When TDD Pays Off

SituationTDD Value
New feature with clear acceptance criteriaHigh
Complex business logic (pricing, validation, state machines)High
Regression prevention on a known bugHigh
Exploratory / spike codeLow — write tests after the design emerges
UI layout and visual componentsLow — prefer visual regression tools
Third-party integration spikeLow — integration tests are more appropriate

Outside-In TDD

Start from the outer boundary (e.g., an HTTP handler or a use case) and write a failing acceptance test. Mock all inner collaborators. Then drive out each collaborator with its own unit tests, working inward until all mocks are replaced by real implementations.


Coverage Strategy

What Each Metric Measures

MetricMeasuresLimitation
Line coverageWhether each line was executedA line can be hit without testing all outcomes
Branch coverageWhether each if/else path was takenMisses logic errors in the execution path
Mutation testingWhether tests detect small code changesSlow; use on critical modules only

Realistic Targets

ContextLine CoverageBranch Coverage
Greenfield project80%+70%+
Existing codebaseRaise incrementally by 5% per sprintFocus on new code
Critical path (payments, auth, core domain)95%+90%+
Generated or migration codeExcludeExclude

What to Exclude from Coverage

  • Database migration files
  • Auto-generated code (protobuf, ORM schema)
  • main() entrypoints and CLI bootstrap
  • Configuration loaders and environment readers
  • Third-party adapter shims

Tools

# Python — pytest-cov
pytest --cov=src --cov-report=term-missing --cov-branch
// TypeScript — Jest
// jest.config.ts
export default {
  collectCoverage: true,
  coverageThreshold: { global: { lines: 80, branches: 70 } },
  coveragePathIgnorePatterns: ['/generated/', '/migrations/'],
};

// Vitest — vitest.config.ts
export default defineConfig({
  test: {
    coverage: { provider: 'v8', thresholds: { lines: 80, branches: 70 } },
  },
});
# Go
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out

Coverage is a floor, not a goal. 100% line coverage achieved by testing only the happy path with no meaningful assertions is worse than 70% coverage with rigorous, well-structured tests.


Test Organization

Co-location vs Separate Directory

LanguageConventionNotes
Pythontests/ at project rootMirrors src/ structure; conftest.py for shared fixtures
TypeScript*.test.ts or *.spec.ts co-located, or __tests__/ folderCo-location is most common in React/Node projects
Go_test.go suffix, same directory as sourceSame package = white-box; package foo_test suffix = black-box

Python: conftest.py and Fixtures

# tests/conftest.py — shared fixtures available to all tests
import pytest
from myapp.db import Database

@pytest.fixture
def db():
    database = Database(url="sqlite:///:memory:")
    database.create_tables()
    yield database
    database.drop_tables()

@pytest.fixture
def user_repo(db):
    return UserRepository(db)

Go: White-Box vs Black-Box Tests

// white_box_test.go — same package, can access unexported identifiers
package cart

func TestInternalDiscount(t *testing.T) {
    d := applyInternalDiscount(100, 0.10) // unexported function
    if d != 90.0 {
        t.Errorf("expected 90.0, got %f", d)
    }
}

// black_box_test.go — _test package suffix, only public API visible
package cart_test

func TestCart_AddItem(t *testing.T) {
    c := cart.New()
    c.Add(cart.Item{Price: 50})
    if c.Total() != 50 {
        t.Errorf("expected total 50, got %f", c.Total())
    }
}

Test Isolation

# BAD — shared mutable state between tests
_global_cache = {}

def test_a():
    _global_cache["key"] = "value"

def test_b():
    assert _global_cache.get("key") is None  # fails if test_a ran first

# GOOD — each test owns its state
def test_b():
    cache = {}
    result = process_with_cache(cache, "key")
    assert result is not None

Clean up after each test. Use setUp/tearDown, pytest fixtures with yield, Jest's beforeEach/afterEach, or Go's t.Cleanup() to reset shared resources.

Parallel Test Execution

ToolParallel FlagNotes
pytestpytest-xdist: pytest -n autoTests must be stateless; use tmp_path fixture for filesystem
Jest--maxWorkers=4 (parallel) or --runInBand (serial)Use --runInBand when tests share a real database
Vitestpool: 'threads' (default) or pool: 'forks'Configure in vitest.config.ts
Got.Parallel() inside each testCall at the top of the test function; subtests inherit the pool
func TestCalculate(t *testing.T) {
    t.Parallel() // opt in to parallel execution

    t.Run("adds correctly", func(t *testing.T) {
        t.Parallel()
        result := Add(2, 3)
        if result != 5 {
            t.Errorf("expected 5, got %d", result)
        }
    })
}

Red Flags

  • Testing implementation details instead of behavior — asserting on private method calls or internal state ties tests to refactoring; test what the unit does, not how it does it
  • Mocking the unit under test — a mock of the same class being tested verifies nothing; mocks belong at external boundaries only (DB, HTTP, filesystem)
  • 100% line coverage as the target — chasing line coverage produces tests with no assertions ("did it run?" not "did it work?"); track branch coverage and mutation score instead
  • setUp that builds shared mutable state — shared state between tests creates ordering dependencies and flaky failures; each test must create its own independent fixtures
  • sleep() or time delays in tests — time-dependent tests are inherently flaky; inject a clock abstraction and control time explicitly in tests
  • Testing private methods directly — private methods are implementation details; test them through the public interface that uses them, or extract them into a separate collaborator
  • No tests for error paths — testing only the happy path misses the most common production bugs; every test file should cover each error condition and boundary case

Checklist

  • Tests follow AAA pattern with clear arrange/act/assert separation
  • Test names describe the scenario and expected outcome
  • Each test has a single logical assertion (one behaviour per test)
  • External dependencies (DB, HTTP, filesystem) are mocked or faked
  • Edge cases covered: null/None/nil, empty collections, boundary values, error paths
  • Parameterized tests used for multiple input/output scenarios of the same logic
  • No test relies on execution order or shared mutable state
  • Coverage meets project threshold for both line and branch coverage
  • Flaky tests are quarantined or fixed before merging to main
  • Test suite runs in under 60 seconds locally
  • Public interfaces are tested, not private implementation details
  • Mocks are only used for external boundaries, not for the unit under test
  • Fixtures and fakes are cleaned up after each test (no state leakage)
  • TDD red/green/refactor cycle followed for new business logic
  • Generated, migration, and configuration code is excluded from coverage requirements

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,790. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.