0

How to write tests that actually catch bugs

Most test suites I've inherited test the wrong things. They have 90% coverage and still miss every real bug. Here's what I've learned about writing tests that matter.

Test Behavior, Not Implementation

// Bad: tests implementation details
test('should call database.save', () => {
    const spy = jest.spyOn(database, 'save');
        createUser({ name: 'Alice' });
            expect(spy).toHaveBeenCalledWith({ name: 'Alice', role: 'user' });
            });
            
            // Good: tests behavior
            test('new users get the default role', () => {
                const user = createUser({ name: 'Alice' });
                    expect(user.role).toBe('user');
                    });
                    ```
                    
                    The bad test breaks if you rename the method, change the ORM, or refactor the internals — even if the behavior is correct. The good test only breaks if the actual behavior changes.
                    
                    ## Test Boundaries, Not Everything
                    
                    ```
                    ┌─────────────────────┐
                    │   API Endpoint      │ ← Integration test
                    ├─────────────────────┤
                    │   Business Logic    │ ← Unit test (pure functions)
                    ├─────────────────────┤
                    │   Database Layer    │ ← Integration test
                    └─────────────────────┘
                    ```
                    
                    - **Unit test** pure functions: validators, calculators, formatters, parsers
                    - **Integration test** boundaries: API endpoints, database queries
                    - **Don't test** glue code: `getUser` that just calls `db.users.findById(id)`
                    
                    ## The 3 Tests That Catch Real Bugs
                    
                    ### 1. Edge Cases
                    
                    ```javascript
                    test('handles empty cart', () => {
                        expect(calculateTotal([])).toBe(0);
                        });
                        
                        test('handles negative quantities', () => {
                            expect(() => addToCart(item, -1)).toThrow('Quantity must be positive');
                            });
                            
                            test('handles concurrent updates', async () => {
                                const [result1, result2] = await Promise.all([
                                        updateBalance(userId, +100),
                                                updateBalance(userId, -50),
                                                    ]);
                                                        const balance = await getBalance(userId);
                                                            expect(balance).toBe(initialBalance + 50);
                                                            });
                                                            ```
                                                            
                                                            ### 2. Error Paths
                                                            
                                                            ```javascript
                                                            test('returns 404 when user not found', async () => {
                                                                const res = await request(app).get('/users/nonexistent');
                                                                    expect(res.status).toBe(404);
                                                                        expect(res.body.error.code).toBe('USER_NOT_FOUND');
                                                                        });
                                                                        
                                                                        test('retries on transient database error', async () => {
                                                                            database.query
                                                                                    .mockRejectedValueOnce(new Error('connection reset'))
                                                                                            .mockResolvedValueOnce({ rows: [user] });
                                                                                            
                                                                                                const result = await getUser(userId);
                                                                                                    expect(result).toEqual(user);
                                                                                                        expect(database.query).toHaveBeenCalledTimes(2);
                                                                                                        });
                                                                                                        ```
                                                                                                        
                                                                                                        ### 3. State Transitions
                                                                                                        
                                                                                                        ```javascript
                                                                                                        test('order lifecycle', async () => {
                                                                                                            const order = await createOrder(items);
                                                                                                                expect(order.status).toBe('pending');
                                                                                                                
                                                                                                                    await payOrder(order.id, paymentInfo);
                                                                                                                        expect((await getOrder(order.id)).status).toBe('paid');
                                                                                                                        
                                                                                                                            await shipOrder(order.id, trackingNumber);
                                                                                                                                expect((await getOrder(order.id)).status).toBe('shipped');
                                                                                                                                
                                                                                                                                    // Can't pay a shipped order
                                                                                                                                        await expect(payOrder(order.id, paymentInfo))
                                                                                                                                                .rejects.toThrow('Cannot pay a shipped order');
                                                                                                                                                });
                                                                                                                                                ```
                                                                                                                                                
                                                                                                                                                ## What NOT to Test
                                                                                                                                                
                                                                                                                                                - **Trivial code**: Getters, setters, constructors with no logic
                                                                                                                                                - **Framework code**: Does Express routing work? Yes. Don't test it.
                                                                                                                                                - **External services in unit tests**: Mock them, or use integration tests
                                                                                                                                                - **CSS/styling**: Unless it's critical business logic (pricing display)
                                                                                                                                                
                                                                                                                                                ## Test Naming
                                                                                                                                                
                                                                                                                                                ```javascript
                                                                                                                                                // Bad
                                                                                                                                                test('test1', ...);
                                                                                                                                                test('createUser', ...);
                                                                                                                                                
                                                                                                                                                // Good: describes the scenario and expected outcome
                                                                                                                                                test('createUser with duplicate email returns conflict error', ...);
                                                                                                                                                test('expired tokens are rejected with 401', ...);
                                                                                                                                                ```
                                                                                                                                                
                                                                                                                                                When a test fails, the name should tell you what broke without reading the code.
                                                                                                                                                
                                                                                                                                                ---
                                                                                                                                                
                                                                                                                                                What's your testing philosophy? I'm always refining my approach.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.