refactor(testing): migrate from Jest to Vitest for testing framework

This commit is contained in:
Naoki Oketani
2025-05-02 14:03:34 +00:00
parent a898de739e
commit 2564984eab
11 changed files with 3190 additions and 467 deletions

View File

@@ -30,7 +30,7 @@ This document outlines the development guidelines and best practices for our Typ
- **Testing** - **Testing**
- Write unit tests for all business logic - Write unit tests for all business logic
- Aim for high test coverage (at least 80%) - Aim for high test coverage (at least 80%)
- Use Jest or Mocha for testing frameworks - Use Vitest as the testing framework
- Implement integration tests for critical paths - Implement integration tests for critical paths
- **Build Process** - **Build Process**

View File

@@ -17,15 +17,14 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Install dependencies and run all scripts - name: Install dependencies and run tests
run: | run: |
npm ci npm ci
npm run all npm run test -- --run
- uses: coverallsapp/github-action@master - uses: coverallsapp/github-action@master
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
# Adding a comment to clarify the purpose of the Windows build job
build-on-windows: build-on-windows:
strategy: strategy:
matrix: matrix:
@@ -36,11 +35,10 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
# Node.js 20 already includes a recent npm version, so npm upgrade is not needed - name: Install dependencies and run tests
- name: Install dependencies and run all scripts
run: | run: |
npm ci npm ci
npm run all npm run test -- --run
test: # make sure the action works on a clean machine without building test: # make sure the action works on a clean machine without building
strategy: strategy:

View File

@@ -14,3 +14,25 @@
### GitHub REST API v3 ### GitHub REST API v3
- https://developer.github.com/v3/ - https://developer.github.com/v3/
## Development Instructions
### Running Tests
This project uses [Vitest](https://vitest.dev/) for testing. To run the tests, use the following command:
```bash
npm run test
```
Vitest will execute all test files and provide a detailed report of the results.
### Generating Coverage Reports
To generate a test coverage report, use the following command:
```bash
npm run test:coverage
```
The coverage report will be available in the `coverage` directory.

View File

@@ -73,6 +73,28 @@ jobs:
dedupe_issues: true dedupe_issues: true
``` ```
## Development
### Running Tests
This project uses [Vitest](https://vitest.dev/) for testing. To run the tests, use the following command:
```bash
npm run test
```
Vitest will execute all test files and provide a detailed report of the results. For coverage reports, you can use:
```bash
npm run test:coverage
```
Ensure all dependencies are installed before running the tests:
```bash
npm ci
```
- - - - - -
This action is inspired by [homoluctus/gitrivy](https://github.com/homoluctus/gitrivy). This action is inspired by [homoluctus/gitrivy](https://github.com/homoluctus/gitrivy).

View File

@@ -3,17 +3,17 @@ import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import {Audit} from '../src/audit' import {Audit} from '../src/audit'
jest.mock('child_process') vi.mock('child_process')
const audit = new Audit() const audit = new Audit()
describe('run', () => { describe('run', () => {
beforeEach(() => { beforeEach(() => {
jest.mocked(child_process).spawnSync.mockClear() vi.mocked(child_process).spawnSync.mockClear()
}) })
test('finds vulnerabilities with default values', () => { test('finds vulnerabilities with default values', () => {
jest.mocked(child_process).spawnSync.mockImplementation((): any => { vi.mocked(child_process).spawnSync.mockImplementation((): any => {
const stdout = fs.readFileSync( const stdout = fs.readFileSync(
path.join(__dirname, 'testdata/audit/error.txt') path.join(__dirname, 'testdata/audit/error.txt')
) )
@@ -34,7 +34,7 @@ describe('run', () => {
}) })
test('finds vulnerabilities with production flag enabled', () => { test('finds vulnerabilities with production flag enabled', () => {
jest.mocked(child_process).spawnSync.mockImplementation((): any => { vi.mocked(child_process).spawnSync.mockImplementation((): any => {
const stdout = fs.readFileSync( const stdout = fs.readFileSync(
path.join(__dirname, 'testdata/audit/error.txt') path.join(__dirname, 'testdata/audit/error.txt')
) )
@@ -55,7 +55,7 @@ describe('run', () => {
}) })
test('finds vulnerabilities with json flag enabled', () => { test('finds vulnerabilities with json flag enabled', () => {
jest.mocked(child_process).spawnSync.mockImplementation((): any => { vi.mocked(child_process).spawnSync.mockImplementation((): any => {
const stdout = fs.readFileSync( const stdout = fs.readFileSync(
path.join(__dirname, 'testdata/audit/error.json') path.join(__dirname, 'testdata/audit/error.json')
) )
@@ -76,7 +76,7 @@ describe('run', () => {
}) })
test('does not find vulnerabilities', () => { test('does not find vulnerabilities', () => {
jest.mocked(child_process).spawnSync.mockImplementation((): any => { vi.mocked(child_process).spawnSync.mockImplementation((): any => {
const stdout = fs.readFileSync( const stdout = fs.readFileSync(
path.join(__dirname, 'testdata/audit/success.txt') path.join(__dirname, 'testdata/audit/success.txt')
) )
@@ -97,7 +97,7 @@ describe('run', () => {
}) })
test('throws an error if error is not null', () => { test('throws an error if error is not null', () => {
jest.mocked(child_process).spawnSync.mockImplementation((): any => { vi.mocked(child_process).spawnSync.mockImplementation((): any => {
return { return {
pid: 100, pid: 100,
output: '', output: '',
@@ -115,7 +115,7 @@ describe('run', () => {
}) })
test('throws an error if status is null', () => { test('throws an error if status is null', () => {
jest.mocked(child_process).spawnSync.mockImplementation((): any => { vi.mocked(child_process).spawnSync.mockImplementation((): any => {
return { return {
pid: 100, pid: 100,
output: '', output: '',
@@ -133,7 +133,7 @@ describe('run', () => {
}) })
test('throws an error if stderr is null', () => { test('throws an error if stderr is null', () => {
jest.mocked(child_process).spawnSync.mockImplementation((): any => { vi.mocked(child_process).spawnSync.mockImplementation((): any => {
return { return {
pid: 100, pid: 100,
output: '', output: '',

View File

@@ -68,7 +68,7 @@ describe('getExistingIssueNumber', () => {
}) })
test('gets existing open issue', async () => { test('gets existing open issue', async () => {
const getIssues = jest.fn() const getIssues = vi.fn()
getIssues.mockResolvedValue({ getIssues.mockResolvedValue({
data: [ data: [
{ {
@@ -89,7 +89,7 @@ describe('getExistingIssueNumber', () => {
}) })
test('returns null when there is no open issue', async () => { test('returns null when there is no open issue', async () => {
const getIssues = jest.fn() const getIssues = vi.fn()
getIssues.mockResolvedValue({data: []}) getIssues.mockResolvedValue({data: []})
const result = await issue.getExistingIssueNumber(getIssues, {repo, owner}) const result = await issue.getExistingIssueNumber(getIssues, {repo, owner})
@@ -104,7 +104,7 @@ describe('getExistingIssueNumber', () => {
}) })
test('returns null when no issues match the issue title', async () => { test('returns null when no issues match the issue title', async () => {
const getIssues = jest.fn() const getIssues = vi.fn()
getIssues.mockResolvedValue({ getIssues.mockResolvedValue({
data: [ data: [
{ {

View File

@@ -5,17 +5,17 @@ import {run} from '../src/main'
import * as issue from '../src/issue' import * as issue from '../src/issue'
import * as pr from '../src/pr' import * as pr from '../src/pr'
jest.mock('../src/audit') vi.mock('../src/audit')
jest.mock('../src/issue') vi.mock('../src/issue')
jest.mock('../src/pr') vi.mock('../src/pr')
jest.mock('@octokit/rest', () => { vi.mock('@octokit/rest', () => {
return { return {
Octokit: jest.fn().mockImplementation(() => { Octokit: vi.fn().mockImplementation(() => {
return { return {
issues: { issues: {
listForRepo: jest.fn(), listForRepo: vi.fn(),
createComment: jest.fn(), createComment: vi.fn(),
create: jest.fn() create: vi.fn()
} }
} }
}) })
@@ -25,8 +25,8 @@ jest.mock('@octokit/rest', () => {
describe('run: pr', () => { describe('run: pr', () => {
beforeEach(() => { beforeEach(() => {
// initialize mock // initialize mock
jest.mocked(Audit).mockClear() vi.mocked(Audit).mockClear()
jest.mocked(pr).createComment.mockClear() vi.mocked(pr).createComment.mockClear()
process.env.INPUT_AUDIT_LEVEL = 'low' process.env.INPUT_AUDIT_LEVEL = 'low'
process.env.INPUT_PRODUCTION_FLAG = 'false' process.env.INPUT_PRODUCTION_FLAG = 'false'
@@ -39,7 +39,7 @@ describe('run: pr', () => {
}) })
test('does not call pr.createComment if vulnerabilities are not found', () => { test('does not call pr.createComment if vulnerabilities are not found', () => {
jest.mocked(Audit).mockImplementation((): any => { vi.mocked(Audit).mockImplementation((): any => {
return { return {
stdout: fs.readFileSync( stdout: fs.readFileSync(
path.join(__dirname, 'testdata/audit/success.txt') path.join(__dirname, 'testdata/audit/success.txt')
@@ -57,14 +57,14 @@ describe('run: pr', () => {
} }
}) })
jest.mocked(pr).createComment.mockResolvedValue() vi.mocked(pr).createComment.mockResolvedValue()
expect(run).not.toThrowError() expect(run).not.toThrowError()
expect(pr.createComment).not.toHaveBeenCalled() expect(pr.createComment).not.toHaveBeenCalled()
}) })
test('calls pr.createComment if vulnerabilities are found in PR', () => { test('calls pr.createComment if vulnerabilities are found in PR', () => {
jest.mocked(Audit).mockImplementation((): any => { vi.mocked(Audit).mockImplementation((): any => {
return { return {
stdout: fs.readFileSync( stdout: fs.readFileSync(
path.join(__dirname, 'testdata/audit/error.txt') path.join(__dirname, 'testdata/audit/error.txt')
@@ -82,7 +82,7 @@ describe('run: pr', () => {
} }
}) })
jest.mocked(pr).createComment.mockResolvedValue() vi.mocked(pr).createComment.mockResolvedValue()
expect(run).not.toThrowError() expect(run).not.toThrowError()
expect(pr.createComment).toHaveBeenCalled() expect(pr.createComment).toHaveBeenCalled()
@@ -91,7 +91,7 @@ describe('run: pr', () => {
test('does not call pr.createComment if create_pr_comments is set to false', () => { test('does not call pr.createComment if create_pr_comments is set to false', () => {
process.env.INPUT_CREATE_PR_COMMENTS = 'false' process.env.INPUT_CREATE_PR_COMMENTS = 'false'
jest.mocked(Audit).mockImplementation((): any => { vi.mocked(Audit).mockImplementation((): any => {
return { return {
stdout: fs.readFileSync( stdout: fs.readFileSync(
path.join(__dirname, 'testdata/audit/error.txt') path.join(__dirname, 'testdata/audit/error.txt')
@@ -117,8 +117,8 @@ describe('run: pr', () => {
describe('run: issue', () => { describe('run: issue', () => {
beforeEach(() => { beforeEach(() => {
// initialize mock // initialize mock
jest.mocked(Audit).mockClear() vi.mocked(Audit).mockClear()
jest.mocked(issue).getExistingIssueNumber.mockClear() vi.mocked(issue).getExistingIssueNumber.mockClear()
process.env.INPUT_AUDIT_LEVEL = 'low' process.env.INPUT_AUDIT_LEVEL = 'low'
process.env.INPUT_PRODUCTION_FLAG = 'false' process.env.INPUT_PRODUCTION_FLAG = 'false'
@@ -133,7 +133,7 @@ describe('run: issue', () => {
test('does not call octokit.rest.issues.create if create_issues is set to false', () => { test('does not call octokit.rest.issues.create if create_issues is set to false', () => {
process.env.INPUT_CREATE_ISSUES = 'false' process.env.INPUT_CREATE_ISSUES = 'false'
jest.mocked(Audit).mockImplementation((): any => { vi.mocked(Audit).mockImplementation((): any => {
return { return {
stdout: fs.readFileSync( stdout: fs.readFileSync(
path.join(__dirname, 'testdata/audit/error.txt') path.join(__dirname, 'testdata/audit/error.txt')
@@ -151,7 +151,7 @@ describe('run: issue', () => {
} }
}) })
jest.mocked(issue).getExistingIssueNumber.mockResolvedValue(null) vi.mocked(issue).getExistingIssueNumber.mockResolvedValue(null)
expect(run).not.toThrowError() expect(run).not.toThrowError()
expect(issue.getExistingIssueNumber).not.toHaveBeenCalled() expect(issue.getExistingIssueNumber).not.toHaveBeenCalled()

View File

@@ -1,11 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

3503
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,9 @@
"format-check": "prettier --check **/*.ts", "format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts",
"pack": "ncc build", "pack": "ncc build",
"test": "jest", "test": "vitest",
"all": "npm run build && npm run format && npm run lint && npm run pack && npm test -- --coverage" "test:coverage": "vitest --coverage",
"all": "npm run build && npm run format && npm run lint && npm run pack && npm run test:coverage"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -35,18 +36,16 @@
"strip-ansi": "^6.0.1" "strip-ansi": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"@typescript-eslint/parser": "^6.7.4", "@typescript-eslint/parser": "^6.7.4",
"@vercel/ncc": "^0.38.3", "@vercel/ncc": "^0.38.3",
"eslint": "^8.51.0", "eslint": "^8.51.0",
"eslint-plugin-github": "^4.10.1", "eslint-plugin-github": "^4.10.1",
"eslint-plugin-jest": "^27.4.2", "eslint-plugin-jest": "^27.4.2",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"js-yaml": "^4.0.0", "js-yaml": "^4.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"ts-jest": "^29.3.2", "typescript": "^5.8.3",
"typescript": "^5.8.3" "vite": "^6.3.4",
"vitest": "^3.1.2"
} }
} }

12
vitest.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['__tests__/**/*.test.ts'],
coverage: {
reporter: ['text', 'json', 'html'],
},
},
});