Storybook ile UI Komponent Test ve Dokümantasyon
Bir frontend projesinde bileşen sayısı elliye ulaştığında işler karışmaya başlar. Yüzü geçtiğinde ise artık kimse hangi button varyantının nerede kullanıldığını, o modal’ın hover state’inde ne olduğunu ya da loading spinner’ın dark modda nasıl göründüğünü tam olarak bilmez. İşte tam bu noktada Storybook devreye girer ve hayatı kurtarır.
Storybook, UI bileşenlerini izole ortamda geliştirmenizi, test etmenizi ve dokümante etmenizi sağlayan açık kaynaklı bir araç. Ben bunu ilk kez büyük bir e-ticaret projesinde kullandım ve o günden beri her ciddiye aldığım frontend projesinde standart altyapının parçası haline getirdim. Bu yazıda sıfırdan kurulumdan ileri düzey test senaryolarına kadar Storybook’u gerçek anlamda nasıl kullanabileceğinizi aktaracağım.
Neden Storybook? Alternatifler Ne Olacak?
Piyasada Styleguidist, Docz, Bit gibi alternatifler var. Hepsini bir dönem kullandım. Storybook’u öne çıkaran birkaç somut neden var:
- Ekosistem büyüklüğü: Addons mağazası inanılmaz zengin. Accessibility testi, viewport simülasyonu, görsel regresyon, hepsi birer addon ile geliyor.
- Framework agnostik: React, Vue, Angular, Svelte, hatta vanilla HTML ile çalışıyor. Polimorfik bir ekibiniz varsa tek araç yeterli.
- Gerçek izolasyon: Bileşen uygulama context’inden tamamen bağımsız render ediliyor. Bu, gizli bağımlılıkları yüzeye çıkarmak için mükemmel.
- CI entegrasyonu: Chromatic gibi araçlarla birleşince görsel regresyon testi production-grade seviyeye çıkıyor.
Şimdi işe koyulalım.
Kurulum ve Temel Yapılandırma
Mevcut bir React + TypeScript projenize Storybook eklemek için:
npx storybook@latest init
Bu komut projenizi analiz eder, hangi framework kullandığınızı algılar ve gerekli bağımlılıkları kurar. Sonunda .storybook/ klasörü oluşur ve package.json‘a script’ler eklenir. Manuel bir kurulum yapmak zorunda değilsiniz, ama ne kurulduğunu bilmek önemli.
Kurulum sonrası .storybook/main.ts dosyasına bakın:
cat .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-onboarding",
"@storybook/addon-links",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
};
export default config;
Vite tabanlı projelerde @storybook/react-vite kullanılıyor. Webpack tabanlı projelerde @storybook/react-webpack5 gelir. Build sisteminize göre otomatik seçiliyor ama farklı bir şey istiyorsanız burada değiştirebilirsiniz.
İlk Story’yi Yazmak
Story yazmak, bileşeni farklı state’lerde belgeleyen bir yapı oluşturmak demek. Basit bir Button bileşeni üzerinden gidelim:
// src/components/Button/Button.tsx
interface ButtonProps {
variant: "primary" | "secondary" | "danger";
size: "sm" | "md" | "lg";
loading?: boolean;
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export const Button = ({
variant,
size,
loading = false,
disabled = false,
children,
onClick,
}: ButtonProps) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? <span className="spinner" /> : children}
</button>
);
};
Bu bileşen için story dosyası:
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
title: "UI/Button",
component: Button,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "danger"],
description: "Butonun görsel varyantı",
},
size: {
control: "radio",
options: ["sm", "md", "lg"],
},
loading: {
control: "boolean",
},
},
args: {
onClick: fn(),
children: "Tıkla",
size: "md",
variant: "primary",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: "primary",
},
};
export const Danger: Story = {
args: {
variant: "danger",
children: "Sil",
},
};
export const Loading: Story = {
args: {
loading: true,
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: "Pasif Buton",
},
};
tags: ["autodocs"] satırı sayesinde Storybook argTypes tanımlarınızdan otomatik olarak dokümantasyon sayfası üretiyor. Ekstra MDX dosyası yazmak zorunda kalmıyorsunuz.
Dekoratörlerle Global Sağlayıcıları Yönetmek
Gerçek projelerde bileşenler nadiren sade halde yaşar. Theme provider, i18n context, Redux store, React Query client… Bunların hepsinin story ortamında da var olması gerekiyor. Bunu dekoratörlerle çözüyoruz.
.storybook/preview.tsx dosyasını düzenleyin:
// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "../src/providers/ThemeProvider";
import { I18nextProvider } from "react-i18next";
import i18n from "../src/i18n/config";
import "../src/styles/globals.css";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false, // Test ortamında retry istemiyoruz
staleTime: 0,
},
},
});
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
viewport: {
defaultViewport: "tablet",
},
},
decorators: [
(Story) => (
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18n}>
<ThemeProvider>
<Story />
</ThemeProvider>
</I18nextProvider>
</QueryClientProvider>
),
],
};
export default preview;
Bu yapıyla her story otomatik olarak gerekli sağlayıcıların içine alınıyor. Tek tek story’lerde override etmek istediğinizde parameters veya decorators array’i kullanabilirsiniz.
MSW ile API Mock’lama
Storybook’un en güçlü özelliklerinden biri, gerçek API çağrıları yerine Mock Service Worker kullanarak network katmanını kontrol edebilmek. Özellikle loading, error ve empty state senaryolarını test etmek için vazgeçilmez.
npm install msw msw-storybook-addon --save-dev
npx msw init public/
Ardından .storybook/preview.tsx‘e ekleyin:
import { initialize, mswLoader } from "msw-storybook-addon";
initialize({
onUnhandledRequest: "bypass",
});
const preview: Preview = {
loaders: [mswLoader],
// ... diğer ayarlar
};
Şimdi bir UserProfile bileşeni için farklı API senaryolarını story ile modelleyelim:
// src/components/UserProfile/UserProfile.stories.tsx
import { http, HttpResponse, delay } from "msw";
import { UserProfile } from "./UserProfile";
export const SuccessState: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/user/:id", async () => {
await delay(800);
return HttpResponse.json({
id: "1",
name: "Ahmet Yılmaz",
email: "[email protected]",
role: "admin",
});
}),
],
},
},
};
export const LoadingState: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/user/:id", async () => {
await delay("infinite"); // Sonsuz yükleme simülasyonu
return HttpResponse.json({});
}),
],
},
},
};
export const ErrorState: Story = {
parameters: {
msw: {
handlers: [
http.get("/api/user/:id", () => {
return new HttpResponse(null, { status: 500 });
}),
],
},
},
};
Bu yaklaşım özellikle UX tartışmalarında çok işe yarıyor. Tasarımcı ve PM’e “loading state’de ne göreceğiz?” sorusunun cevabını canlı olarak gösterebiliyorsunuz.
Interaction Testing: Storybook Sadece Dokümantasyon Değil
Storybook 7 ile birlikte gelen @storybook/test paketi, @testing-library/react ve jest benzeri API’yi doğrudan story içinde kullanmanıza olanak tanıyor. Bu sayede bileşenin kullanıcı etkileşimlerine nasıl tepki verdiğini de test edebiliyorsunuz.
// src/components/LoginForm/LoginForm.stories.tsx
import { within, userEvent, expect } from "@storybook/test";
export const SuccessfulLogin: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Form elemanlarını bul
const emailInput = canvas.getByLabelText("E-posta");
const passwordInput = canvas.getByLabelText("Şifre");
const submitButton = canvas.getByRole("button", { name: "Giriş Yap" });
// Kullanıcı eylemlerini simüle et
await userEvent.type(emailInput, "[email protected]", { delay: 100 });
await userEvent.type(passwordInput, "securepassword", { delay: 100 });
await userEvent.click(submitButton);
// Beklenen sonucu doğrula
await expect(
canvas.findByText("Giriş başarılı!")
).resolves.toBeInTheDocument();
},
};
export const ValidationError: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const submitButton = canvas.getByRole("button", { name: "Giriş Yap" });
// Boş formla submit deneyin
await userEvent.click(submitButton);
await expect(
canvas.getByText("E-posta adresi zorunludur")
).toBeInTheDocument();
},
};
play fonksiyonu Storybook arayüzünde otomatik çalışıyor ve Interactions sekmesinde adım adım görebiliyorsunuz. storybook test komutuyla ise CI’da headless olarak tüm bu testleri koşabiliyorsunuz.
Accessibility Testlerini Otomatikleştirmek
@storybook/addon-a11y her story için otomatik erişilebilirlik analizi yapıyor ve ihlalleri Accessibility sekmesinde gösteriyor. Kurulum:
npm install @storybook/addon-a11y --save-dev
.storybook/main.ts addons dizisine ekleyin:
addons: [
// ... diğer addons
"@storybook/addon-a11y",
],
Belirli story’lerde a11y kurallarını yapılandırabilirsiniz:
export const WithCustomA11yRules: Story = {
parameters: {
a11y: {
config: {
rules: [
{
id: "color-contrast",
enabled: false, // Bu story için renk kontrastını atla
},
],
},
},
},
};
Gerçek dünyada bunu en çok icon-only butonlarda kullandım. aria-label eksikliği anında yakalanıyor ve geliştirici aşamasında fix ediliyor.
CI Pipeline’a Entegrasyon
Storybook’u sadece yerel geliştirme aracı olarak kullanmak potansiyelinin yarısını kullanmak demek. Chromatic ile entegre ettiğinizde görsel regresyon testi de devreye giriyor.
GitHub Actions ile basit bir yapı:
# .github/workflows/storybook.yml
name: Storybook Tests
on:
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Node.js kur
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Bağımlılıkları yükle
run: npm ci
- name: Storybook build
run: npm run build-storybook
- name: Storybook testlerini çalıştır
run: npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue"
"npx http-server storybook-static --port 6006 --silent"
"npx wait-on tcp:6006 && npm run test-storybook"
- name: Chromatic'e gönder
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
exitZeroOnChanges: true ile görsel değişiklikler build’i kırmıyor ama Chromatic dashboard’unda review bekliyor. PR onaylanmadan merge edilemiyor. Bu workflow’u kurduktan sonra “ben bu bileşeni değiştirmedim neden etkilendi?” türünden tartışmalar neredeyse sıfıra indi.
Storybook’u Gerçek Projelerde Ölçeklendirmek
Bileşen sayısı arttığında organizasyon kritik hale geliyor. title alanını hiyerarşik kullanın:
"Foundations/Colors"temel tasarım tokenları için"UI/Forms/Input"form bileşenleri için"Features/Checkout/OrderSummary"feature-specific bileşenler için"Pages/Dashboard"sayfa seviyesi bileşenler için
Global story parametrelerini .storybook/preview.tsx‘de merkezileştirin ama her story’nin kendi override mekanizması olduğunu unutmayın. Bir yaklaşım olarak parameters.docs.description.component ile bileşen açıklaması, parameters.docs.description.story ile story açıklaması yazabilirsiniz.
Büyük ekiplerde story review sürecini tanımlayın. Storybook dosyaları da kod review’dan geçmeli. “Bu state’i neden eklemedik?” sorusu PR aşamasında sorulmalı, production’dan sonra değil.
Monorepo yapılarında ise her paket kendi stories dizinine sahip olabilir, main.ts içindeki stories glob pattern’ini buna göre düzenleyebilirsiniz.
Sonuç
Storybook bir dokümantasyon aracı olarak başladı ama bugün çok daha fazlası. İzolasyonlu geliştirme ortamı, interaction test altyapısı, accessibility checker, görsel regresyon testi ve ekip içi iletişim aracı olarak tek çatı altında topluyor bunların hepsini.
Projenize değer katıp katmayacağını test etmenin en kolay yolu şu: Tasarımcınıza “bu bileşenin empty state’i var mıydı?” diye sorun. Eğer cevap “dur bir bakayım” ise Storybook’a ihtiyacınız var demektir. Story yazmak sizi o soruyu önceden sormaya, dolayısıyla önceden cevaplamaya zorluyor.
Kurulum maliyeti düşük, geri dönüşü yüksek araçlar arasında kalan nadir örneklerden biri. Bir sonraki sprint’inizde denemeye değer.
