Skip to content

Commit

Permalink
feat: support in-source tests via import.meta.vitest (#901)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Mar 7, 2022
1 parent a3768a5 commit 3eec387
Show file tree
Hide file tree
Showing 16 changed files with 233 additions and 28 deletions.
72 changes: 72 additions & 0 deletions docs/guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,75 @@ export default defineConfig({
}
})
```

## In-source testing

Vitest also provides a way to run tests with in your source code along with the implementation, simliar to [Rust's module tests](https://doc.rust-lang.org/book/ch11-03-test-organization.html#the-tests-module-and-cfgtest).

This makes the tests share the same closure as the implementations and able to test against private states without exporting. Meanwhile, it also brings the closer feedback loop for development.

To get started, write a `if (import.meta.vitest)` block at the end of your source file, and write the tests inside it. For example:

```ts
// src/index.ts

// the implementation
export function add(...args: number[]) {
return args.reduce((a, b) => a + b, 0)
}

// in-source test suites
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
}
```

Update the `includeSource` config for Vitest to grab the files under `src/`:

```ts
// vite.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
includeSource: ['src/**/*.{js,ts}']
}
})
```

Then you can start to test!

```bash
$ npx vitest
```

For production build, you will need to set the `define` options in your config file, letting the bundler to do the dead code elimination. For example, in Vite

```diff
// vite.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
+ define: {
+ 'import.meta.vitest': false,
+ },
test: {
includeSource: ['src/**/*.{js,ts}']
},
})
```

For reference to [`test/import-meta`](https://github.com/vitest-dev/vitest/tree/main/test/import-meta) for a full example.

This feature could be useful for:

- Unit testing for small-scoped functions or utilities
- Prototyping
- Inline Assertion

It's recommended to use **separate test files instead** for more complex tests like components or E2E testing.
20 changes: 9 additions & 11 deletions examples/react-mui/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { defineConfig } from 'vite'
import { defineConfig } from 'vitest/config'

export default defineConfig(() => {
return {
esbuild: {
jsxInject: 'import React from \'react\'',
},
test: {
environment: 'jsdom',
globals: true,
},
}
export default defineConfig({
esbuild: {
jsxInject: 'import React from \'react\'',
},
test: {
environment: 'jsdom',
globals: true,
},
})
4 changes: 4 additions & 0 deletions packages/vitest/importMeta.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface ImportMeta {
url: string
readonly vitest?: typeof import('vitest')
}
3 changes: 3 additions & 0 deletions packages/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"./globals": {
"types": "./globals.d.ts"
},
"./importMeta": {
"types": "./importMeta.d.ts"
},
"./node": {
"import": "./dist/node.js",
"types": "./dist/node.d.ts"
Expand Down
56 changes: 41 additions & 15 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from 'fs'
import { existsSync, promises as fs } from 'fs'
import type { ViteDevServer } from 'vite'
import fg from 'fast-glob'
import mm from 'micromatch'
Expand Down Expand Up @@ -310,9 +310,9 @@ export class Vitest {
this.changedTests.delete(id)
}
}
const onAdd = (id: string) => {
const onAdd = async(id: string) => {
id = slash(id)
if (this.isTargetFile(id)) {
if (await this.isTargetFile(id)) {
this.changedTests.add(id)
this.scheduleRerun(id)
}
Expand Down Expand Up @@ -394,25 +394,51 @@ export class Vitest {
}

async globTestFiles(filters?: string[]) {
let files = await fg(
this.config.include,
{
absolute: true,
cwd: this.config.dir || this.config.root,
ignore: this.config.exclude,
},
)
const globOptions = {
absolute: true,
cwd: this.config.dir || this.config.root,
ignore: this.config.exclude,
}

let testFiles = await fg(this.config.include, globOptions)

if (filters?.length)
files = files.filter(i => filters.some(f => i.includes(f)))
testFiles = testFiles.filter(i => filters.some(f => i.includes(f)))

if (this.config.includeSource) {
let files = await fg(this.config.includeSource, globOptions)
if (filters?.length)
files = files.filter(i => filters.some(f => i.includes(f)))

await Promise.all(files.map(async(file) => {
try {
const code = await fs.readFile(file, 'utf-8')
if (this.isInSourceTestFile(code))
testFiles.push(file)
}
catch {
return null
}
}))
}

return files
return testFiles
}

isTargetFile(id: string): boolean {
async isTargetFile(id: string, source?: string): Promise<boolean> {
if (mm.isMatch(id, this.config.exclude))
return false
return mm.isMatch(id, this.config.include)
if (mm.isMatch(id, this.config.include))
return true
if (this.config.includeSource?.length && mm.isMatch(id, this.config.includeSource)) {
source = source || await fs.readFile(id, 'utf-8')
return this.isInSourceTestFile(source)
}
return false
}

isInSourceTestFile(code: string) {
return code.includes('import.meta.vitest')
}

printError(err: unknown) {
Expand Down
9 changes: 8 additions & 1 deletion packages/vitest/src/node/execute.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ViteNodeRunner } from 'vite-node/client'
import type { ModuleCache, ViteNodeRunnerOptions } from 'vite-node'
import { normalizePath } from 'vite'
import type { SuiteMocks } from './mocker'
import { VitestMocker } from './mocker'

Expand All @@ -23,6 +24,7 @@ export async function executeInViteNode(options: ExecuteOptions) {

export class VitestRunner extends ViteNodeRunner {
mocker: VitestMocker
entries = new Set<string>()

constructor(public options: ExecuteOptions) {
super(options)
Expand All @@ -38,10 +40,15 @@ export class VitestRunner extends ViteNodeRunner {
this.setCache(dep, module)
})

// support `import.meta.vitest` for test entry
if (__vitest_worker__.filepath && normalizePath(__vitest_worker__.filepath) === normalizePath(context.__filename)) {
// @ts-expect-error injected untyped global
Object.defineProperty(context.__vite_ssr_import_meta__, 'vitest', { get: () => globalThis.__vitest_index__ })
}

return Object.assign(context, {
__vite_ssr_import__: (dep: string) => mocker.requestWithMock(dep),
__vite_ssr_dynamic_import__: (dep: string) => mocker.requestWithMock(dep),

__vitest_mocker__: mocker,
})
}
Expand Down
3 changes: 3 additions & 0 deletions packages/vitest/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest())
const preOptions = deepMerge({}, configDefaults, options, viteConfig.test ?? {})
preOptions.api = resolveApiConfig(preOptions)

if (viteConfig.define)
delete viteConfig.define['import.meta.vitest']

// store defines for globalThis to make them
// reassignable when running in worker in src/runtime/setup.ts
const defines: Record<string, any> = {}
Expand Down
8 changes: 7 additions & 1 deletion packages/vitest/src/runtime/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import { Writable } from 'stream'
import { environments } from '../integrations/env'
import type { ResolvedConfig } from '../types'
import { toArray } from '../utils'
import * as VitestIndex from '../index'
import { rpc } from './rpc'

let globalSetup = false
export async function setupGlobalEnv(config: ResolvedConfig) {
// should be redeclared for each test
Object.defineProperty(globalThis, '__vitest_index__', {
value: VitestIndex,
enumerable: false,
})

// should be re-declared for each test
// if run with "threads: false"
setupDefines(config.defines)

Expand Down
7 changes: 7 additions & 0 deletions packages/vitest/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export interface InlineConfig {
*/
exclude?: string[]

/**
* Include globs for in-source test files
*
* @default []
*/
includeSource?: string[]

/**
* Handling for dependencies inlining or externalizing
*/
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions shims.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/// <reference path="./packages/vitest/config.d.ts" />
/// <reference path="./packages/vitest/globals.d.ts" />
/// <reference path="./packages/vitest/importMeta.d.ts" />
12 changes: 12 additions & 0 deletions test/import-meta/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@vitest/test-core",
"private": true,
"scripts": {
"test": "vitest",
"build": "vite build",
"coverage": "vitest run --coverage"
},
"devDependencies": {
"vitest": "workspace:*"
}
}
13 changes: 13 additions & 0 deletions test/import-meta/src/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function add(...args: number[]) {
return args.reduce((a, b) => a + b, 0)
}

// in-source test suites
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
}
23 changes: 23 additions & 0 deletions test/import-meta/src/fibonacci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { add } from './add'

export function fibonacci(n: number): number {
if (n < 2)
return n
return add(fibonacci(n - 1), fibonacci(n - 2))
}

if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('fibonacci', () => {
expect(fibonacci(0)).toBe(0)
expect(fibonacci(1)).toBe(1)
expect(fibonacci(2)).toBe(1)
expect(fibonacci(3)).toBe(2)
expect(fibonacci(4)).toBe(3)
expect(fibonacci(5)).toBe(5)
expect(fibonacci(6)).toBe(8)
expect(fibonacci(7)).toBe(13)
expect(fibonacci(8)).toBe(21)
expect(fibonacci(9)).toMatchInlineSnapshot('34')
})
}
2 changes: 2 additions & 0 deletions test/import-meta/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './add'
export * from './fibonacci'
20 changes: 20 additions & 0 deletions test/import-meta/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { resolve } from 'pathe'
import { defineConfig } from 'vite'

export default defineConfig({
test: {
includeSource: [
'src/**/*.ts',
],
},
define: {
'import.meta.vitest': false,
},
build: {
lib: {
formats: ['es', 'cjs'],
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'index',
},
},
})

0 comments on commit 3eec387

Please sign in to comment.