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)