Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Vitest #7

Merged
merged 13 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/pnpm-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ runs:
steps:
- uses: actions/setup-node@v3
env:
NODE_VERSION: "16"
NODE_VERSION: "20"
with:
node-version: "${{ env.NODE_VERSION }}"
- name: Install pnpm
uses: pnpm/action-setup@v2
id: pnpm-install
with:
version: 7
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
- uses: ./.github/actions/pnpm-install
with:
target: '.'
- uses: oven-sh/setup-bun@v1
- name: Lint
run: pnpm lint
- name: Test
Expand Down
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.0.0-beta.1] - 2023-03-17
### Added
- Support for `vitest` and `jest` ESM mode
- `Spy.setup` to set up the library

### Changed
- `Spy.mockModule` removed
- `Spy.mockReactComponents` changed first param type
- side effects removed: `Spy.setup` needs to be invoked
- default configs changed:
- `enforceOrder`: false -> true
- `useGenericReactMocks`: false -> true
- `useOwnEquals`: true -> false

For more detailed information please see [Migration Guide v4](https://github.com/fdc-viktor-luft/spy4js/blob/master/MIGRATIONGUIDE.md#400)


## [3.4.1] - 2022-05-25
### Fixed
- Only use generated TS declaration files instead of sources to avoid TS errors in peers.
Expand Down
36 changes: 36 additions & 0 deletions MIGRATIONGUIDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
# Migration Guide
You can find here tips for migrating breaking changes.

## 4.0.0

The library was previously causing side effects on import. This behavior changed now and requires manual
invocation of `Spy.setup()`. This can be done ideally in some central setup file for all of your tests.

The default configuration changed now. In order to continue using the previous defaults, you can do this:

```ts
Spy.setup({ enforceOrder: false, useGenericReactMocks: false, useOwnEquals: true });
```

The method `Spy.initMocks` was removed now as it was previously already more of an internal function. Mocks
get initialized in test runner or provided `beforeEach` callbacks. This also means that `Spy.mock`,
`Spy.mockReactComponents` cannot be invoked anymore inside the `test` (`it`) function.

The library was also doing some custom module mocking that is no longer maintainable considering that it
was based on CommonJS and does not work with `vitest` or the new (still experimental) ESM mode from `jest`.

The method `Spy.mockModule` was removed. To achieve the same you need to transform like this:

```ts
Spy.mockModule('./my-module', 'foo'); // OLD
Spy.mock(require('./my-module'), 'foo'); // NEW
```

The method `Spy.mockReactComponents` uses also no build-in module mocks anymore and requires this change:

```ts
Spy.mockReactComponents('./my-module', 'foo'); // OLD
Spy.mockReactComponents(require('./my-module'), 'foo'); // NEW
```

## 3.0.0

- `new Spy()` -> `Spy()`

## 2.0.0
- If you have previously added an own hook, which called `Spy.restoreAll` after each test suite, you may remove it.
- If you get troubles with the automatically added test hooks, you can override/remove it by usage of `Spy.configure`
Expand Down
127 changes: 67 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@

### Introduction

**spy4js** provides a stand-alone spy framework. It is decoupled by any dependencies
and other assertion frameworks.
**spy4js** provides a stand-alone spy framework that can be integrated well with the frameworks
[Vitest](https://vitest.dev/) and [Jest](https://jestjs.io/).

⚠️Disclaimer: You don't need this library as the frameworks already everything in order to achieve
everything without this library.

**spy4js** exports only one object called `Spy`. The spy instances
come with a lot of useful features. See below for more.

**Hint**:
My favorite test framework is [Jest](https://jestjs.io/). If you are using other
frameworks you might get issues related to automatically applied test suite hooks.
To overcome this default behavior see [here](#configure-static). Since Jest already
includes excellent spies itself, you might ask yourself, why `spy4js`. Because it's better.
**Hint**:
If you are using other frameworks you might not be able to benefit from all features of this library.
Since the test frameworks already include excellent spies themselves, you might ask yourself, why `spy4js`...

Because it can make your tests more readable.

Advantages over Jest spies:
- Very important for tests is their readability. This spy API is much easier to learn, and
Expand Down Expand Up @@ -72,25 +75,29 @@ const someObject2 = new Date(2017, 1, 15);
const someObject2$Mock = Spy.mock(someObject2, 'toJSON', 'toString', 'getDate');

// mock exported functions from other modules
const myModuleMocks = Spy.mockModule('./my-module', 'useMe');
const myModuleMocksInJest = Spy.mock(require('./my-module'), 'useMe');
vi.mock('./my-module', async () => ({ ...((await vi.importActual('./my-module')) as any) }));
const myModuleMocksInVitest = Spy.mock(await import('./my-module'), 'useMe');

// mock React components from other modules
const { Calculator } = Spy.mockModule('./my/fancy/Calculator', 'Calculator');
const mockedReactComponentsInJest = Spy.mockReactComponents(require('./my/fancy/Calculator'), 'Calculator');
vi.mock('./my/fancy/Calculator', async () => ({ ...((await vi.importActual('./my/fancy/Calculator')) as any) }));
const mockedReactComponentsInVitest = Spy.mockReactComponents(await import('./my/fancy/Calculator'), 'Calculator');
```

You may apply additional behavior to every spy. The valid operations here are:

- `configure` (some external libraries may use own "equals" implementations unexpectedly)
- `calls` (does make the spy call the provided functions sequentially)
- `returns` (does make the spy return the provided params sequentially)
- `throws` (does make the spy throw an error when called)
- `resolves` (does make the spy resolve the provided params sequentially) #Promise
- `rejects` (does make the spy reject an error when called) #Promise
- `transparent` (does make the spy call the original method of a mocked object)
- `transparentAfter` (does make the spy call the original method of a mocked object after a certain amount of made calls)
- `reset` (resets the registered calls which were already made)
- `restore` (does make the spy restore the mocked object)
- `addSnapshotSerializer` (defines in `jest` snapshots how the spy will be serialized)
- `configure` (allows to override some behavior of the spy)
- `calls` (does make the spy call the provided functions sequentially)
- `returns` (does make the spy return the provided params sequentially)
- `throws` (does make the spy throw an error when called)
- `resolves` (does make the spy resolve the provided params sequentially) #Promise
- `rejects` (does make the spy reject an error when called) #Promise
- `transparent` (does make the spy call the original method of a mocked object)
- `transparentAfter` (does make the spy call the original method of a mocked object after a certain amount of made calls)
- `reset` (resets the registered calls which were already made)
- `restore` (does make the spy restore the mocked object)
- `addSnapshotSerializer` (defines in `jest` snapshots how the spy will be serialized)

All those methods on a spy has been designed in a builder pattern. So you may chain any of
these configurations. Be aware some behaviors override existing behaviors.
Expand Down Expand Up @@ -136,12 +143,12 @@ spy.restore(); // other than "transparent" does not control input and output of

Even as important are the "facts", we want to display:

- `wasCalled` (does display that the spy has been called a specifiable amount of times)
- `wasNotCalled` (does display that the spy has never been called)
- `wasCalledWith` (does display that the spy has been called at least once like with the provided params)
- `wasNotCalledWith` (does display that the spy was never like with the provided params)
- `hasCallHistory` (does display that the spy has been called with the following params in the given order)
- `hasProps` (does display that the spy has been called with the given argument on the last invocation. In the
- `wasCalled` (does display that the spy has been called a specifiable amount of times)
- `wasNotCalled` (does display that the spy has never been called)
- `wasCalledWith` (does display that the spy has been called at least once like with the provided params)
- `wasNotCalledWith` (does display that the spy was never like with the provided params)
- `hasCallHistory` (does display that the spy has been called with the following params in the given order)
- `hasProps` (does display that the spy has been called with the given argument on the last invocation. In the
context of React: The spy being a mocked React component has currently the given props.)

Those methods on a spy display facts. Facts have to be true, otherwise they
Expand Down Expand Up @@ -173,25 +180,15 @@ spy.wasNotCalledWith([1, 'test', {attr: [3]}]);
spy.hasCallHistory([ [1, 'test', {attr: [4]}] ], 'with this text');
```

There is one static method that does restore all existing spies in all tests.
This is extremely useful to clean up all still existing mocks. By default, this is
automatically done after every test run (this is done by default).

- `restoreAll` (does restore every existing spy)

```ts
Spy.restoreAll();
```

Sometimes it is necessary to have access to some call arguments with
which the spy had been called.

- `getAllCallArguments` (returns all call arguments for all calls in an array containing arrays)
- `getCallArguments` (returns all call arguments for a specified call in an array)
- `getCallArgument` (same as getCallArguments, but returns only a single element of the array)
- `getLatestCallArgument` (same as getCallArgument, but for the latest call)
- `getProps` (same as getLatestCallArgument, but only for the first param. Can be useful for mocked React components)
- `getCallCount` (returns the number of made calls)
- `getAllCallArguments` (returns all call arguments for all calls in an array containing arrays)
- `getCallArguments` (returns all call arguments for a specified call in an array)
- `getCallArgument` (same as getCallArguments, but returns only a single element of the array)
- `getLatestCallArgument` (same as getCallArgument, but for the latest call)
- `getProps` (same as getLatestCallArgument, but only for the first param. Can be useful for mocked React components)
- `getCallCount` (returns the number of made calls)

```ts
const spy = Spy();
Expand Down Expand Up @@ -232,26 +229,42 @@ Spy(spyName:string = 'the spy') => SpyInstance
```
The returned Spy instance has his own name-attribute (only) for debugging purpose.

### setup (static)
```ts
Spy.setup(config: {
useOwnEquals?: boolean;
enforceOrder?: boolean;
useGenericReactMocks?: boolean;
afterEachCb?: () => void;
afterEach?: (cb: () => void) => void;
beforeEach?: (cb: () => void) => void;
expect?: { addSnapshotSerializer: (serializer: any) => void; getState: () => { currentTestName?: string } };
runner?: 'jest' | 'vitest' | 'other';
}) => void
```
This function should be ideally called in some setup-test file, but is required to be called to get all
benefits of `spy4js`.
- **afterEach**: The test runners `afterEach` hook (default: `global.afterEach`)
- **beforeEach**: The test runners `beforeEach` hook (default: `global.beforeEach`)
- **runner**: The test runner name (default: determines if "jest" or "vitest")

For the other options see [below](#configure-static).

### configure (static)
```ts
Spy.configure(config: {
useOwnEquals?: boolean,
enforceOrder?: boolean,
useGenericReactMocks?: boolean,
beforeEach?: (scope: string) => void,
afterEach?: (scope: string) => void,
useOwnEquals?: boolean;
enforceOrder?: boolean;
useGenericReactMocks?: boolean;
afterEachCb?: () => void;
}) => void
```
Using this function you may edit the default behavior spy4js itself.
The scope param will contain the test-suite name, which was provided as first parameter
of the `describe` function. Please make sure that every scope name is unique per test file.
The configuration possibility are:
Using this function you may edit the default behavior `spy4js` itself.
- **useOwnEquals**: Applies for all spy instances. See [configure](#configure) for more details.
- **enforceOrder**: Opt-in to the [enforce-order mode](#enforce-order-mode).
- **useGenericReactMocks**: Lets you opt in into using generic react components for mocks
created via [mockReactComponents](#mockreactcomponents-static).
- **beforeEach**: Lets you override the default beforeEach test suite hook.
- **afterEach**: Lets you override the default afterEach test suite hook.
- **afterEachCb**: Lets you override the default afterEach fuctionality.

### on (static)
```ts
Expand All @@ -272,15 +285,9 @@ Creating an object that references spies for all given methodNames.
Initialize as many spies as required for the same object. Only
after `Spy.initMocks` gets called, the created mock does affect the given object.

### mockModule (static)
```ts
Spy.mockModule(moduleName: string, ...methodNames: string[]) => Object (Mock)
```
Same as [mock](#mock-static) but only necessary if you want to mock exported functions.

### mockReactComponents (static)
```ts
Spy.mockReactComponents(moduleName: string, ...methodNames: string[]) => Object (Mock)
Spy.mockReactComponents(object: object, ...methodNames: string[]) => Object (Mock)
```
Same as [mockModule](#mockModule-static) but designed for ReactJS components. The registered
spies return `null` instead of `undefined`. This makes minimal usable React components.
Expand Down
24 changes: 24 additions & 0 deletions bun-prepare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from 'vitest';

expect.addSnapshotSerializer = () => null;
expect.getState = () => ({} as any);
expect.getState = () => ({} as any);
vikingair marked this conversation as resolved.
Show resolved Hide resolved
expect.extend({
toMatchInlineSnapshot: function (actual, match) {
const padding = match.includes('\n') ? ' '.repeat(match.substring(1).indexOf('"')) : '';
const pass = match.trim().slice(1, -1).replaceAll(padding, '') === actual;
if (pass) {
return {
message: () =>
`expected ${this.utils.printReceived(actual)} not to match ${this.utils.printExpected(match)}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${this.utils.printReceived(actual)} to match ${this.utils.printExpected(match)}`,
pass: false,
};
}
},
});
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
preload = "./bun-prepare.ts"
9 changes: 3 additions & 6 deletions jest.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
export default {
collectCoverageFrom: ['src/**/*.ts?(x)', '!src/index.ts'],
testMatch: ["<rootDir>/test/**/(*.)test.ts?(x)"],
coverageThreshold: { global: { statements: 100, branches: 100, functions: 100, lines: 100 } },
coverageDirectory: "<rootDir>/coverage",
testEnvironment: 'jsdom',
transform: { "\\.(js|jsx|ts|tsx)$": "@sucrase/jest-plugin" },
testMatch: ['<rootDir>/test/**/(*.)jest.ts?(x)'],
testEnvironment: 'jsdom',
transform: { '\\.(js|jsx|ts|tsx)$': '@sucrase/jest-plugin' },
};
Loading