| name | angular-core |
| description | Angular patterns and testable architecture |
| allowed-tools | [] |
Hevery: Testable Architecture
Miško Hevery's core belief: If it's hard to test, it's hard to use. Testability is not a feature—it's a design constraint that produces better architecture.
The Foundational Principle
"The secret to writing testable code is to write code that is easy to test."
This sounds circular but isn't. Code that's easy to test has:
- Clear inputs and outputs
- No hidden dependencies
- Single responsibilities
- Explicit state management
Core Principles
1. Dependency Injection Is Architecture
DI isn't just a pattern—it's the foundation of maintainable systems.
Not this:
class UserService {
private http = new HttpClient();
private cache = new CacheService();
getUser(id: string) {
return this.http.get(`/users/${id}`);
}
}
This:
@Injectable({ providedIn: 'root' })
class UserService {
constructor(
private http: HttpClient,
private cache: CacheService
) {}
getUser(id: string) {
return this.http.get(`/users/${id}`);
}
}
Why it matters:
- Tests can provide mocks
- Dependencies are visible in constructor
- Swapping implementations is trivial
- Circular dependencies are caught at compile time
2. The Law of Demeter for DI
Only inject what you directly use. Don't inject a service to get another service.
Not this:
class OrderComponent {
constructor(private userService: UserService) {}
getDiscount() {
return this.userService.pricingService.calculateDiscount();
}
}
This:
class OrderComponent {
constructor(private pricingService: PricingService) {}
getDiscount() {
return this.pricingService.calculateDiscount();
}
}
3. Constructor Does Nothing
Constructors are for assignment only. No logic, no calls, no initialization.
Not this:
class DashboardComponent {
data: any[];
constructor(private api: ApiService) {
this.data = this.api.fetchData();
this.processData();
}
}
This:
class DashboardComponent implements OnInit {
data: any[];
constructor(private api: ApiService) {}
ngOnInit() {
this.api.fetchData().subscribe(data => {
this.data = this.processData(data);
});
}
}
Why: Constructors run during DI resolution. Side effects there are:
- Hard to test (can't construct without triggering)
- Order-dependent (which service constructs first?)
- Error-prone (what if API fails during construction?)
4. Prefer Composition Over Inheritance in Components
Angular components should compose, not inherit.
Not this:
class BaseTableComponent {
sort() { }
filter() { }
paginate() { }
}
class UserTableComponent extends BaseTableComponent {
}
This:
@Injectable()
class SortService { sort<T>(data: T[]) { } }
@Injectable()
class FilterService { filter<T>(data: T[]) { } }
class UserTableComponent {
constructor(
private sortService: SortService,
private filterService: FilterService
) {}
}
5. Smart Components vs Presentational Components
Separate concerns: Smart components manage state, presentational components render.
SMART COMPONENT PRESENTATIONAL COMPONENT
────────────── ────────────────────────
Knows about services Knows only @Input/@Output
Manages state Stateless (mostly)
Handles side effects Pure rendering
Few in app Many in app
Hard to test Easy to test
Example:
@Component({
template: `<user-card [user]="user$ | async" (delete)="onDelete($event)">`
})
class UserContainerComponent {
user$ = this.store.select(selectCurrentUser);
constructor(private store: Store) {}
onDelete(userId: string) { this.store.dispatch(deleteUser({ userId })); }
}
@Component({
template: `<div>{{user.name}}</div><button (click)="delete.emit(user.id)">Delete</button>`
})
class UserCardComponent {
@Input() user: User;
@Output() delete = new EventEmitter<string>();
}
6. Avoid Global State
Global state (window, localStorage, static variables) breaks testability.
Not this:
class ThemeService {
getTheme() {
return localStorage.getItem('theme');
}
}
This:
const STORAGE = new InjectionToken<Storage>('storage');
@Injectable()
class ThemeService {
constructor(@Inject(STORAGE) private storage: Storage) {}
getTheme() {
return this.storage.getItem('theme');
}
}
7. Signals and Fine-Grained Reactivity
With Angular's signals, prefer fine-grained reactivity over coarse change detection.
@Component({
template: `
<div>Count: {{ count() }}</div>
<div>Doubled: {{ doubled() }}</div>
`
})
class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
}
}
Benefits:
- No zone.js overhead
- Precise update tracking
- Better performance
- Easier to reason about
The Hevery Test
Before committing any Angular code, ask:
- Can I test this in isolation? Without setting up the entire app?
- Are all dependencies explicit? Visible in the constructor?
- Is the constructor boring? Just assignments, no logic?
- Can I swap implementations? For testing or different environments?
- Is this component smart or presentational? And is it correctly categorized?
- Am I reaching through services? Or injecting what I directly need?
When Reviewing Code
Apply these checks:
When NOT to Use This Skill
Use a different skill when:
- Applying design patterns → Use
design-patterns
- Writing Java code → Use
java (similar DI principles, different idioms)
- General code clarity → Use
clarity
- Performance optimization → Use
angular-perf
Hevery is the Angular architecture skill—use it for DI, testability, and component design.
Sources
- Hevery, "Misko's Guide to Writing Testable Code" (Google internal, widely shared)
- Angular documentation (authored/influenced by Hevery)
- "Angular: Up and Running" - Shyam Seshadri
- Angular team design documents and RFCs
"The key to testability is the ability to construct the object under test in isolation." — Miško Hevery