Your Tests Are Slow and Brittle: You're Testing the Wrong Thing
Are your tests slow and brittle? It might be your application's architecture. Learn how to create a testable architecture that fosters efficient testing.

Why Are Your Tests Slow and Brittle?
At every technical meeting, the mantra "We should write more tests" resonates. Everyone agrees, recognizing the importance of testing for quality assurance. Yet, when developers get back to their desks, they often feel overwhelmed. Why? Because testing is frequently seen as a chore.
Tests can be excruciatingly slow, with full suites taking long enough for several coffee breaks. Often, test code becomes more complex than the business logic it's meant to verify. The worst part? These tests are incredibly brittle. A minor change, like altering a CSS class or adding a field to a JSON response, can trigger a cascade of test failures. If this scenario sounds familiar, don't lose hope. The problem might not be your tests but your application architecture, which complicates the testing process.
What's the Root Cause? Application Architecture
Many developers mistakenly equate testing with simulating user behavior. This leads to a focus on end-to-end or integration tests, which are slow and brittle. Here's what usually happens:
- End-to-End Tests: Initiating a web server, sending a real HTTP request, and verifying the response. While useful, overreliance on these tests is a mistake.
- Performance Issues: Each test is slow, taking hundreds of milliseconds. With hundreds or thousands of tests, the total time can stretch to several minutes, severely impacting your development feedback loop.
- Brittleness: These tests are tightly coupled to external details, so minor changes can lead to numerous failures.
- Edge Case Coverage: Effectively covering edge cases, like simulating a database failure during user registration, is nearly impossible with HTTP requests.
Your testing strategy should resemble a pyramid:
- Unit Tests at the base: Numerous, fast, and reliable.
- Integration Tests in the middle: Ensuring interconnectedness, but fewer in number.
- End-to-End Tests at the top: Minimal, focusing on overall system verification.
How Does Layered Architecture Empower Testing?
To achieve this testing pyramid, you need a layered architecture. A well-designed application separates core business logic from external systems like web frameworks and databases. The Hyperlane blueprint exemplifies this, guiding you to place complex logic in service and domain layers, using pure Rust code. This setup facilitates fast and effective unit testing.
Consider a UserService responsible for user registration:
pub struct UserService {
pub user_repo: Arc<dyn UserRepository>,
}
impl UserService {
pub fn register_user(&self, username: &str) -> Result<User, Error> {
if self.user_repo.find_by_username(username).is_some() {
return Err(Error::UsernameExists);
}
let user = User::new(username);
self.user_repo.save(&user)?;
Ok(user)
}
}
How to Test with Mock Objects
Testing the register_user function becomes straightforward. You don't need to start a server or connect to a database; you only test the logic itself. By using mock objects, you can simulate dependencies' behavior. Here's an example in Rust using the mockall crate:
#[cfg(test)]
mod tests {
use super::*;
use mockall::*;
#[automock]
trait UserRepository {
fn find_by_username(&self, username: &str) -> Option<User>;
fn save(&self, user: &User) -> Result<(), DbError>;
}
#[test]
fn test_register_user_fails_if_username_exists() {
let mut mock_repo = MockUserRepository::new();
mock_repo.expect_find_by_username()
.with(predicate::eq("testuser"))
.returning(|| Some(User::new("testuser")));
let user_service = UserService { user_repo: Arc::new(mock_repo) };
let result = user_service.register_user("testuser");
assert!(matches!(result, Err(Error::UsernameExists)));
}
}
Benefits of This Approach
- Speed: These tests run in memory, executing in milliseconds. You can run thousands in seconds.
- Precision: The focus is solely on business logic, unaffected by external factors.
- Control: Simulating edge cases, like database failures, is easy, enhancing robustness.
Why Not Forget Integration Tests?
While unit tests are crucial, integration tests are still needed. They confirm that your controller layer correctly connects to the service layer and that JSON serialization/deserialization works as expected. Since your service layer logic is already verified by unit tests, your integration tests can be simple, focusing on the "happy path."
Conclusion: Time to Rethink Testing Strategies
The key takeaway is that your application's architecture greatly influences testability. A disorganized architecture complicates testing. Emphasizing a clean, layered architecture enables quick, independent unit testing of business logic.
When choosing a framework, consider not just its ease of use but also its support for easy testing. A framework that facilitates testability will help you build high-quality applications, making testing a reliable feedback tool rather than a burdensome chore. Embrace this change, and you might find yourself loving testing once again.
Related Articles

The Adventures of Blink S4e9: The Gilded Rose DevEx Patch
Join us in Blink S4e9: The Gilded Rose DevEx Patch as we explore enhancing Developer Experience in legacy codebases through refactoring and unit tests.
Oct 30, 2025

Pure CSS Tabs With Details, Grid, and Subgrid
Discover how to build pure CSS tabs using the <details> element, CSS Grid, and Subgrid for an accessible, elegant design.
Oct 30, 2025

You Might Not Need WebSockets: The Power of Server-Sent Events
Explore the simplicity and efficiency of Server-Sent Events (SSE) as an alternative to WebSockets for real-time web applications.
Oct 26, 2025
