š Why Your App Is One Test Away from Disaster (And How to Fix It)
Hamza Khan
Posted on October 24, 2024
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;
}
}
š¤ 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');
});
});
});
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');
});
});
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');
});
});
š¤ 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();
});
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);
})
);
});
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();
});
});
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
šÆ 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!
Posted on October 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.