Suggest an editImprove this articleRefine the answer for “Testing in Angular (unit and integration tests)”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Testing in Angular** uses Jasmine specs and `TestBed` to verify components and services in isolation (unit tests) and together (integration tests). ```typescript // Mock the service, test the component behavior it('should emit event on button click', () => { spyOn(component.delete, 'emit'); fixture.nativeElement.querySelector('.delete-btn').click(); expect(component.delete.emit).toHaveBeenCalledWith(component.user.id); }); ``` **Key point:** unit tests mock all dependencies and pinpoint logic failures; integration tests use `HttpClientTestingModule` with real services to catch bugs at the boundaries between pieces. Always call `fixture.detectChanges()` after changing state, and use `fakeAsync` + `tick` for timing control.Shown above the full answer for quick recall.Answer (EN)Image**Testing in Angular** means verifying that components, services, and their interactions work correctly, in isolation (unit tests) and together (integration tests), using Jasmine specs and TestBed's dependency injection system. ## Theory ### TL;DR - Unit tests verify a single class or function with all dependencies mocked - Integration tests let real dependencies run together and catch bugs at the boundaries - `TestBed` is Angular's test harness: it replaces the real injector and controls what gets injected - Unit tests are fast and point to exact failures; integration tests are slower but catch contract bugs - Default to unit tests for logic; add integration tests where pieces interact (forms, HTTP, routing) ### Quick example ```typescript // Unit test - AuthService is mocked, we verify the call it('should call login with credentials', () => { const authService = jasmine.createSpyObj('AuthService', ['login']); authService.login.and.returnValue(of({ token: 'abc123' })); component.form.patchValue({ email: 'user@test.com', password: 'pass' }); component.onSubmit(); expect(authService.login).toHaveBeenCalledWith({ email: 'user@test.com', password: 'pass' }); }); // Integration test - real UserService runs, HTTP intercepted it('should load and display users on init', fakeAsync(() => { fixture.detectChanges(); // Triggers ngOnInit const req = httpMock.expectOne('/api/users'); req.flush([{ id: 1, name: 'Alice' }]); tick(); fixture.detectChanges(); const items = fixture.nativeElement.querySelectorAll('.user-item'); expect(items.length).toBe(1); expect(items[0].textContent).toContain('Alice'); })); ``` In the unit test, the service is a spy object and we assert on the call. In the integration test, the real service runs and we intercept the HTTP request. ### Unit vs integration: the core difference Unit tests isolate one piece and mock everything else. They run in milliseconds and point directly to the broken line when something fails. Integration tests let real services interact with real components, catching bugs that only appear at the boundary, like when a component doesn't handle the actual response format the service returns. You need both. Unit tests catch bugs in logic. Integration tests catch bugs in contracts between pieces. ### When to use each Unit test: - Service method logic or calculations - Component input/output behavior - Pipe transformation or guard decision - Any code with no external dependencies Integration test: - Form submission through a service to an API - Component loading and displaying data from a service - Routing with guards - HTTP interceptor behavior E2E tests (Cypress, Playwright) cover full browser workflows with a real backend. That is a separate layer entirely. ### Comparison table | Aspect | Unit test | Integration test | |--------|-----------|------------------| | Scope | Single class or function | Multiple components and services | | Dependencies | Mocked or stubbed | Real or semi-real | | Speed | Milliseconds | Hundreds of milliseconds | | Failure clarity | Points to exact unit | Points to interaction issue | | Setup | Simple | Requires TestBed configuration | | Best for | Business logic, calculations | Data flow, component lifecycle | ### How TestBed works internally `TestBed.configureTestingModule()` sets up a test injector that mirrors Angular's real DI system. You specify which providers, imports, and components to use. When you call `TestBed.createComponent()`, Angular instantiates the component through that injector and returns a `ComponentFixture`, a wrapper with access to the component instance, the DOM, and manual change detection control. The fixture does not auto-detect changes. You call `fixture.detectChanges()` yourself. This gives you control: set state, assert, update state, assert again. For HTTP, `HttpClientTestingModule` replaces the real `HttpClient` with a mock. You inject `HttpTestingController`, intercept requests with `expectOne()`, and respond with `flush()`. No real network call happens. ### Common mistakes **Forgetting `fixture.detectChanges()` after state changes** ```typescript // Wrong - DOM not updated component.isLoading = false; expect(fixture.nativeElement.textContent).toContain('Done'); // fails // Right component.isLoading = false; fixture.detectChanges(); // Re-renders the template expect(fixture.nativeElement.textContent).toContain('Done'); // passes ``` Angular does not re-render the template automatically in tests. You trigger it manually. **Testing implementation details instead of behavior** ```typescript // Wrong - breaks if you rename the method it('should call getUserData', () => { spyOn(component, 'getUserData'); component.ngOnInit(); expect(component.getUserData).toHaveBeenCalled(); }); // Right - tests what the user actually sees it('should display user name after init', () => { fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Alice'); }); ``` The first test breaks on refactoring even when the feature works. Test outcomes, not method names. **Using `async()` instead of `fakeAsync()` for timing control** ```typescript // Wrong - real timers, flaky in CI it('should debounce search', async () => { component.search('test'); await new Promise(r => setTimeout(r, 300)); expect(component.results).toBeDefined(); }); // Right - time is controlled it('should debounce search', fakeAsync(() => { component.search('test'); tick(300); expect(component.results).toBeDefined(); })); ``` `fakeAsync()` + `tick()` gives you full control over timers. Real timers in tests lead to intermittent failures in CI. **Mocking too much in integration tests** ```typescript // Wrong - testing mocks, not integration providers: [ { provide: UserService, useValue: mockUserService }, { provide: HttpClient, useValue: mockHttp } ] // Right - mock only external systems, let real services run imports: [UserListComponent, HttpClientTestingModule], providers: [UserService] // Real service, HTTP intercepted ``` If you mock everything in an integration test, you get no information about how the pieces actually work together. **Not calling `httpMock.verify()` in `afterEach`** Without `httpMock.verify()`, unexpected HTTP requests pass silently. Add it to `afterEach` in every suite that makes HTTP calls. ### Real-world usage - NestJS uses `TestingModule`, the same pattern as Angular's `TestBed`, so the concept transfers directly - React (Jest + Testing Library) uses `render()` + `screen` queries, same idea as `fixture.nativeElement` - Vue (Vitest) uses `mount()` + `flushPromises()`, the equivalent of `fakeAsync` + `tick` - Redux: unit test reducers as pure functions, integration test with the store and middleware together Teams that skip integration tests often find issues only in staging, when two well-unit-tested services produce a correctly typed response that the component's template just never renders. Five lines with `HttpTestingController` would have caught it in local development. ### Follow-up questions **Q:** What is the difference between `TestBed.inject()` and getting a service from the component's constructor? **A:** Both return the same singleton from the test injector. `TestBed.inject()` gives you direct access in the test to set up spies or verify calls without going through the component instance. **Q:** When should I use `fakeAsync()` vs `async()` vs returning a Promise? **A:** Use `fakeAsync()` when you need to control time with `tick()` for debounce, throttle, or setTimeout. Use `async()` when you just need to wait for Promises without controlling time. `fakeAsync()` is the most stable option for CI environments. **Q:** How do you test a component with `ChangeDetectionStrategy.OnPush`? **A:** The same way, but you must call `fixture.detectChanges()` after every state change. OnPush components only re-render when inputs change or events fire. Angular will not pick up direct property mutations, even in tests. **Q:** What is the difference between `TestBed.createComponent()` and calling `new ComponentClass()`? **A:** `TestBed.createComponent()` uses the configured injector, so dependencies are injected and lifecycle hooks fire. `new` bypasses the injector entirely: no injection, no lifecycle hooks, no Angular features. **Q (senior-level):** Your test for a component that loads data in `ngOnInit` passes, but the data never shows up in the DOM. What do you check? **A:** First, verify that `fixture.detectChanges()` is called after the async operation completes, not just at init. Then check whether `fakeAsync` + `tick` is needed because the service returns an Observable with a delay. If the component uses `OnPush`, state set directly on the component instance won't trigger re-render without explicit detection. Finally, add a spy to confirm the service actually returns data: `spyOn(service, 'getData').and.returnValue(of(mockData))`. If the spy fires but the DOM stays empty, the problem is change detection. If the spy shows no call at all, the problem is in `ngOnInit` itself. ## Examples ### Unit test: LoginComponent with mocked AuthService ```typescript describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture<LoginComponent>; let authService: jasmine.SpyObj<AuthService>; beforeEach(async () => { authService = jasmine.createSpyObj('AuthService', ['login']); authService.login.and.returnValue(of({ token: 'abc123' })); await TestBed.configureTestingModule({ imports: [LoginComponent, ReactiveFormsModule], providers: [{ provide: AuthService, useValue: authService }] }).compileComponents(); fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should call authService.login with form values', () => { component.form.patchValue({ email: 'user@test.com', password: 'pass' }); component.onSubmit(); expect(authService.login).toHaveBeenCalledWith({ email: 'user@test.com', password: 'pass' }); }); it('should display error on login failure', () => { authService.login.and.returnValue( throwError(() => new Error('Invalid credentials')) ); component.form.patchValue({ email: 'user@test.com', password: 'wrong' }); component.onSubmit(); fixture.detectChanges(); const errorEl = fixture.nativeElement.querySelector('.error'); expect(errorEl.textContent).toContain('Invalid credentials'); }); }); ``` `jasmine.createSpyObj` creates a mock with tracked methods. The second test overrides the spy's return value to simulate a failure, then checks that the template reacts. Both tests are isolated: `AuthService` never touches the network. ### Integration test: UserListComponent with real service and intercepted HTTP ```typescript describe('UserListComponent integration', () => { let fixture: ComponentFixture<UserListComponent>; let httpMock: HttpTestingController; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserListComponent, HttpClientTestingModule], providers: [UserService] // Real service, not a mock }).compileComponents(); fixture = TestBed.createComponent(UserListComponent); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { httpMock.verify(); // Fail if any request was not handled }); it('should load and display users', fakeAsync(() => { fixture.detectChanges(); // Triggers ngOnInit -> UserService.getUsers() const req = httpMock.expectOne('/api/users'); expect(req.request.method).toBe('GET'); req.flush([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]); tick(); fixture.detectChanges(); const items = fixture.nativeElement.querySelectorAll('.user-item'); expect(items.length).toBe(2); expect(items[0].textContent).toContain('Alice'); })); it('should show error message on network failure', fakeAsync(() => { fixture.detectChanges(); const req = httpMock.expectOne('/api/users'); req.error(new ErrorEvent('Network error')); tick(); fixture.detectChanges(); const errorMsg = fixture.nativeElement.querySelector('.error-message'); expect(errorMsg.textContent).toContain('Failed to load users'); })); }); ``` `UserService` is the real class but `HttpClient` is intercepted. The test verifies the full path: `ngOnInit` calls the service, the service makes an HTTP request, the component renders the response. The network failure test confirms the error handling path works too.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.