DEV Community

Hamza Khan
Hamza Khan

Posted on

๐Ÿš€ Why Your App Is One Test Away from Disaster (And How to Fix It)

Ever deployed code at 5 PM on a Friday, feeling confident, only to get that dreaded midnight call? We've all been there. But what if I told you there's a better way? Let's dive into how automated testing can save your weekends (and your sanity).

The Real Cost of Skipping Tests

Picture this: You're building a simple user authentication system. Seems straightforward, right?

class UserAuth {
  async validatePassword(password: string): Promise<boolean> {
    if (!password) return false;
    // Some validation logic here
    return password.length >= 8 && /[A-Z]/.test(password);
  }

  async loginUser(email: string, password: string): Promise<boolean> {
    const isValid = await this.validatePassword(password);
    if (!isValid) return false;
    // Login logic here
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿค” Pop Quiz: Can you spot potential issues in this code? What edge cases might we be missing?

Without proper testing, we might miss:

  • Empty email validation
  • Password complexity requirements
  • SQL injection vulnerabilities
  • Rate limiting concerns

The Testing Arsenal: Tools You Need

Before we dive deeper, let's look at the essential testing tools that will make your life easier:

1. Unit Testing

  • Jest: The Swiss Army knife of testing
  • Mocha: Flexible testing framework
  • Vitest: Ultra-fast unit testing

2. End-to-End Testing

  • Cypress: Modern web testing
  • Playwright: Cross-browser testing
  • Selenium: Traditional but powerful

3. Integration Testing

  • Supertest: HTTP assertions
  • TestCafe: No Selenium required
  • Postman: API testing made easy

4. Performance Testing

  • k6: Developer-centric load testing
  • Artillery: Load and performance testing
  • Apache JMeter: Comprehensive performance testing

๐Ÿ’ก Pro Tip: Start with Jest for unit tests and Cypress for E2E tests. Add others as needed.

Real-World Testing Scenarios

1. Authentication Flow Testing

describe('Authentication Flow', () => {
  describe('Login Process', () => {
    it('should handle rate limiting', async () => {
      const auth = new UserAuth();
      for (let i = 0; i < 5; i++) {
        await auth.loginUser('test@email.com', 'wrong');
      }

      await expect(
        auth.loginUser('test@email.com', 'correct')
      ).rejects.toThrow('Too many attempts');
    });

    it('should prevent timing attacks', async () => {
      const auth = new UserAuth();
      const start = process.hrtime();

      await auth.loginUser('fake@email.com', 'wrongpass');
      const end = process.hrtime(start);

      // Should take same time regardless of email existence
      expect(end[0]).toBeLessThan(1);
    });
  });

  describe('Password Reset', () => {
    it('should validate token expiration', async () => {
      const auth = new UserAuth();
      const token = await auth.generateResetToken('user@email.com');

      // Fast-forward time by 1 hour
      jest.advanceTimersByTime(3600000);

      await expect(
        auth.resetPassword(token, 'newPassword')
      ).rejects.toThrow('Token expired');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

2. E2E Testing with Cypress

describe('User Journey', () => {
  it('should complete signup and first task', () => {
    cy.intercept('POST', '/api/signup').as('signupRequest');

    // Visit signup page
    cy.visit('/signup');

    // Fill form
    cy.get('[data-testid="name-input"]').type('John Doe');
    cy.get('[data-testid="email-input"]').type('john@example.com');
    cy.get('[data-testid="password-input"]').type('SecurePass123!');

    // Submit and verify
    cy.get('[data-testid="signup-button"]').click();
    cy.wait('@signupRequest');

    // Should redirect to dashboard
    cy.url().should('include', '/dashboard');

    // Create first task
    cy.get('[data-testid="new-task"]').click();
    cy.get('[data-testid="task-title"]').type('My First Task');
    cy.get('[data-testid="save-task"]').click();

    // Verify task creation
    cy.contains('My First Task').should('be.visible');
  });
});
Enter fullscreen mode Exit fullscreen mode

3. API Integration Testing

describe('API Integration', () => {
  it('should handle third-party API failures gracefully', async () => {
    const paymentService = new PaymentService();

    // Mock failed API response
    jest.spyOn(paymentService, 'processPayment').mockRejectedValue(
      new Error('Gateway timeout')
    );

    const order = new Order({
      items: [{id: 1, quantity: 2}],
      total: 50.00
    });

    // Should retry and fall back to backup provider
    const result = await order.checkout();
    expect(result.status).toBe('success');
    expect(result.provider).toBe('backup-provider');
  });
});
Enter fullscreen mode Exit fullscreen mode

๐Ÿค” Question Time: How do you handle flaky tests in your CI pipeline?

Advanced Testing Patterns

1. Snapshot Testing

it('should maintain consistent UI components', () => {
  const button = render(<PrimaryButton label="Click me" />);
  expect(button).toMatchSnapshot();
});
Enter fullscreen mode Exit fullscreen mode

2. Property-Based Testing

import fc from 'fast-check';

test('string reversal is symmetric', () => {
  fc.assert(
    fc.property(fc.string(), str => {
      expect(reverseString(reverseString(str))).toBe(str);
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

3. Contract Testing

const userContract = {
  id: Joi.number().required(),
  email: Joi.string().email().required(),
  role: Joi.string().valid('user', 'admin')
};

describe('User API', () => {
  it('should return data matching contract', async () => {
    const response = await request(app).get('/api/user/1');
    const validation = Joi.validate(response.body, userContract);
    expect(validation.error).toBeNull();
  });
});
Enter fullscreen mode Exit fullscreen mode

Setting Up Your Testing Pipeline

# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: '18'

      - name: Install Dependencies
        run: npm ci

      - name: Unit Tests
        run: npm run test:unit

      - name: Integration Tests
        run: npm run test:integration

      - name: E2E Tests
        run: npm run test:e2e

      - name: Upload Coverage
        uses: codecov/codecov-action@v2
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ Challenge: Set up a pre-commit hook that runs tests and maintains a minimum coverage threshold!

Best Practices Checklist

โœ… Write tests before fixing bugs
โœ… Use data-testid for E2E test selectors
โœ… Implement retry logic for flaky tests
โœ… Maintain test isolation
โœ… Mock external dependencies
โœ… Use test doubles appropriately (stubs, spies, mocks)
โœ… Follow the AAA pattern (Arrange, Act, Assert)

Conclusion

Remember: Every untested function is a potential bug waiting to happen. But with automated testing, you're not just writing code โ€“ you're building confidence.

๐ŸŽฏ Final Challenge: Take your most critical piece of untested code and write at least three test cases for it today. Share your experience in the comments!

Top comments (0)