Claude-skill-registry gate-4a-unit-tests

Gate 4a validation: Unit test creation and nock-only enforcement

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/gate-4a-unit-tests" ~/.claude/skills/majiayu000-claude-skill-registry-gate-4a-unit-tests && rm -rf "$T"
manifest: skills/data/gate-4a-unit-tests/SKILL.md
source content

Gate 4a: Unit Test Creation

Purpose: Validate unit test quality, mocking strategy, and coverage

STOP AND CHECK:

# 1. Unit test files exist
if ! ls test/unit/*Test.ts 2>/dev/null | head -1 > /dev/null; then
  echo "❌ Gate 4a FAILED: No unit test files found"
  echo "   Create test/unit/{Resource}ProducerTest.ts"
  echo "   See unit-test-patterns skill"
  exit 1
fi

TEST_FILE_COUNT=$(ls test/unit/*Test.ts 2>/dev/null | wc -l)
echo "✅ Found $TEST_FILE_COUNT unit test file(s)"

# 2. Test suites exist with proper structure
DESCRIBE_COUNT=$(grep -h "^describe(" test/unit/*.ts 2>/dev/null | wc -l)
if [ "$DESCRIBE_COUNT" -lt 1 ]; then
  echo "❌ Gate 4a FAILED: No describe blocks found"
  echo "   Unit tests must have describe blocks"
  exit 1
fi
echo "✅ Found $DESCRIBE_COUNT describe block(s)"

# 3. Multiple test cases (3+ per operation)
IT_COUNT=$(grep -h "^\s*it(" test/unit/*.ts 2>/dev/null | wc -l)
if [ "$IT_COUNT" -lt 3 ]; then
  echo "❌ Gate 4a FAILED: Insufficient test cases (found $IT_COUNT, need 3+)"
  echo "   Each operation needs multiple test cases including error scenarios"
  exit 1
fi
echo "✅ Found $IT_COUNT test case(s)"

# 4. CRITICAL: ONLY nock used for HTTP mocking
if ! grep -h "from ['\"]nock['\"]" test/unit/*.ts 2>/dev/null | head -1 > /dev/null; then
  echo "❌ Gate 4a FAILED: nock not imported in unit tests"
  echo "   Unit tests MUST use nock for HTTP mocking"
  echo ""
  echo "Add to test files:"
  echo "  import * as nock from 'nock';"
  echo ""
  echo "See nock-patterns skill for examples"
  exit 1
fi
echo "✅ nock imported for HTTP mocking"

# 5. CRITICAL: No forbidden mocking libraries
FORBIDDEN_MOCKS=$(grep -h -E "jest\.mock|sinon|fetch-mock|msw" test/unit/*.ts 2>/dev/null || true)
if [ -n "$FORBIDDEN_MOCKS" ]; then
  echo "❌ Gate 4a FAILED: Forbidden mocking library detected"
  echo "   ONLY nock is allowed for HTTP mocking"
  echo ""
  echo "Found forbidden libraries:"
  echo "$FORBIDDEN_MOCKS"
  echo ""
  echo "Fix: Replace with nock HTTP mocking"
  echo "See failure-conditions skill Rule #4"
  exit 1
fi
echo "✅ No forbidden mocking libraries"

# 6. Verify Common.ts exists and is properly structured
if [ ! -f "test/unit/Common.ts" ]; then
  echo "❌ Gate 4a FAILED: test/unit/Common.ts missing"
  echo "   Create Common.ts with getConnectedInstance helper"
  echo "   See unit-test-patterns skill for template"
  exit 1
fi

# Verify Common.ts uses nock (not real credentials)
if ! grep -q "import.*nock" test/unit/Common.ts; then
  echo "⚠️  WARNING: Common.ts should import nock"
  echo "   Unit test Common.ts must use mocked HTTP, not real API"
fi

# Verify Common.ts does NOT use dotenv or process.env
if grep -E "dotenv|process\.env" test/unit/Common.ts > /dev/null 2>&1; then
  echo "❌ Gate 4a FAILED: Common.ts uses environment variables"
  echo "   Unit tests must NOT depend on .env or process.env"
  echo "   Use nock to mock HTTP requests instead"
  echo ""
  echo "Found:"
  grep -n -E "dotenv|process\.env" test/unit/Common.ts
  exit 1
fi
echo "✅ Common.ts properly structured (no env vars)"

# 7. Verify error cases are tested
ERROR_TESTS=$(grep -h -i "error\|fail\|invalid\|unauthorized\|notfound\|404\|401" test/unit/*.ts 2>/dev/null | wc -l)
if [ "$ERROR_TESTS" -lt 1 ]; then
  echo "⚠️  WARNING: No error case tests found"
  echo "   Tests should cover error scenarios:"
  echo "   - 401 → InvalidCredentialsError"
  echo "   - 404 → NoSuchObjectError"
  echo "   - 429 → RateLimitExceededError"
fi

# 8. Verify mocking at HTTP level (not method level)
METHOD_MOCKS=$(grep -h -E "\.mockReturnValue\(|\.mockResolvedValue\(|\.mockImplementation\(" test/unit/*.ts 2>/dev/null || true)
if [ -n "$METHOD_MOCKS" ]; then
  echo "⚠️  WARNING: Method-level mocking detected"
  echo "   Use HTTP-level mocking with nock instead"
  echo ""
  echo "Found:"
  echo "$METHOD_MOCKS"
  echo ""
  echo "Fix: Use nock('https://api.example.com').get('/path').reply(200, data);"
fi

# 9. Verify nock cleanup in tests
if ! grep -h "nock.cleanAll()" test/unit/*.ts > /dev/null 2>&1; then
  echo "⚠️  WARNING: No nock.cleanAll() found"
  echo "   Add afterEach(() => nock.cleanAll()) to clean up mocks"
fi

# 10. Check test file naming convention
for file in test/unit/*Test.ts; do
  if [ -f "$file" ]; then
    basename=$(basename "$file")
    if [[ ! "$basename" =~ ^[A-Z][a-zA-Z]*Test\.ts$ ]] && [[ "$basename" != "Common.ts" ]]; then
      echo "⚠️  WARNING: Incorrect test file naming: $basename"
      echo "   Should be: {Resource}ProducerTest.ts or ConnectionTest.ts"
    fi
  fi
done

echo ""
echo "✅ Gate 4a: Unit Test Creation - PASSED"

PROCEED ONLY IF:

  • ✅ Unit test file(s) exist in test/unit/
  • ✅ Test suites have describe blocks
  • ✅ At least 3 test cases per operation
  • ✅ Error cases are covered
  • ONLY nock used for HTTP mocking (CRITICAL)
  • NO forbidden mocking libraries (jest.mock, sinon, fetch-mock, msw)
  • ✅ Common.ts exists and uses nock (NO env vars)
  • ✅ Mocking at HTTP level (not method/function level)
  • ✅ nock cleanup present (afterEach)

IF FAILED:

  • Unit tests are MANDATORY
  • Fix mocking violations immediately
  • See unit-test-patterns skill for correct patterns
  • See nock-patterns skill for HTTP mocking examples

Common Failures

1. Using jest.mock or sinon

// ❌ WRONG - forbidden mocking library
import { jest } from '@jest/globals';
jest.mock('./ServiceClient');

const mockClient = {
  getUser: jest.fn().mockResolvedValue({ id: '123' })
};

// ❌ WRONG - sinon stubs
import sinon from 'sinon';
const stub = sinon.stub(client, 'getUser').resolves({ id: '123' });

// ✅ CORRECT - nock HTTP mocking
import * as nock from 'nock';

nock('https://api.service.com')
  .get('/users/123')
  .reply(200, { id: '123', name: 'John' });

const user = await producer.getUser('123');

Why:

  • nock mocks at HTTP network level
  • Consistent with existing test infrastructure
  • Works with any HTTP client library
  • Can record and replay real API interactions

2. Method-level mocking

// ❌ WRONG - mocking methods
const mockGetUser = jest.fn().mockResolvedValue({ id: '123' });
client.getUser = mockGetUser;

// ❌ WRONG - mocking class instance
jest.spyOn(client, 'getUser').mockReturnValue(Promise.resolve({ id: '123' }));

// ✅ CORRECT - HTTP level with nock
import * as nock from 'nock';

nock('https://api.service.com')
  .get('/users/123')
  .reply(200, { id: '123', name: 'John' });

// Call real implementation - nock intercepts HTTP request
const user = await producer.getUser('123');
expect(user.id).to.be.instanceof(UUID);

Why:

  • Tests real implementation code paths
  • Verifies HTTP request formatting
  • Ensures proper error handling
  • Catches integration issues

3. No error case tests

// ❌ WRONG - only happy path
describe('UserProducer', () => {
  it('should list users', async () => {
    nock('https://api.service.com')
      .get('/users')
      .reply(200, [{ id: '123' }]);

    const users = await producer.listUsers();
    expect(users).to.have.length(1);
  });
});

// ✅ CORRECT - include error cases
describe('UserProducer', () => {
  it('should list users successfully', async () => {
    nock('https://api.service.com')
      .get('/users')
      .reply(200, [{ id: '123' }]);

    const users = await producer.listUsers();
    expect(users).to.have.length(1);
  });

  it('should throw InvalidCredentialsError on 401', async () => {
    nock('https://api.service.com')
      .get('/users')
      .reply(401, { error: 'Unauthorized' });

    try {
      await producer.listUsers();
      expect.fail('Should have thrown an error');
    } catch (error: any) {
      expect(error).to.be.instanceOf(InvalidCredentialsError);
    }
  });

  it('should throw NoSuchObjectError on 404', async () => {
    nock('https://api.service.com')
      .get('/users/999')
      .reply(404, { error: 'Not found' });

    try {
      await producer.getUser('999');
      expect.fail('Should have thrown an error');
    } catch (error: any) {
      expect(error).to.be.instanceOf(NoSuchObjectError);
    }
  });

  it('should throw RateLimitExceededError on 429', async () => {
    nock('https://api.service.com')
      .get('/users')
      .reply(429, { error: 'Rate limit exceeded' });

    try {
      await producer.listUsers();
      expect.fail('Should have thrown an error');
    } catch (error: any) {
      expect(error).to.be.instanceOf(RateLimitExceededError);
    }
  });
});

Why:

  • Error handling is critical for production code
  • Verifies correct error type mapping
  • Tests edge cases and failure modes
  • Ensures graceful degradation

4. Missing nock cleanup

// ❌ WRONG - no cleanup
describe('UserProducer', () => {
  it('should get user', async () => {
    nock('https://api.service.com')
      .get('/users/123')
      .reply(200, { id: '123' });

    const user = await producer.getUser('123');
    expect(user.id).to.be.instanceof(UUID);
  });
});

// ✅ CORRECT - cleanup after each test
describe('UserProducer', () => {
  afterEach(() => {
    nock.cleanAll();
  });

  it('should get user', async () => {
    nock('https://api.service.com')
      .get('/users/123')
      .reply(200, { id: '123' });

    const user = await producer.getUser('123');
    expect(user.id).to.be.instanceof(UUID);
  });

  it('should get another user', async () => {
    nock('https://api.service.com')
      .get('/users/456')
      .reply(200, { id: '456' });

    const user = await producer.getUser('456');
    expect(user.id).to.be.instanceof(UUID);
  });
});

Why:

  • Prevents test interference
  • Ensures clean state between tests
  • Avoids "Nock: No match for request" errors
  • Makes tests independent and reliable

5. Unit test Common.ts using environment variables

// ❌ WRONG - unit test Common.ts with env vars
import { config } from 'dotenv';
import { Email } from '@zerobias-org/types-core-js';
import { newService } from '../../src';

config(); // NO! Unit tests don't use .env

export const SERVICE_EMAIL = process.env.SERVICE_EMAIL || '';
export const SERVICE_PASSWORD = process.env.SERVICE_PASSWORD || '';

export async function getConnectedInstance() {
  const connector = newService();
  await connector.connect({
    email: new Email(SERVICE_EMAIL),
    password: SERVICE_PASSWORD,
  });
  return connector;
}

// ✅ CORRECT - unit test Common.ts with nock
import * as nock from 'nock';
import { Email } from '@zerobias-org/types-core-js';
import { newService } from '../../src';
import type { ServiceConnector } from '../../src';

/**
 * Get a connected instance for unit testing.
 * Uses mocked HTTP - no real credentials needed.
 * Unit tests should NEVER depend on environment variables.
 */
export async function getConnectedInstance(): Promise<ServiceConnector> {
  nock('https://api.example.com')
    .post('/auth/login')
    .reply(200, {
      accessToken: 'test-token-123',
      expiresAt: '2025-10-02T00:00:00Z',
    });

  const connector = newService();

  await connector.connect({
    email: new Email('test@example.com'),
    password: 'testpass',
  });

  return connector;
}

Why:

  • Unit tests must work without .env file
  • Should be deterministic and repeatable
  • Credentials belong in integration tests only
  • Mocked tests don't need real values

Validation Scripts

Check unit test structure

# Verify unit tests exist
ls -lh test/unit/*Test.ts

# Count test suites and cases
echo "Describe blocks:"
grep -h "^describe(" test/unit/*.ts | wc -l

echo "Test cases:"
grep -h "^\s*it(" test/unit/*.ts | wc -l

echo "Error tests:"
grep -h -i "error\|404\|401\|429" test/unit/*.ts | wc -l

Validate nock usage

# Check nock imports
echo "Files importing nock:"
grep -l "from ['\"]nock['\"]" test/unit/*.ts

# Check for forbidden libraries
echo "Checking for forbidden mocking libraries:"
grep -n -E "jest\.mock|sinon|fetch-mock|msw" test/unit/*.ts && \
  echo "❌ FAIL: Forbidden mocking library found" || \
  echo "✅ PASS: Only nock used"

# Check nock cleanup
echo "Files with nock.cleanAll():"
grep -l "nock.cleanAll()" test/unit/*.ts

Validate Common.ts

# Check Common.ts structure
echo "Checking test/unit/Common.ts:"

# Should use nock
grep -q "import.*nock" test/unit/Common.ts && \
  echo "✅ Uses nock" || \
  echo "❌ Should import nock"

# Should NOT use dotenv or process.env
grep -E "dotenv|process\.env" test/unit/Common.ts && \
  echo "❌ FAIL: Uses env vars (forbidden in unit tests)" || \
  echo "✅ No env vars"

Run unit tests

# Run unit tests (should work without .env file)
npm run test:unit

# Expected: All tests pass, no env file needed

Related Rules

  • unit-test-patterns skill ⭐ - All unit test patterns and examples
  • nock-patterns skill ⭐ - HTTP mocking with nock (ONLY allowed library)
  • testing-core-rules skill - General testing principles
  • failure-conditions skill - Rule #4: Forbidden mocking libraries
  • test-fixture-patterns skill - Test data and fixture organization

Success Criteria

Unit test creation MUST meet ALL criteria:

  • ✅ Unit test files exist in test/unit/
  • ✅ Each operation has unit tests (ConnectionTest.ts + {Resource}ProducerTest.ts)
  • ✅ At least 3 test cases per operation
  • ✅ Both success and error cases tested
  • ONLY nock used for HTTP mocking (CRITICAL)
  • NO jest.mock, sinon, fetch-mock, or msw (CRITICAL)
  • ✅ HTTP-level mocking (not method/function level)
  • ✅ Common.ts exists with getConnectedInstance helper
  • ✅ Common.ts uses nock (NO dotenv or process.env)
  • ✅ nock.cleanAll() in afterEach blocks
  • ✅ Proper test file naming convention
  • ✅ Unit tests pass without .env file
  • ✅ 100% unit test pass rate