Technology

Directus CMS Testing Strategy QA

directus cms testing strategy qa
Directus CMS Testing Strategy QA | SiamCafe Blog
2026-01-14· อ. บอม — SiamCafe.net· 1,590 คำ

ทำไมต้องทำ Testing Strategy สำหรับ Directus CMS

Directus เป็น headless CMS แบบ open source ที่สร้าง REST และ GraphQL API จาก database โดยตรง เมื่อใช้ Directus เป็น backend ให้กับ production app ต้องมี testing strategy ที่ครอบคลุมทั้ง API layer, custom extensions, data validation และ integration กับ frontend เพราะ Directus ให้อิสระในการออกแบบ schema มาก ถ้าไม่มี test รองรับจะเจอปัญหา regression ทุกครั้งที่ upgrade version หรือเปลี่ยน schema

ติดตั้ง Directus สำหรับ Testing Environment

# docker-compose.yml สำหรับ test environment
version: '3.8'
services:
  directus:
    image: directus/directus:10.10.0
    ports:
      - "8055:8055"
    environment:
      KEY: "test-key-not-for-production"
      SECRET: "test-secret-not-for-production"
      ADMIN_EMAIL: "admin@test.local"
      ADMIN_PASSWORD: "TestPassword123!"
      DB_CLIENT: "pg"
      DB_HOST: "postgres"
      DB_PORT: "5432"
      DB_DATABASE: "directus_test"
      DB_USER: "directus"
      DB_PASSWORD: "directus_test_pass"
      CACHE_ENABLED: "false"
      RATE_LIMITER_ENABLED: "false"
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: directus_test
      POSTGRES_USER: directus
      POSTGRES_PASSWORD: directus_test_pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U directus"]
      interval: 5s
      timeout: 3s
      retries: 5
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
# รัน test environment
docker compose up -d

# รอจน Directus พร้อม
until curl -s http://localhost:8055/server/health | grep -q '"status":"ok"'; do
  echo "Waiting for Directus..."
  sleep 2
done
echo "Directus is ready"

API Testing ด้วย Jest และ Axios

ทดสอบ Directus API endpoints ทั้ง CRUD operations, filtering, permissions และ custom endpoints

# ติดตั้ง dependencies สำหรับ test
npm init -y
npm install --save-dev jest @types/jest ts-jest axios dotenv
npm install --save-dev @directus/sdk
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testTimeout: 30000,
  setupFilesAfterSetup: ['./tests/setup.ts'],
  testMatch: ['**/tests/**/*.test.ts'],
};
// tests/setup.ts
import axios from 'axios';

const DIRECTUS_URL = process.env.DIRECTUS_URL || 'http://localhost:8055';

export async function getAdminToken(): Promise<string> {
  const resp = await axios.post(`/auth/login`, {
    email: 'admin@test.local',
    password: 'TestPassword123!',
  });
  return resp.data.data.access_token;
}

export async function setupTestCollection(token: string) {
  const api = axios.create({
    baseURL: DIRECTUS_URL,
    headers: { Authorization: `Bearer ` },
  });

  // สร้าง collection สำหรับ test
  try {
    await api.post('/collections', {
      collection: 'test_articles',
      meta: { icon: 'article', note: 'Test collection' },
      schema: {},
      fields: [
        { field: 'id', type: 'integer', meta: { hidden: true, interface: 'input' }, schema: { is_primary_key: true, has_auto_increment: true } },
        { field: 'title', type: 'string', meta: { interface: 'input' }, schema: { is_nullable: false } },
        { field: 'content', type: 'text', meta: { interface: 'input-rich-text-md' } },
        { field: 'status', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [{ text: 'Draft', value: 'draft' }, { text: 'Published', value: 'published' }] } }, schema: { default_value: 'draft' } },
        { field: 'publish_date', type: 'timestamp', meta: { interface: 'datetime' } },
      ],
    });
  } catch (e: any) {
    if (e.response?.status !== 400) throw e; // 400 = already exists
  }
}
// tests/crud.test.ts
import axios, { AxiosInstance } from 'axios';
import { getAdminToken, setupTestCollection } from './setup';

const DIRECTUS_URL = process.env.DIRECTUS_URL || 'http://localhost:8055';
let api: AxiosInstance;
let createdId: number;

beforeAll(async () => {
  const token = await getAdminToken();
  api = axios.create({
    baseURL: DIRECTUS_URL,
    headers: { Authorization: `Bearer ` },
  });
  await setupTestCollection(token);
});

describe('Directus CRUD Operations', () => {
  test('CREATE - สร้าง article ใหม่', async () => {
    const resp = await api.post('/items/test_articles', {
      title: 'ทดสอบบทความ',
      content: 'เนื้อหาทดสอบ',
      status: 'draft',
    });
    expect(resp.status).toBe(200);
    expect(resp.data.data.title).toBe('ทดสอบบทความ');
    createdId = resp.data.data.id;
  });

  test('READ - อ่าน article ที่สร้าง', async () => {
    const resp = await api.get(`/items/test_articles/`);
    expect(resp.status).toBe(200);
    expect(resp.data.data.id).toBe(createdId);
    expect(resp.data.data.status).toBe('draft');
  });

  test('UPDATE - แก้ไข title', async () => {
    const resp = await api.patch(`/items/test_articles/`, {
      title: 'บทความที่แก้ไขแล้ว',
      status: 'published',
    });
    expect(resp.status).toBe(200);
    expect(resp.data.data.title).toBe('บทความที่แก้ไขแล้ว');
  });

  test('FILTER - ค้นหาด้วย filter', async () => {
    const resp = await api.get('/items/test_articles', {
      params: {
        'filter[status][_eq]': 'published',
        'fields': 'id, title, status',
        'sort': '-id',
        'limit': 10,
      },
    });
    expect(resp.status).toBe(200);
    expect(resp.data.data.length).toBeGreaterThan(0);
    expect(resp.data.data[0].status).toBe('published');
  });

  test('DELETE - ลบ article', async () => {
    const resp = await api.delete(`/items/test_articles/`);
    expect(resp.status).toBe(204);

    // ยืนยันว่าลบแล้ว
    try {
      await api.get(`/items/test_articles/`);
      fail('Should have thrown 403/404');
    } catch (e: any) {
      expect([403, 404]).toContain(e.response.status);
    }
  });
});

Permission Testing — ทดสอบระบบ Access Control

Directus มี granular permission system ที่ต้องทดสอบให้ครบทุก role เพื่อป้องกัน data leak

// tests/permissions.test.ts
import axios, { AxiosInstance } from 'axios';
import { getAdminToken } from './setup';

const DIRECTUS_URL = process.env.DIRECTUS_URL || 'http://localhost:8055';

let adminApi: AxiosInstance;
let editorToken: string;
let editorApi: AxiosInstance;

beforeAll(async () => {
  const adminToken = await getAdminToken();
  adminApi = axios.create({
    baseURL: DIRECTUS_URL,
    headers: { Authorization: `Bearer ` },
  });

  // สร้าง editor role
  const roleResp = await adminApi.post('/roles', {
    name: 'Editor',
    icon: 'edit',
    admin_access: false,
    app_access: true,
  });
  const roleId = roleResp.data.data.id;

  // กำหนด permissions - editor อ่านได้ทุก article แต่แก้ได้เฉพาะ draft
  await adminApi.post('/permissions', {
    role: roleId,
    collection: 'test_articles',
    action: 'read',
    fields: ['*'],
  });
  await adminApi.post('/permissions', {
    role: roleId,
    collection: 'test_articles',
    action: 'update',
    fields: ['title', 'content'],
    permissions: { status: { _eq: 'draft' } },
  });

  // สร้าง editor user
  await adminApi.post('/users', {
    email: 'editor@test.local',
    password: 'EditorPass123!',
    role: roleId,
  });

  // login เป็น editor
  const loginResp = await axios.post(`/auth/login`, {
    email: 'editor@test.local',
    password: 'EditorPass123!',
  });
  editorToken = loginResp.data.data.access_token;
  editorApi = axios.create({
    baseURL: DIRECTUS_URL,
    headers: { Authorization: `Bearer ` },
  });
});

describe('Permission Tests', () => {
  test('Editor ไม่สามารถสร้าง article ได้', async () => {
    try {
      await editorApi.post('/items/test_articles', { title: 'Hacked!' });
      fail('Should not allow create');
    } catch (e: any) {
      expect(e.response.status).toBe(403);
    }
  });

  test('Editor ไม่สามารถแก้ published article ได้', async () => {
    // admin สร้าง published article
    const resp = await adminApi.post('/items/test_articles', {
      title: 'Published Article',
      status: 'published',
    });
    const id = resp.data.data.id;

    try {
      await editorApi.patch(`/items/test_articles/`, {
        title: 'Hacked Title',
      });
      fail('Should not allow editing published articles');
    } catch (e: any) {
      expect(e.response.status).toBe(403);
    }
  });

  test('Editor สามารถแก้ draft article ได้', async () => {
    const resp = await adminApi.post('/items/test_articles', {
      title: 'Draft Article',
      status: 'draft',
    });
    const id = resp.data.data.id;

    const updateResp = await editorApi.patch(`/items/test_articles/`, {
      title: 'Updated Draft',
    });
    expect(updateResp.status).toBe(200);
  });
});

Load Testing ด้วย k6

ทดสอบว่า Directus API รับ load ได้เพียงพอก่อน deploy production

// load-test.js (k6 script)
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 20 },   // ramp up
    { duration: '1m', target: 50 },    // hold
    { duration: '30s', target: 100 },  // spike
    { duration: '30s', target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],  // 95% ต้องเร็วกว่า 500ms
    http_req_failed: ['rate<0.01'],    // error rate < 1%
  },
};

const BASE = 'http://localhost:8055';
let TOKEN;

export function setup() {
  const loginRes = http.post(`/auth/login`, JSON.stringify({
    email: 'admin@test.local',
    password: 'TestPassword123!',
  }), { headers: { 'Content-Type': 'application/json' } });
  return { token: loginRes.json().data.access_token };
}

export default function (data) {
  const headers = {
    Authorization: `Bearer `,
    'Content-Type': 'application/json',
  };

  // อ่าน articles list
  const listRes = http.get(`/items/test_articles?limit=20&sort=-id`, { headers });
  check(listRes, {
    'list status 200': (r) => r.status === 200,
    'list has data': (r) => r.json().data.length > 0,
  });

  // อ่าน single article
  const items = listRes.json().data;
  if (items.length > 0) {
    const id = items[Math.floor(Math.random() * items.length)].id;
    const getRes = http.get(`/items/test_articles/`, { headers });
    check(getRes, { 'get status 200': (r) => r.status === 200 });
  }

  sleep(0.5);
}
# รัน load test
k6 run load-test.js

# ตัวอย่าง output
#      ✓ list status 200
#      ✓ list has data
#      ✓ get status 200
#
#      http_req_duration..........: avg=45ms min=12ms med=38ms max=890ms p(90)=120ms p(95)=230ms
#      http_req_failed............: 0.00%   ✓ 0  ✗ 4521
#      iterations.................: 4521    75.35/s

CI/CD Pipeline สำหรับ Directus Testing

# .github/workflows/test-directus.yml
name: Directus API Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: directus_test
          POSTGRES_USER: directus
          POSTGRES_PASSWORD: directus_test_pass
        ports: ['5432:5432']
        options: --health-cmd="pg_isready" --health-interval=5s --health-timeout=3s --health-retries=5

    steps:
      - uses: actions/checkout@v4

      - name: Start Directus
        run: |
          docker run -d --name directus --network host \
            -e KEY=test-key -e SECRET=test-secret \
            -e ADMIN_EMAIL=admin@test.local \
            -e ADMIN_PASSWORD=TestPassword123! \
            -e DB_CLIENT=pg -e DB_HOST=localhost \
            -e DB_PORT=5432 -e DB_DATABASE=directus_test \
            -e DB_USER=directus -e DB_PASSWORD=directus_test_pass \
            directus/directus:10.10.0

      - name: Wait for Directus
        run: |
          for i in $(seq 1 30); do
            curl -s http://localhost:8055/server/health && break
            sleep 2
          done

      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - run: npm ci
      - run: npm test -- --coverage

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

FAQ — คำถามที่พบบ่อย

Q: ควรใช้ Directus SDK หรือ Axios ในการเขียน test?

A: ถ้าต้องการทดสอบ API behavior ตรงๆ ให้ใช้ Axios เพราะเห็น request/response ชัดเจน ถ้าต้องการทดสอบ business logic ที่ใช้ SDK อยู่แล้วให้ใช้ SDK เพื่อให้ test ใกล้เคียง production code

Q: ทำ snapshot testing กับ Directus schema ได้ไหม?

A: ได้ ใช้คำสั่ง npx directus schema snapshot ./snapshot.yaml เพื่อ export schema แล้วเก็บใน git ตอน CI ให้เปรียบเทียบกับ snapshot ก่อนหน้าเพื่อตรวจจับ schema change ที่ไม่ตั้งใจ

Q: ทดสอบ Directus Flows (automation) อย่างไร?

A: สร้าง test ที่ trigger event แล้วตรวจ side effect เช่น สร้าง item แล้วเช็คว่า Flow ส่ง webhook หรือสร้าง related item ตามที่ตั้งไว้หรือไม่ ใช้ mock server อย่าง msw หรือ nock รับ webhook request ระหว่าง test

Q: ควร test migration ของ Directus อย่างไร?

A: ใช้ npx directus schema apply ./snapshot.yaml กับ test database ก่อน apply กับ production ตรวจสอบว่า migration ไม่ทำ data loss โดยสร้าง seed data ก่อน migrate แล้วเช็คว่า data ยังครบอยู่

📖 บทความที่เกี่ยวข้อง

Directus CMS Automation Scriptอ่านบทความ → Directus CMS Event Driven Designอ่านบทความ → Directus CMS Feature Flag Managementอ่านบทความ → Directus CMS Metric Collectionอ่านบทความ → Directus CMS Code Review Best Practiceอ่านบทความ →

📚 ดูบทความทั้งหมด →