메인 콘텐츠로 건너뛰기
SDK는 테스트 코드에서 앱을 빌드, 배포, 설치 및 제거할 수 있는 프로그래매틱 API를 제공합니다. 이것을 Vitest 및 타입이 지정된 API 클라이언트와 함께 사용하면 실제 Twenty 서버를 대상으로 앱이 종단 간으로 동작하는지 검증하는 통합 테스트를 작성할 수 있습니다.

npm 패키지 사용

앱에서 원하는 npm 패키지를 설치해 사용할 수 있습니다. 로직 함수와 프런트 컴포넌트는 모두 esbuild로 번들되며, 모든 의존성이 출력물에 인라인됩니다 — 런타임에는 node_modules가 필요하지 않습니다.

패키지 설치

yarn add axios
그런 다음 코드에서 임포트하세요:
src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';

const handler = async (): Promise<any> => {
  const { data } = await axios.get('https://api.example.com/data');

  return { data };
};

export default defineLogicFunction({
  universalIdentifier: '...',
  name: 'fetch-data',
  description: 'Fetches data from an external API',
  timeoutSeconds: 10,
  handler,
});
프런트 컴포넌트에서도 동일하게 작동합니다:
src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';

const DateWidget = () => {
  return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'date-widget',
  component: DateWidget,
});

번들링 동작 방식

빌드 단계에서는 esbuild를 사용하여 로직 함수와 프런트 컴포넌트마다 단일 자체 포함 파일을 생성합니다. 모든 임포트된 패키지는 번들에 인라인됩니다. 로직 함수는 Node.js 환경에서 실행됩니다. Node 기본 모듈(fs, path, crypto, http 등) 을(를) 사용할 수 있으며 설치할 필요가 없습니다. 프런트 컴포넌트는 Web Worker에서 실행됩니다. Node 기본 모듈은 사용할 수 없습니다 — 브라우저 환경에서 동작하는 브라우저 API와 npm 패키지만 사용할 수 있습니다. 두 환경 모두에서 twenty-client-sdk/coretwenty-client-sdk/metadata가 사전 제공 모듈로 사용 가능합니다 — 이는 번들되지 않고 서버가 런타임에 해석합니다.

설정

스캐폴딩된 앱에는 이미 Vitest가 포함되어 있습니다. 수동으로 설정하는 경우, 의존성을 설치하세요:
yarn add -D vitest vite-tsconfig-paths
앱 루트에 vitest.config.ts를 생성하세요:
vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';

// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;

export default defineConfig({
  plugins: [
    tsconfigPaths({
      projects: ['tsconfig.spec.json'],
      ignoreConfigErrors: true,
    }),
  ],
  test: {
    testTimeout: 120_000,
    hookTimeout: 120_000,
    fileParallelism: false,
    include: ['src/**/*.integration-test.ts'],
    globalSetup: ['src/__tests__/global-setup.ts'],
    env: {
      TWENTY_API_URL,
      TWENTY_API_KEY,
    },
  },
});
서버에 연결할 수 있는지 확인하고, SDK용 테스트 구성 파일(~/.twenty/config.test.json)을 작성한 다음, 테스트 실행 전에 앱을 동기화하는 글로벌 설정 파일을 생성합니다:
src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

import { appDevOnce, appUninstall } from 'twenty-sdk/cli';

const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');

export async function setup() {
  const apiUrl = process.env.TWENTY_API_URL!;
  const apiKey = process.env.TWENTY_API_KEY!;

  // Verify the server is running
  const response = await fetch(`${apiUrl}/healthz`);
  if (!response.ok) {
    throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
  }

  // Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
  fs.writeFileSync(
    path.join(CONFIG_DIR, 'config.test.json'),
    JSON.stringify({
      remotes: { local: { apiUrl, apiKey } },
      defaultRemote: 'local',
    }, null, 2),
  );

  // Start from a clean slate, then sync the app
  await appUninstall({ appPath: APP_PATH }).catch(() => {});

  const result = await appDevOnce({ appPath: APP_PATH });
  if (!result.success) {
    throw new Error(`Dev sync failed: ${result.error?.message}`);
  }
}

export async function teardown() {
  await appUninstall({ appPath: APP_PATH });
}

프로그래매틱 SDK API

twenty-sdk/cli 서브 경로는 테스트 코드에서 직접 호출할 수 있는 함수를 내보냅니다:
함수설명
appBuild앱을 빌드하고 필요하면 타르볼로 패키징
appDeploy타르볼을 서버로 업로드
appDevOnce앱을 한 번만 빌드하고 동기화합니다(yarn twenty apply와 동일).
appInstall활성 워크스페이스에 앱 설치
appUninstall활성 워크스페이스에서 앱 제거
각 함수는 success: booleandata 또는 error를 포함한 결과 객체를 반환합니다.

통합 테스트 작성

다음은 앱을 빌드, 배포, 설치한 후 워크스페이스에 표시되는지 검증하는 전체 예제입니다:
src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

const APP_PATH = process.cwd();

describe('App installation', () => {
  beforeAll(async () => {
    const buildResult = await appBuild({
      appPath: APP_PATH,
      tarball: true,
      onProgress: (message: string) => console.log(`[build] ${message}`),
    });

    if (!buildResult.success) {
      throw new Error(`Build failed: ${buildResult.error?.message}`);
    }

    const deployResult = await appDeploy({
      tarballPath: buildResult.data.tarballPath!,
      onProgress: (message: string) => console.log(`[deploy] ${message}`),
    });

    if (!deployResult.success) {
      throw new Error(`Deploy failed: ${deployResult.error?.message}`);
    }

    const installResult = await appInstall({ appPath: APP_PATH });

    if (!installResult.success) {
      throw new Error(`Install failed: ${installResult.error?.message}`);
    }
  });

  afterAll(async () => {
    await appUninstall({ appPath: APP_PATH });
  });

  it('should find the installed app in the workspace', async () => {
    const metadataClient = new MetadataApiClient();

    const result = await metadataClient.query({
      findManyApplications: {
        id: true,
        name: true,
        universalIdentifier: true,
      },
    });

    const installedApp = result.findManyApplications.find(
      (app: { universalIdentifier: string }) =>
        app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
    );

    expect(installedApp).toBeDefined();
  });
});

테스트 실행

로컬 Twenty 서버가 실행 중인지 확인한 다음, 다음을 실행하세요:
yarn test
또는 개발 중에는 워치 모드로 실행하세요:
yarn test:watch

타입 검사

테스트를 실행하지 않고도 앱에 대해 타입 검사를 수행할 수 있습니다:
yarn twenty dev:typecheck
이는 앱의 tsconfig.json에 대해 tsc --noEmit를 실행하고 모든 타입 오류를 보고합니다. 스캐폴딩된 앱에는 테스트 파일(tsconfig.spec.json)도 검사하는 yarn typecheck 스크립트가 함께 제공됩니다.

GitHub Actions로 CI

스캐폴더는 .github/workflows/ci.yml에 바로 사용할 수 있는 워크플로를 생성합니다. main으로의 모든 푸시와 모든 풀 리퀘스트마다, 러너에서 임시 Twenty 서버를 실행하고(twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test 액션을 통해), 그 서버를 가리키도록 설정된 TWENTY_API_URL / TWENTY_API_KEY와 함께 yarn lint, yarn typecheck, yarn test:unit, yarn test를 실행합니다. 시크릿은 필요 없으며, 워크플로 상단의 TWENTY_VERSION 환경 변수로 서버 버전을 고정할 수 있습니다. 스캐폴딩된 두 워크플로(ci.ymlcd.yml 배포 파이프라인)에 대한 전체 단계별 안내는 Publishing → Automated CI/CD를 참고하세요.