Vitest

Vitest는 Vite 생태계의 테스트 러너입니다. Turborepo와 통합하면 엄청난 속도 향상을 얻을 수 있습니다.

Vitest 문서는 하나의 루트 명령에서 monorepo의 모든 테스트를 실행하는 "Vitest Projects" 구성을 만드는 방법을 보여줍니다. 이는 병합된 커버리지 보고서와 같은 동작을 즉시 가능하게 합니다. 이 기능은 monorepo에 대한 현대적인 모범 사례를 따르지 않는데, Jest와의 호환성을 위해 설계되었기 때문입니다(Jest의 Workspace 기능은 패키지 관리자 Workspaces 이전에 구축되었습니다).

Vitest는 projects를 위해 workspaces를 더 이상 사용하지 않습니다. projects를 사용할 때 개별 프로젝트 vitest 구성은 projects 구성을 상속하므로 더 이상 루트 구성을 확장할 수 없습니다. 대신 vitest.shared.ts와 같은 별도의 공유 파일이 필요합니다.

이 때문에 각각 고유한 장단점이 있는 두 가지 옵션이 있습니다:

캐싱을 위한 Turborepo 활용

캐시 적중률을 개선하고 변경된 테스트만 실행하려면 패키지별로 작업을 구성하여 Vitest 명령을 각 패키지의 별도 캐시 가능한 스크립트로 분할할 수 있습니다. 이 속도는 병합된 커버리지 보고서를 직접 만들어야 한다는 절충안을 수반합니다.

완전한 예제를 보려면 npx create-turbo@latest --example with-vitest를 실행하거나 예제의 소스 코드를 방문하세요.

설정

다음과 같은 간단한 패키지 관리자 Workspace가 있다고 가정해 봅시다:

package.json
package.json

apps/webpackages/ui 모두 자체 테스트 스위트가 있으며, vitest사용하는 패키지에 설치됩니다. package.json 파일에는 Vitest를 실행하는 test 스크립트가 포함되어 있습니다:

./apps/web/package.json
{
  "scripts": {
    "test": "vitest run"
  },
  "devDependencies": {
    "vitest": "latest"
  }
}

루트 turbo.json 내부에 test 작업을 만듭니다:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["transit"]
    },
    "transit": {
      "dependsOn": ["^transit"]
    }
  }
}

이제 turbo run test는 각 패키지의 모든 테스트 스위트를 병렬화하고 캐시하여 변경된 코드만 테스트합니다.

watch 모드에서 테스트 실행

CI에서 테스트 스위트를 실행하면 결과를 기록하고 완료 시 종료됩니다. 즉, Turborepo로 캐시할 수 있습니다. 그러나 개발 중에 Vitest의 watch 모드를 사용하여 테스트를 실행하면 프로세스가 종료되지 않습니다. 이로 인해 watch 작업은 장기 실행 개발 작업과 더 비슷합니다.

이러한 차이로 인해 두 개의 별도 Turborepo 작업을 지정하는 것을 권장합니다: 하나는 테스트를 실행하는 것이고, 다른 하나는 watch 모드에서 실행하는 것입니다.

아래 전략은 두 가지 작업을 만듭니다. 하나는 로컬 개발용이고 다른 하나는 CI용입니다. 로컬 개발용으로 test 작업을 만들고 대신 test:ci 작업을 만들 수도 있습니다.

예를 들어, 각 워크스페이스의 package.json 파일 내부에:

./apps/web/package.json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch"
  }
}

그리고 루트 turbo.json 내부에:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["^test"]
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

이제 global turbo를 사용하여 turbo run test:watch로 작업을 실행하거나 루트 package.json의 스크립트에서 실행할 수 있습니다:

Terminal
turbo run test
 
turbo run test:watch

병합된 커버리지 보고서 생성

Vitest의 Projects 기능은 모든 패키지의 테스트 커버리지 보고서를 병합하는 즉시 사용 가능한 커버리지 보고서를 생성합니다. 하지만 Turborepo 전략을 따르는 경우 커버리지 보고서를 직접 병합해야 합니다.

with-vitest 예제는 여러분의 요구에 맞게 적용할 수 있는 완전한 예제를 보여줍니다. npx create-turbo@latest --example with-vitest를 사용하여 빠르게 시작할 수 있습니다.

이를 수행하려면 다음과 같은 일반적인 단계를 따릅니다:

  1. turbo run test를 실행하여 커버리지 보고서를 생성합니다.
  2. nyc merge로 커버리지 보고서를 병합합니다.
  3. nyc report를 사용하여 보고서를 생성합니다.

이를 수행할 Turborepo 작업은 다음과 같습니다:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["^test", "@repo/vitest-config#build"],
      "outputs": ["coverage.json"]
    }
    "merge-json-reports": {
      "inputs": ["coverage/raw/**"],
      "outputs": ["coverage/merged/**"]
    },
    "report": {
      "dependsOn": ["merge-json-reports"],
      "inputs": ["coverage/merge"],
      "outputs": ["coverage/report/**"]
    },
  }
}

이를 설정한 후 turbo test && turbo report를 실행하여 병합된 커버리지 보고서를 생성합니다.

with-vitest 예제는 여러분의 요구에 맞게 적용할 수 있는 완전한 예제를 보여줍니다. npx create-turbo@latest --example with-vitest를 사용하여 빠르게 시작할 수 있습니다.

Vitest의 Projects 기능 사용

Vitest Projects 기능은 패키지 관리자 Workspace와 동일한 모델을 따르지 않습니다. 대신 각 패키지의 테스트를 처리하기 위해 저장소의 각 패키지에 도달하는 루트 스크립트를 사용합니다.

이 모델에서는 현대 JavaScript 생태계 관점에서 패키지 경계가 없습니다. 이는 Turborepo가 이러한 패키지 경계에 의존하기 때문에 Turborepo의 캐싱을 신뢰할 수 없다는 것을 의미합니다.

이 때문에 Turborepo를 사용하여 테스트를 실행하려면 Root Tasks를 사용해야 합니다. Vitest Projects 설정을 구성한 후 Turborepo용 Root Tasks를 만듭니다:

Turborepo logo
./turbo.json
{
  "tasks": {
    "//#test": {
      "outputs": ["coverage/**"]
    },
    "//#test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

특히 Root Task의 파일 입력에는 기본적으로 모든 패키지가 포함되므로 어떤 패키지에서든 변경이 발생하면 캐시 미스가 발생합니다. 이는 병합된 커버리지 보고서를 생성하기 위한 간단한 구성을 만들지만, 반복되는 작업을 캐시할 수 있는 기회를 놓치게 됩니다.

하이브리드 접근 방식 사용

하이브리드 솔루션을 구현하여 두 접근 방식의 이점을 결합할 수 있습니다. 이 접근 방식은 Vitest의 Projects 기능을 사용하여 로컬 개발을 통합하면서 CI에서 Turborepo의 캐싱을 보존합니다. 이는 약간 더 많은 구성과 저장소에서 혼합된 작업 실행 모델이라는 트레이드오프를 수반합니다.

먼저 projects를 사용할 때 개별 프로젝트가 루트 구성을 확장할 수 없으므로 공유 구성 패키지를 만듭니다. 공유 Vitest 구성을 위한 새 패키지를 만듭니다:

./packages/vitest-config/package.json
{
  "name": "@repo/vitest-config",
  "version": "0.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "vitest": "latest"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*",
    "typescript": "latest"
  }
}
./packages/vitest-config/tsconfig.json
{
  "extends": "@repo/typescript-config/base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["dist", "node_modules"]
}
./packages/vitest-config/src/index.ts
export const sharedConfig = {
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    // Other shared configuration
  },
};

그런 다음 projects를 사용하여 루트 Vitest 구성을 만듭니다:

./vitest.config.ts
import { defineConfig } from 'vitest/config';
import { sharedConfig } from '@repo/vitest-config';
 
export default defineConfig({
  ...sharedConfig,
  projects: [
    {
      name: 'packages',
      root: './packages/*',
      test: {
        ...sharedConfig.test,
        // Project-specific configuration
      },
    },
  ],
});

이 설정에서 패키지는 공유 구성을 가져오는 개별 Vitest 구성을 유지합니다. 먼저 공유 구성 패키지를 설치합니다:

./packages/ui/package.json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch"
  },
  "devDependencies": {
    "@repo/vitest-config": "workspace:*",
    "vitest": "latest"
  }
}

그런 다음 Vitest 구성을 만듭니다:

./packages/ui/vitest.config.ts
import { defineConfig } from 'vitest/config';
import { sharedConfig } from '@repo/vitest-config';
 
export default defineConfig({
  ...sharedConfig,
  test: {
    ...sharedConfig.test,
    // Package-specific overrides if needed
  },
});

turbo.json을 업데이트하여 종속성 그래프에 새 구성 패키지를 포함해야 합니다:

Turborepo logo
./turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^test", "@repo/vitest-config#build"]
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

루트 package.json은 전역적으로 테스트를 실행하기 위한 스크립트를 포함합니다:

./package.json
{
  "scripts": {
    "test:projects": "vitest run",
    "test:projects:watch": "vitest --watch"
  }
}

이 구성을 통해 개발자는 Vitest projects를 사용하여 원활한 로컬 개발 경험을 위해 루트에서 pnpm test:projects 또는 pnpm test:projects:watch를 실행할 수 있으며, CI는 패키지 수준 캐싱을 활용하기 위해 계속 turbo run test를 사용합니다. 이전 섹션에서 설명한 대로 병합된 커버리지 보고서를 여전히 수동으로 처리해야 합니다.