This episode covers testing and code quality: unit testing components and services, testing pipes, directives, and guards, integration testing with TestBed, and end-to-end testing with Cypress or Playwright.

Untested code is code that can break without anyone noticing. Testing isn't an administrative obligation — it's a safety net that gives teams the confidence to change code.
Episode 16 covers unit testing components and services, testing pipes, directives, and guards, integration testing with TestBed, and end-to-end testing with Cypress or Playwright. You'll build the habit of writing tests alongside production code.
A default Angular project uses Jasmine and Karma:
ng testng test runs all spec files and reports the results in the browser. Test files sit next to their code: produk.component.spec.ts for produk.component.ts.
Services that don't depend on the browser are the easiest to test:
import { TestBed } from '@angular/core/testing';
import { KeranjangService } from './keranjang.service';
import { Produk } from './produk.model';
describe('KeranjangService', () => {
let service: KeranjangService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [KeranjangService] });
service = TestBed.inject(KeranjangService);
});
it('menghitung total harga dengan benar', () => {
service.tambah({ id: 1, nama: 'Kopi', harga: 25000 } as Produk);
service.tambah({ id: 2, nama: 'Teh', harga: 15000 } as Produk);
expect(service.totalHarga()).toBe(40000);
expect(service.totalItem()).toBe(2);
});
});TestBed creates an isolated test environment. The test above verifies KeranjangService behavior without touching the UI — fast, reliable, and independent of the browser.
A pipe is tested by calling its transform method directly. A directive is tested with a template that uses it:
import { RupiahPipe } from './rupiah.pipe';
describe('RupiahPipe', () => {
const pipe = new RupiahPipe();
it('memformat angka menjadi Rupiah', () => {
expect(pipe.transform(25000)).toContain('25.000');
});
});Testing a pipe is just instantiating the class and calling transform — no TestBed needed. Directives and guards need a little setup: create a host component for the directive, and call the guard function with stubbed dependencies to check the access decision.
A guard is an ordinary function, so it's easy to test with a fake AuthService:
import { TestBed } from '@angular/core/testing';
import { authGuard } from './auth.guard';
describe('authGuard', () => {
it('mengizinkan akses saat pengguna login', () => {
const hasil = TestBed.runInInjectionContext(() => authGuard());
expect(hasil).toBe(true);
});
});runInInjectionContext runs the guard with the TestBed injector so inject inside it works. For the not-logged-in scenario, provide an AuthService stub that returns a different state.
Integration tests connect several real parts — for example a component with a real service:
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { PostService } from './post.service';
describe('PostService integration', () => {
let service: PostService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
PostService,
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(PostService);
httpMock = TestBed.inject(HttpTestingController);
});
it('memanggil endpoint yang benar', () => {
service.listar().subscribe();
const req = httpMock.expectOne('/api/posts');
expect(req.request.method).toBe('GET');
req.flush([]);
});
});HttpTestingController captures outgoing requests without actually sending them to the network — tests run fast and deterministically. req.flush([]) simulates the server response.
Unit and integration tests verify parts; end-to-end (E2E) tests verify entire user flows in a real browser — from login, navigation, to form submission.
npm install -D cypress
npx cypress openCypress and Playwright are equally powerful. Protractor (the predecessor) is deprecated, so for new projects choose one of the two.
describe('Alur login', () => {
it('berhasil masuk dan menuju dashboard', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('admin@example.com');
cy.get('[data-cy=password]').type('rahasia123');
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
});
});[data-cy=...] selectors are stable, test-specific attributes — better than CSS selectors that often change. E2E tests running in CI give you full confidence that the application's main flows work from the user's perspective.
Key takeaways:
ng test runs Jasmine tests with Karma; spec files sit alongside the code.TestBed.inject; components with a fixture and detectChanges.transform; guards with runInInjectionContext.HttpTestingController simulates responses without a real network.data-cy selectors make E2E tests stable against UI changes.In the next episode, episode 17, we'll cover accessibility and UX — ARIA attributes and keyboard navigation, semantic markup and accessible forms, responsive layout and mobile support, and internationalization and localization.