Back to Blog
·5 min read

Why I stopped writing integration tests and started testing user flows

Traditional integration tests miss the forest for the trees -- testing user flows catches real bugs that matter to your users.

AI Dev
testing
user-flows
integration-tests
quality

Why I stopped writing integration tests and started testing user flows

Integration tests felt like the responsible thing to do. Test the database connection, verify the API endpoints work together, make sure the authentication middleware talks to the user service correctly. I spent months building out comprehensive integration test suites that covered every possible service interaction. Then I'd ship features that technically passed all tests but completely broke the user experience.

The wake-up call came when our "login flow" integration tests were all green, but users could not actually log in. The tests verified that the authentication service returned tokens, the user service stored sessions, and the API responded with 200 status codes. But they missed that the frontend was not properly handling the token refresh flow, leaving users stuck on a loading screen after successful authentication.

That's when I realized I was testing the wrong things. Integration tests focus on technical contracts between services -- user flow tests focus on whether the product actually works for humans.

The Problem with Traditional Integration Tests

Integration tests examine individual pieces of functionality in isolation, even when they involve multiple services. Here's what a typical integration test looked like in my old approach:

describe('User Authentication', () => {
  it('should create a session when credentials are valid', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({ email: 'test@example.com', password: 'password123' });
    
    expect(response.status).toBe(200);
    expect(response.body.token).toBeDefined();
    expect(response.body.user.id).toBeDefined();
  });
  
  it('should return error when credentials are invalid', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({ email: 'test@example.com', password: 'wrongpassword' });
    
    expect(response.status).toBe(401);
    expect(response.body.error).toBe('Invalid credentials');
  });
});

This test verifies the API contract, but it does not tell me if users can actually log in to my application. It misses crucial details like:

  • Does the frontend properly store and use the returned token?
  • Can users navigate to protected pages after logging in?
  • What happens when the token expires during a session?
  • Does the logout flow actually clear user data?

User Flow Testing: The Better Approach

User flow tests follow the path a real person takes through your application. Instead of testing isolated API endpoints, they test complete workflows from start to finish. Here's how I test the same login functionality now:

describe('User Login Flow', () => {
  it('should allow user to log in and access protected content', async () => {
    // Start from the user's perspective
    await page.goto('/login');
    
    // Fill out the form like a real user
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');
    
    // Verify the user sees what they expect
    await expect(page.locator('[data-testid="dashboard-title"]')).toBeVisible();
    
    // Test that protected functionality works
    await page.click('[data-testid="profile-link"]');
    await expect(page.locator('[data-testid="user-profile"]')).toBeVisible();
    
    // Verify logout works completely
    await page.click('[data-testid="logout-button"]');
    await expect(page.locator('[data-testid="login-form"]')).toBeVisible();
    
    // Ensure user is actually logged out
    await page.goto('/profile');
    await expect(page.locator('[data-testid="login-form"]')).toBeVisible();
  });
});

This test catches the issues that my integration tests missed. If the frontend does not handle tokens correctly, the dashboard will not appear. If logout does not properly clear session data, the user will still have access to protected pages.

AI-Assisted Flow Generation

Writing comprehensive user flows manually is time-consuming, so I use AI to generate test scenarios based on user stories. I describe the feature to Claude and ask it to identify the critical paths users will take:

I have a feature that lets users upload documents, add tags, and share them with teammates. What are the most important user flows I should test?

The AI typically suggests flows like:

  • Happy path: Upload → tag → share → verify teammate access
  • Error handling: Upload fails → user sees clear error → can retry
  • Edge cases: Upload large file → shows progress → handles timeout gracefully
  • Permissions: User without share permission → sees appropriate error

I then convert these flows into actual tests, focusing on the scenarios that would break user workflows if they failed.

What I Learned

Making this shift taught me several important lessons:

  • User flows catch integration bugs that unit tests miss -- like when services work individually but fail together
  • Frontend-backend integration issues become obvious when you test complete workflows instead of isolated API calls
  • Error handling quality improves dramatically when you test how errors appear to users, not just that APIs return correct status codes
  • Performance regressions get caught earlier because user flows include realistic data loads and user interactions

The trade-off is that user flow tests take longer to run and require more setup. But they catch the bugs that actually matter to your users, while integration tests often give you false confidence that everything works when it really does not.

I still write some integration tests for critical service boundaries, but I spend most of my testing effort on flows that mirror what users actually do with my product. The result is fewer production bugs and more confidence that new features will work in the real world.