Agent skill
testing
Writing tests for the B2C CLI project using Mocha, Chai, and MSW. Use when writing unit or integration tests, mocking HTTP requests with MSW, testing CLI commands with oclif, isolating config in tests, setting up test coverage, using sinon stubs, c8 coverage, capturing or silencing stdout, or creating MSW handlers.
Install this agent skill to your Project
npx add-skill https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/tree/main/.claude/skills/testing
Metadata
Additional technical details for this skill
- internal
- YES
SKILL.md
Testing
This skill covers project-specific testing patterns for the B2C CLI project.
Test Framework Stack
- Test Runner: Mocha
- Assertions: Chai (property-based)
- HTTP Mocking: MSW (Mock Service Worker)
- Stubbing/Mocking: Sinon
- Code Coverage: c8
- TypeScript: tsx (native execution without compilation)
Running Tests
For coding agents (minimal output - only failures shown):
# Run tests - only failures + summary
pnpm run test:agent
# Run tests for specific package
pnpm --filter @salesforce/b2c-tooling-sdk run test:agent
pnpm --filter @salesforce/b2c-cli run test:agent
For debugging (full output with coverage):
# Run all tests with coverage
pnpm run test
# Run tests for specific package
pnpm --filter @salesforce/b2c-tooling-sdk run test
pnpm --filter @salesforce/b2c-cli run test
# Run single test file (no coverage, faster)
cd packages/b2c-tooling-sdk
pnpm mocha "test/clients/webdav.test.ts"
# Run tests matching pattern
pnpm mocha --grep "mkcol" "test/**/*.test.ts"
# Watch mode for TDD
pnpm --filter @salesforce/b2c-tooling-sdk run test:watch
Test Organization
Tests mirror the source directory structure with .test.ts suffix:
packages/b2c-tooling-sdk/
├── src/
│ └── clients/
│ └── webdav.ts
└── test/
└── clients/
└── webdav.test.ts
Import Patterns
Always use package exports, not relative paths:
// Good - uses package exports
import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients';
import { OAuthStrategy } from '@salesforce/b2c-tooling-sdk/auth';
// Avoid - relative paths
import { WebDavClient } from '../../src/clients/webdav.js';
This ensures tests use the same export paths as consumers.
Config Isolation
Tests that check for "missing credentials" or "no config" scenarios need isolation from the developer's real configuration files (~/.mobify, dw.json) and environment variables.
Using Config Isolation Helpers
import { isolateConfig, restoreConfig } from '../helpers/config-isolation.js';
describe('config-dependent tests', () => {
beforeEach(() => {
isolateConfig();
});
afterEach(() => {
restoreConfig();
});
it('handles missing credentials', async () => {
// Test now runs without reading real ~/.mobify or SFCC_* env vars
});
});
The helpers:
- Clear all
SFCC_*andMRT_*environment variables - Clear other config-affecting vars (
LANGUAGE,NO_COLOR) - Must call
restoreConfig()in afterEach to restore original state
For SDK Unit Tests (bypass config sources)
When testing resolveConfig directly without file system:
import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config';
const config = resolveConfig({}, {
replaceDefaultSources: true,
sources: [] // No file-based sources
});
For MRT Credential Isolation
Use the credentialsFile option to override the default ~/.mobify path:
import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config';
// Point to non-existent file for isolation
const config = resolveConfig({}, {
credentialsFile: '/dev/null'
});
In CLI command tests, use the stubParse helper with the credentials-file flag:
import { stubParse } from '../helpers/stub-parse.js';
stubParse(command, {'credentials-file': '/dev/null'}); // Isolates from real ~/.mobify
Polling Tests (Avoid Fake Timers)
Do not use fake timers with MSW. MSW v2 uses microtasks internally, and fake timers prevent MSW's promises from resolving.
Instead, use the pollInterval option for fast tests:
// Good - use short poll interval
const result = await siteArchiveImport(mockInstance, siteDir, {
archiveName: 'test-import',
waitOptions: { pollInterval: 10 } // 10ms instead of default 3000ms
});
// Bad - fake timers break MSW
import FakeTimers from '@sinonjs/fake-timers';
const clock = FakeTimers.install(); // DON'T DO THIS with MSW
HTTP Mocking with MSW
Basic Setup
import { expect } from 'chai';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients';
import { MockAuthStrategy } from '../helpers/mock-auth.js';
const TEST_HOST = 'test.salesforce.com';
const BASE_URL = `https://${TEST_HOST}`;
const server = setupServer();
describe('WebDavClient', () => {
let client: WebDavClient;
let mockAuth: MockAuthStrategy;
before(() => {
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
server.resetHandlers();
});
after(() => {
server.close();
});
beforeEach(() => {
mockAuth = new MockAuthStrategy();
client = new WebDavClient(TEST_HOST, mockAuth);
});
it('creates a directory successfully', async () => {
server.use(
http.all(`${BASE_URL}/*`, ({ request }) => {
if (request.method === 'MKCOL') {
return new HttpResponse(null, { status: 201 });
}
return new HttpResponse(null, { status: 405 });
}),
);
await client.mkcol('Cartridges/v1');
});
});
Request Capture Pattern
To verify request details, capture requests in an array:
interface CapturedRequest {
method: string;
url: string;
headers: Headers;
body?: unknown;
}
const requests: CapturedRequest[] = [];
beforeEach(() => {
requests.length = 0;
});
it('sends correct headers', async () => {
server.use(
http.put(`${BASE_URL}/*`, async ({ request }) => {
requests.push({
method: request.method,
url: request.url,
headers: request.headers,
body: await request.text(),
});
return new HttpResponse(null, { status: 201 });
}),
);
await client.put('path/to/file', Buffer.from('content'));
expect(requests).to.have.length(1);
expect(requests[0].method).to.equal('PUT');
expect(requests[0].headers.get('Authorization')).to.equal('Bearer test-token');
});
Error Responses
it('handles 404 errors', async () => {
server.use(
http.get(`${BASE_URL}/api/items/:id`, () => {
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
}),
);
try {
await client.getItem('nonexistent');
expect.fail('Should have thrown');
} catch (error) {
expect(error.message).to.include('404');
}
});
it('handles network errors', async () => {
server.use(
http.get(`${BASE_URL}/api/items`, () => {
return HttpResponse.error();
}),
);
try {
await client.listItems();
expect.fail('Should have thrown');
} catch (error) {
expect(error.message).to.include('network');
}
});
MockAuthStrategy
Use the test helper for authentication:
// test/helpers/mock-auth.ts
import type { AuthStrategy } from '@salesforce/b2c-tooling-sdk/auth';
export class MockAuthStrategy implements AuthStrategy {
constructor(private token: string = 'test-token') {}
async fetch(url: string, init?: RequestInit): Promise<Response> {
const headers = new Headers(init?.headers);
headers.set('Authorization', `Bearer ${this.token}`);
return fetch(url, { ...init, headers });
}
async getAuthorizationHeader(): Promise<string> {
return `Bearer ${this.token}`;
}
}
Usage:
import { MockAuthStrategy } from '../helpers/mock-auth.js';
const mockAuth = new MockAuthStrategy();
const client = new WebDavClient(TEST_HOST, mockAuth);
// Custom token for specific tests
const customAuth = new MockAuthStrategy('custom-token');
Silencing Test Output & Command Test Guidelines
See Command Testing Patterns for detailed patterns on:
- Silencing output with
stubCommandConfigAndLoggerandrunSilent - Verifying console output content
stubParsesilent logging defaults- What to test (and what not to test) in commands
- Low-value tests to avoid
Quick reference: Use stubCommandConfigAndLogger(command) for AM/sandbox-style commands, runSilent(() => command.run()) for everything else. stubParse sets 'log-level': 'silent' automatically.
Testing CLI Commands with oclif
See CLI Command Testing Patterns for integration tests with runCommand, SDK base command fixture tests, E2E test patterns, and when to use each approach.
Coverage
Coverage is configured in .c8rc.json. View the HTML report after running tests:
pnpm run test
open coverage/index.html
Test Helpers Reference
See Test Helpers Reference for a full list of helpers available in both the CLI and SDK packages.
Troubleshooting
MSW handler not matching requests: Verify the URL pattern in http.get()/http.post() matches the full URL including base path. Use onUnhandledRequest: 'error' in server.listen() to surface unmatched requests. Check that the HTTP method matches (e.g., http.all() for WebDAV methods like MKCOL/PROPFIND).
Config or env vars leaking between tests: Always pair isolateConfig() with restoreConfig() in beforeEach/afterEach. Missing restoreConfig() causes subsequent tests to run with cleared env vars. Use sinon.restore() in afterEach to clean up all stubs.
Import path errors ("module not found"): Use package exports (@salesforce/b2c-tooling-sdk/clients) not relative paths. If a new export was added, ensure it's in package.json exports with the development condition pointing to the .ts source file.
Fake timers break MSW: MSW v2 uses microtasks internally. Never use @sinonjs/fake-timers or sinon.useFakeTimers() in tests that use MSW. Use pollInterval: 10 for fast polling tests instead.
Test output is noisy: Use runSilent() to suppress stdout/stderr from commands. The stubParse helper automatically sets 'log-level': 'silent' to quiet pino logger output.
Writing Tests Checklist
- Create test file in
test/mirroring source structure - Use
.test.tssuffix - Import from package names, not relative paths
- Set up MSW server for HTTP tests (avoid fake timers)
- Use
isolateConfig()/restoreConfig()for config-dependent tests - Use
runSilent()for commands that produce console output - Use
pollIntervaloption for polling operations - Use MockAuthStrategy for authenticated clients
- Test both success and error paths
- Focus on command-specific logic, not trivial delegation
- Run tests:
pnpm --filter <package> run test
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
api-client-development
Creating typed API clients with OpenAPI specs, authentication, and OAuth scopes for SCAPI and similar APIs. Use when adding a new SCAPI client, generating types from an OpenAPI spec, setting up OAuth middleware, or integrating a new Commerce API endpoint.
documentation
Updating user guides, CLI reference, and API documentation for the B2C CLI project. Use when adding or changing CLI command docs, writing JSDoc for TypeDoc generation, updating Vitepress sidebar config, or creating new guide pages.
cli-command-development
Creating new CLI commands and topics for the B2C CLI using oclif. Use when adding a new command, creating a topic, adding flags or arguments, implementing table output, or extending BaseCommand/OAuthCommand/InstanceCommand.
sdk-module-development
Adding new modules and exports to the @salesforce/b2c-tooling-sdk package. Use when creating a new SDK module, adding barrel file exports, configuring package.json exports, or building client factory functions.
b2c-code
Deploy and manage code versions/cartridges on B2C Commerce instances/sandboxes with the b2c cli. Always reference when using the CLI to upload cartridges, deploy code, activate code versions, manage code versions, or watch for file changes during development.
b2c-slas
Manage SLAS (Shopper Login and API Access Service) clients for B2C Commerce (SFCC/Demandware) with the b2c cli. Always reference when using the CLI to create, update, list, or delete SLAS clients, manage shopper OAuth scopes (including custom scopes like c_loyalty), or configure shopper authentication for PWA/headless. SLAS is for shopper (customer) authentication, not admin APIs.
Didn't find tool you were looking for?