This episode covers how to maintain code quality: unit testing components with Vitest and Svelte Testing Library, integration testing for SvelteKit apps, E2E testing with Playwright, and static analysis, linting, and type checking.

Code without tests feels like a building without a foundation: it works until it breaks in the most unexpected place. Automated testing makes bold changes safe, because errors are caught in seconds instead of in production.
This episode covers unit testing components with Vitest and Svelte Testing Library, integration testing for SvelteKit apps, E2E testing with Playwright, and static analysis, linting, and type checking.
When you're done, you'll have a testing layer that catches errors at the most appropriate level: unit tests for logic, integration tests for modules that work together, and E2E for real user flows.
New SvelteKit projects already include Vitest configuration through the @sveltejs/kit/vite plugin. For older projects, install it manually:
npm install -D vitest jsdom @testing-library/sveltejsdom provides a DOM environment in Node, so components can be rendered and tested without a real browser.
Create a small component and test its behavior like a user would, not its implementation details:
<script>
let hitung = $state(0)
</script>
<button onclick={() => hitung += 1}>Hitung: {hitung}</button>The test checks the outcome of an interaction:
import { render, screen, fireEvent } from "@testing-library/svelte"
import { describe, it, expect } from "vitest"
import Counter from "./Counter.svelte"
describe("Counter", () => {
it("menambahkan nilai saat tombol diklik", async () => {
render(Counter)
const tombol = screen.getByRole("button")
await fireEvent.click(tombol)
expect(screen.getByText("Hitung: 1")).toBeTruthy()
})
})screen.getByRole("button") selects the button through its accessibility role — not a DOM selector — making the test more resilient to structural changes. fireEvent.click(tombol) simulates a user click.
Integration testing ensures modules work together: a load function reads data, a form action validates, and the response reaches the component. Server endpoints can be tested by calling their handlers directly in Vitest:
import { describe, it, expect } from "vitest"
import { GET } from "./+server.js"
describe("endpoint /api/health", () => {
it("mengembalikan status sehat", async () => {
const res = await GET()
expect(res.status).toBe(200)
const body = await res.json()
expect(body.status).toBe("ok")
})
})await GET() executes the endpoint handler without a real HTTP server. This pattern is fast and deterministic for testing server logic.
Unit and integration tests can't catch problems that only appear when the whole app runs in a browser: navigation, authentication, and cross-page interactions. Playwright runs these flows in a real browser.
npm run test:e2eSvelteKit ships with Playwright configuration by default. E2E tests run slower than unit tests, so focus on critical flows: login, checkout, and main navigation.
npx svelte-check checks types across the whole project without running a full build. Pair it with ESLint to catch code pattern issues as you type in the editor:
{
"scripts": {
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"lint": "eslint ."
}
}npm run check and npm run lint should run before every commit and inside CI. Type errors caught in the pipeline are cheaper than bugs found by users.
Key takeaways:
svelte-check and ESLint in CI to catch issues early.Next, in episode 17 you'll learn accessibility & UX — ARIA roles and keyboard navigation, semantic HTML and accessible components, responsive design and progressive enhancement, and internationalization basics. The testing from this episode will guarantee that accessibility fixes don't break existing features.