Published on January 8, 2025 • 10 min read
Let me start with a confession: I used to be a 100% code coverage zealot. I would spend hours writing tests for getter and setter methods, testing framework code, and generally making my test suites twice as long as the actual application code.
Then I started working with real clients on real deadlines, and I learned something important: testing strategy should be about covering the most important code, not checking boxes.
Here's what nobody tells you about chasing 100% test coverage: the last 10% takes 50% of your testing effort. You end up writing tests for:
Meanwhile, the complex business logic that actually breaks things gets rushed testing because you're burning time on the trivial stuff.
I learned this the hard way on a financial application where we had 98% test coverage but still shipped a bug in the interest calculation logic because I spent more time testing the date formatting utilities than the core business rules.
After years of trial and error, here's where I put my testing effort:
This is where bugs hurt the most. If your e-commerce platform calculates shipping costs wrong or your inventory system double-allocates products, you lose money and customers.
I write comprehensive unit tests for:
Most production bugs happen at the boundaries between systems. Your code works fine, the API works fine, but somehow they don't work fine together.
Key areas I always test:
This is where E2E testing shines. I focus on the critical paths that users actually take through the application.
I'm a big believer in E2E tests that mimic real user behavior. Too many developers write E2E tests that are really just integration tests in disguise.
Here's how I structure E2E tests using the Page Object Model:
// LoginPage.ts
export class LoginPage {
async enterCredentials(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
}
async submitLogin() {
await this.loginButton.click();
await this.page.waitForURL('/dashboard');
}
async expectLoginError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
// LoginTest.spec.ts
test('User can log in with valid credentials', async () => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.enterCredentials('user@example.com', 'password123');
await loginPage.submitLogin();
await dashboardPage.expectWelcomeMessage('Welcome back!');
});
This approach makes tests readable and maintainable. When the UI changes, you update the page object, not fifty different test files.
Instead of testing "the login function works," I test "a user can access their dashboard after logging in." The difference is subtle but important - you're testing the value delivered to users, not just technical functionality.
This might be controversial, but here's what I deliberately don't spend time testing:
If you're using Angular, React, or .NET, don't test that their built-in functionality works. Test your code that uses the framework, not the framework itself.
// Don't test this
public string Name { get; set; }
// Do test this
public string DisplayName => $"{FirstName} {LastName}".Trim();
Assume that well-maintained libraries work as documented. Test your integration with them, not their internal behavior.
Don't write tests for things like "what happens if we pass null to a method that requires a string." Use proper typing and validation instead.
With .NET (my preferred backend technology), I lean heavily on:
The .NET ecosystem makes it easy to write fast, reliable tests without a lot of ceremony.
For Angular applications, I focus on:
Angular's dependency injection makes testing much easier than other frontend frameworks, so I can achieve good coverage without excessive mocking.
The key to maintaining good testing practices is automation. My testing pipeline:
If tests take too long, developers stop running them. If they're flaky, developers stop trusting them.
Let me show you how this looks in practice. On a recent e-commerce project:
90% coverage focused on:
What we didn't test:
Result: We caught 95% of bugs before production, shipped on time, and the client hasn't had a single payment or inventory issue in 6 months.
Here's why this approach works better for clients:
By not chasing 100% coverage, we deliver features faster and spend more time on functionality that matters to users.
Testing effort focused on business logic catches the bugs that actually cost money.
With fewer, more focused tests, the test suite stays maintainable as the application grows.
Different projects need different approaches:
Financial/Healthcare Applications: Bump to 95% coverage and include more edge case testing
Internal Tools: 80% coverage might be sufficient
Public-Facing Applications: Heavy emphasis on E2E testing for user journeys
APIs: Focus on contract testing and integration tests
If you want to implement this approach:
Testing is about risk management, not box checking. A focused 90% coverage that tests the right things will catch more bugs and save more money than an unfocused 100% coverage that tests everything equally.
Your goal should be confidence in your deployments, not perfect metrics. And remember - the best test is often the simplest one that catches real problems.
Want to discuss testing strategies for your project? Let's talk - I love helping teams build testing approaches that actually work.