agmission/Development/server/docs/TESTING_GUIDE.md

7.0 KiB

Test Framework Documentation

Quick Start

# Run all tests (files ending in .spec.js)
npm test

# Run all test files (including test_*.js)
npm run test:all

# Run with watch mode (re-runs on file changes)
npm run test:watch

# Run a single test file
npm run test:single tests/sample.spec.js

# Run with coverage report
npm run test:coverage

# Coverage for all tests
npm run test:coverage-all

How Mocha Handles Test Failures

Key Behaviors:

  1. Runs ALL tests - Mocha does NOT stop at the first failure

    • All tests are executed regardless of failures
    • Summary shows total passing/failing at the end
  2. Clear failure identification

    • Each failure is numbered (1), 2), 3), etc.)
    • Shows the full test path: Suite > Subsuite > Test Name
    • Displays exact line number: tests/sample.spec.js:25:25
  3. Detailed error messages

    • Shows expected vs actual values
    • Color-coded diff (red for actual, green for expected)
    • Full stack trace for debugging
  4. Exit code indicates failures

    • Exit code 0 = all tests passed
    • Exit code > 0 = number of failures (capped at certain value)
    • CI/CD systems can detect failures automatically

Example Output:

  10 passing (106ms)
  4 failing

  1) Sample Test Suite - Basic Math
       Addition
         should handle zero (INTENTIONAL FAIL):
      AssertionError: expected +0 to equal 1
      at Context.<anonymous> (tests/sample.spec.js:25:25)

How to Locate Failed Tests:

  1. Line numbers: Click the link tests/sample.spec.js:25:25 in VSCode terminal
  2. Test hierarchy: Follow the nested structure (Suite > Subsuite > Test)
  3. Search: Copy the test name and use Ctrl+F in your test file
  4. Summary: Scroll to top for count: "10 passing, 4 failing"

Test File Naming

  • *.spec.js - Unit/integration tests (run with npm test)
  • test_*.js - Manual test scripts (run with npm run test:all or individually)

Writing Tests

Basic Structure:

const { expect } = require('chai');

describe('Feature Name', () => {
  
  describe('Sub-feature', () => {
    
    it('should do something specific', () => {
      const result = myFunction();
      expect(result).to.equal(expectedValue);
    });
    
    it('should handle edge cases', async () => {
      const result = await asyncFunction();
      expect(result).to.be.an('object');
      expect(result.status).to.equal('success');
    });
  });
});

Common Assertions (Chai):

// Equality
expect(value).to.equal(42);
expect(obj).to.deep.equal({ a: 1, b: 2 });

// Types
expect(value).to.be.a('string');
expect(arr).to.be.an('array');

// Arrays
expect(arr).to.have.lengthOf(3);
expect(arr).to.include(item);
expect(arr).to.deep.equal([1, 2, 3]);

// Objects
expect(obj).to.have.property('name');
expect(obj.name).to.equal('test');

// Numbers
expect(num).to.be.above(10);
expect(num).to.be.at.least(5);
expect(num).to.be.below(100);

// Existence
expect(value).to.exist;
expect(value).to.be.null;
expect(value).to.be.undefined;

// Async (returns promise)
await expect(promise).to.be.fulfilled;
await expect(promise).to.be.rejected;

Test Lifecycle Hooks:

describe('Feature', () => {
  
  before(() => {
    // Runs once before all tests in this suite
  });

  after(() => {
    // Runs once after all tests in this suite
  });

  beforeEach(() => {
    // Runs before each test
  });

  afterEach(() => {
    // Runs after each test
  });

  it('test 1', () => { /* ... */ });
  it('test 2', () => { /* ... */ });
});

Async Tests:

// Using async/await (preferred)
it('should fetch data', async () => {
  const data = await fetchData();
  expect(data).to.exist;
});

// Using done callback
it('should call callback', (done) => {
  asyncFunction((err, result) => {
    expect(err).to.be.null;
    expect(result).to.equal('success');
    done();
  });
});

Skipping Tests:

// Skip a single test
it.skip('should be skipped', () => { /* ... */ });

// Skip entire suite
describe.skip('Skipped Suite', () => { /* ... */ });

// Run only specific tests (useful for debugging)
it.only('should run only this test', () => { /* ... */ });
describe.only('Only Suite', () => { /* ... */ });

Coverage Reports

After running npm run test:coverage:

  • Open coverage/index.html in browser for detailed coverage report
  • Shows line, branch, function, and statement coverage
  • Highlights uncovered lines in red

Environment Variables

Tests automatically load from environment.env via tests/setup.js.

To use a different env file:

npm run test:single -- tests/my_test.spec.js --env ./environment_prod.env

CI/CD Integration

Example GitHub Actions workflow:

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: npm ci
      - run: npm test
      - run: npm run test:coverage
      - uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

Best Practices

  1. Test naming: Use descriptive names that explain what's being tested

    • should return 404 when user not found
    • test user function
  2. One assertion per test: Focus each test on a single behavior

    • Makes failures easier to diagnose
    • Tests are more maintainable
  3. Use beforeEach/afterEach: Keep tests independent

    • Create fresh test data for each test
    • Clean up after tests complete
  4. Mock external services: Don't hit real APIs in unit tests

    • Use sinon for mocking
    • Faster tests, no rate limits
  5. Test data isolation: Use unique identifiers (timestamps)

    • Prevents test conflicts
    • Avoids cleanup issues
  6. Rate limiting: Add delays between API calls (see STRIPE_RATE_LIMITING in copilot-instructions)

Troubleshooting

Tests hang and don't exit:

  • Ensure async operations complete
  • Close database/queue connections in after() hooks
  • Use --exit flag (already in npm scripts)

Environment variables not loaded:

  • Check tests/setup.js is required
  • Verify environment.env exists and has correct values

Can't find modules:

  • Run npm install to ensure all dependencies installed
  • Check file paths are relative to project root

Converting Existing Tests

To convert an existing test_*.js file to Mocha:

  1. Wrap test logic in describe and it blocks
  2. Replace console assertions with expect() assertions
  3. Remove manual environment loading (handled by setup.js)
  4. Rename to *.spec.js or keep as test_*.js and run with npm run test:all

Example:

// Before (manual script)
console.log('Testing addition...');
const result = 2 + 2;
if (result !== 4) {
  console.error('FAILED: Expected 4, got', result);
  process.exit(1);
}
console.log('✅ PASSED');

// After (Mocha)
const { expect } = require('chai');

describe('Math Operations', () => {
  it('should add numbers correctly', () => {
    const result = 2 + 2;
    expect(result).to.equal(4);
  });
});