Giter VIP home page Giter VIP logo

tinyspy's Introduction

tinyspy

minimal fork of nanospy, with more features ๐Ÿ•ต๐Ÿปโ€โ™‚๏ธ

A 10KB package for minimal and easy testing with no dependencies. This package was created for having a tiny spy library to use in vitest, but it can also be used in jest and other test environments.

In case you need more tiny libraries like tinypool or tinyspy, please consider submitting an RFC

Installing

// with npm
$ npm install -D tinyspy

// with pnpm
$ pnpm install -D tinyspy

// with yarn
$ yarn install -D tinyspy

Usage

spy

Simplest usage would be:

const fn = (n) => n + '!'
const spied = spy(fn)

spied('a')

console.log(spied.called) // true
console.log(spied.callCount) // 1
console.log(spied.calls) // [['a']]
console.log(spied.results) // [['ok', 'a!']]
console.log(spied.returns) // ['a!']

You can reset calls, returns, called and callCount with reset function:

const spied = spy((n) => n + '!')

spied('a')

console.log(spied.called) // true
console.log(spied.callCount) // 1
console.log(spied.calls) // [['a']]
console.log(spied.returns) // ['a!']

spied.reset()

console.log(spied.called) // false
console.log(spied.callCount) // 0
console.log(spied.calls) // []
console.log(spied.returns) // []

Since 3.0, tinyspy doesn't unwrap the Promise in returns and results anymore, so you need to await it manually:

const spied = spy(async (n) => n + '!')

const promise = spied('a')

console.log(spied.called) // true
console.log(spied.results) // ['ok', Promise<'a!'>]

await promise

console.log(spied.results) // ['ok', Promise<'a!'>]

console.log(await spied.returns[0]) // 'a!'

Warning

This also means the function that returned a Promise will always have result type 'ok' even if the Promise rejected

Tinyspy 3.0 still exposes resolved values on resolves property:

const spied = spy(async (n) => n + '!')

const promise = spied('a')

console.log(spied.called) // true
console.log(spied.resolves) // [] <- not resolved yet

await promise

console.log(spied.resolves) // ['ok', 'a!']

spyOn

All spy methods are available on spyOn.

You can spy on an object's method or setter/getter with spyOn function.

let apples = 0
const obj = {
  getApples: () => 13,
}

const spy = spyOn(obj, 'getApples', () => apples)
apples = 1

console.log(obj.getApples()) // prints 1

console.log(spy.called) // true
console.log(spy.returns) // [1]
let apples = 0
let fakedApples = 0
const obj = {
  get apples() {
    return apples
  },
  set apples(count) {
    apples = count
  },
}

const spyGetter = spyOn(obj, { getter: 'apples' }, () => fakedApples)
const spySetter = spyOn(obj, { setter: 'apples' }, (count) => {
  fakedApples = count
})

obj.apples = 1

console.log(spySetter.called) // true
console.log(spySetter.calls) // [[1]]

console.log(obj.apples) // 1
console.log(fakedApples) // 1
console.log(apples) // 0

console.log(spyGetter.called) // true
console.log(spyGetter.returns) // [1]

You can reassign mocked function and restore mock to its original implementation with restore method:

const obj = {
  fn: (n) => n + '!',
}
const spied = spyOn(obj, 'fn').willCall((n) => n + '.')

obj.fn('a')

console.log(spied.returns) // ['a.']

spied.restore()

obj.fn('a')

console.log(spied.returns) // ['a!']

You can even make an attribute into a dynamic getter!

let apples = 0
const obj = {
  apples: 13,
}

const spy = spyOn(obj, { getter: 'apples' }, () => apples)

apples = 1

console.log(obj.apples) // prints 1

You can restore spied function to its original value with restore method:

let apples = 0
const obj = {
  getApples: () => 13,
}

const spy = spyOn(obj, 'getApples', () => apples)

console.log(obj.getApples()) // 0

obj.restore()

console.log(obj.getApples()) // 13

Authors


Mohammad Bagher

Vladimir

Sponsors

Your sponsorship can make a huge difference in continuing our work in open source!

Vladimir sponsors

Mohammad sponsors

tinyspy's People

Contributors

anru avatar antfu avatar aslemammad avatar demivan avatar dubzzz avatar ecstrema avatar f5io avatar jastor11 avatar nickserv avatar purplnay avatar sheremet-va avatar sirenkovladd avatar snapstromegon avatar tony-go avatar userquin avatar usmanyunusov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tinyspy's Issues

`spy` does not retain the function result object if it is a promise

I have the following vitest test:

vi.mock(f).mockImplementation(() => {
   const res = new Promise()
   res.cancel = () => ....
   return res
})

When f is then called inside the production code, f.cancel is no longer there because - as far as I can see in the source code - tinyspy replaces the promise returned from mockImplementation with its result.

Edit: This is working in Jest, btw. (I am in the process of migration our Jest test code base to vitest)

Edit 2: If I remove the special handling of promises, all our tests run just fine. I did not dig deep enough into the code to fully understand why that code is there in the first place, so please forgive me for making dumb suggestions.

[Bug] Spying on class constructor remove functions on prototype

version: 2.2.0

When I do the following:

const {spyOn} = require('tinyspy')

class A {
    run() {
        console.log('run');
    }
}

const obj = {
    A
};

spyOn(obj, 'A');

const a = new obj.A();

a.run();

Or:

const {spyOn} = require('tinyspy')

function A() {}

A.prototype.run = function () {
    return console.log('run');
};

const obj = {
    A
};

const spy = spyOn(obj, 'A');

const a = new obj.A();

a.run();

Both codes will throw the following error:

TypeError: a.run is not a function

1.1.0 breaks instanceOf tests

I'm not sure if this was intended, but #31, breaks instanceof class.

import { afterEach, describe, expect, test, vi } from 'vitest';

import { MyClass } from './MyClass';

vi.mock('./MyClass');

const mockedMyClass = vi.mocked(MyClass);

describe('MyClass', () => {
    afterEach(() => {
        mockedMyClass.mockClear();
    });

    test.each`
        name          | expected
        ${'Atlantic'} | ${'Atlantic'}
        ${'Pacific'}  | ${'Pacific'}
    `('new MyClass $name', ({ name, expected }) => {
        const instance = new MyClass(name);
        expect(instance).toBeInstanceOf(MyClass);  // Was working in 1.0.2
        expect(mockedMyClass).toHaveBeenCalledTimes(1);
        expect(mockedMyClass).toHaveBeenCalledWith(expected);
    });
});

It is a bit of a contrived example. But I'm not sure how to preserve the inheritance chain.

Repo: https://github.com/Jason3S/tinyspy-issue-32
StackBlitz: https://stackblitz.com/edit/vitest-dev-vitest-dhhorq?file=README.md

Cannot spy on methods inherited from prototype chain. e.g. document.head.appendChild

๐Ÿ‘‹๐Ÿป I'm migrating some tests for VueUse and ran into an issue where spying on document.head.appendChild (removeChild, etc) was throwing errors.

Screen Shot 2021-12-20 at 10 26 54 PM

Currently, the logic for detecting if you can spy on a method/value is based on if you can get the object's descriptor.

This isn't accessible via document.head's own property descriptor. Other libraries don't rely on getting the descriptor and instead always fallback to simply looking up the value via document.head['appendChild'] (this is what nanospy does) or something similar (Sinon's implementation is a bit more complex, but it falls back to looking up the property directly).

I put a mini-repro together here. You can open it via yarn vite.

Full documentation

  • - spies
  • - getInternalState
  • - spy
  • - spyOn
  • - createInternalSpy
  • - internalSpyOn
  • - restoreAll

SpyImpl

  • - SpyImpl.getOriginal
  • - SpyImpl.willCall
  • - SpyImpl.restore

Spy

  • - Spy.returns
  • - Spy.length
  • - Spy.nextError
  • - Spy.nextResult

SpyInternalState

  • - SpyInternalState.called
  • - SpyInternalState.callCount
  • - SpyInternalState.calls
  • - SpyInternalState.results
  • - SpyInternalState.reset
  • - SpyInternalState.impl
  • - SpyInternalState.next

The `new.target` is undefined even if spy is created with new keyword

Describe the bug

The new.target is undefined if spy is created with new keyword.

From MDN for new.target

The new.target meta-property lets you detect whether a function or constructor was called using the new operator. In constructors and functions invoked using the new operator, new.target returns a reference to the constructor or function that new was called upon. In normal function calls, new.target is undefined.

Steps to reproduce

const fn = {
  fnFunc: () => {},
  fnClass: class FnClass {},
};

spyOn(fn, "fnFunc").willCall(function () {
  console.log(`Called using ${new.target ? "new operator" : "function call"}`);
});
spyOn(fn, "fnClass").willCall(function () {
  console.log(`Called using ${new.target ? "new operator" : "function call"}`);
  return fn.fnClass;
});

fn.fnFunc(); // Called using function call
new fn.fnClass(); // Called using function call

Observed behavior

Called using function call
Called using function call

Expected behavior

Called using function call
Called using new operator

Additional context

Noticed while debugging an issue in vitest at vitest-dev/vitest#2821 (comment)

License info missing

Hi,

Please add to github what license this library is published with and add a LICENSE filt to the repo. Thanks.

Make types work

We have an issue with spyOn types

It should have types with the description:

  • if methodName is string, then treat obj[methodName] as function and return Spy<Arguments, ReturnType>
  • if methodName is object { getter: keyof obj }, then treat obj[methodName.getter] as the getter and return Spy<[], GetterType>
  • if methodName is object { setter: keyof obj }, then treat obj[methodName.setter] as the setter and return Spy<GetterArgs, []>

Do not bundle source code before publishing

Is your feature request related to a problem? Please describe.

Tinyspy library bundles using tsup before publishing.

The package.json scripts for reference

tinyspy/package.json

Lines 7 to 9 in 0ac4b76

"build": "tsup",
"prepare": "husky install",
"publish": "npm run build && clean-publish",

This reduces the install size of the published package, but affects readability and usability significantly.

$ tinyspy> pnpm build

$ tinyspy> du -sh dist 
 12K    dist

$ tinyspy> ls dist 
index.cjs  index.d.ts index.js

$ tinyspy> ./node_modules/.bin/tsc -p tsconfig.json --outDir dist

$ tinyspy> du -sh dist
 20K    dist

$ tinyspy> ls dist
index.js      restoreAll.js spy.js        spyOn.js      utils.js

For example, I was debugging a fix for vitest-dev/vitest#2821, and noticed that tinyspy source code is bundled, making it difficult to read through.

Describe the solution you'd like

Just transpile the tinyspy package before publishing, so that it's still readable for someone debugging through the source code.

Describe alternatives you've considered

Look at the source code on GitHub, and attempt to guess equivalent code in bundle

Spy behaves badly when interacting with promise-like return values

I'm trying to port mockingoose over to Vitest (which uses tinyspy), and I have run into two issues, both of which are directly due to the way tinyspy handles promise-like return values:


Firstly, this code:

mongoose.createConnection = vitest.fn().mockReturnValue({
  catch() {
    /* no op */
  },
  model: mongoose.model.bind(mongoose),
  on: vitest.fn(),
  once: vitest.fn(),
  then(resolve) {
    return Promise.resolve(resolve(this));
  },
});

When mongoose.createConnection() is called it will hang forever. This is because of this line:

.then((r: any) => (resultTuple[1] = r))

which means that return Promise.resolve(resolve(this)) effectively becomes await this, which is an infinite await loop.


Secondly, mongoose Query objects are not promises, but they do have a then function. This function commits the query and awaits the result from the database. The issue is that by calling then, again in the same place:

.then((r: any) => (resultTuple[1] = r))

tinyspy is inadvertently committing the query well before it should actually be executed, which results in bad behavior. Specifically, this code:

SomeModel.findOne({ id: someId }).populate('someField').exec();

fails with the error ".populate() isn't a function", because by the time fineOne is finished executing it has already committed the query, even though it shouldn't commit until the call to exec().


Here's a code sandbox with an example of this issue that only uses tinyspy.

I'm not sure what the best solution to this issue is. If the promise resolution code is removed it will fix these issues I've been having, but it was clearly added for a reason. But if it is left in as-is then this code breaks. I'm not a huge fan of adding options or config flags to this library, but I'm struggling to find another option for this issue.

Docs

We need docs

  • mentioning forking of nanospy and vitest and describing why we decided to create our own package,
  • explaining the API
  • having a few examples

Add asserts before mocking

If you are new to stubbing it's hard to find if you get an error or when something is not working as it should, so I think we should prevent these user mistakes by throwing errors before creating a mock

Example from jest spyOn
https://github.com/facebook/jest/blob/main/packages/jest-mock/src/index.ts#L1091

I think the most byteless method would be to create a wrapper

function assert(assertion: boolean, message: string) {
   if(!assertion)
      throw new Error(message)
}

assert(descriptor.configurable, `${propertyName} is not declared configurable`)

Question about changelogithub dep

Hello!
Such a great project!

I have a question. Is changelogithub dep supposed to be a direct dependency for this lib?
Actual release had bloated my project deps a bit ๐Ÿ˜„

Thank you!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.