Search code examples
javascripttypescriptmockingoverloadingsinon

Mocking library function when it has the same namespace name


Similar to a previous question, I am trying to mock an external library using sinon. However, the library exports two functions and a namespace using the same name FastGlob.

I have a basic understanding of function overloading but i'm not sure how namespaces work with function overloading or if this issue is even related.

Regardless, I want to mock the first function definition but sinon is seeing the namespace

declare function FastGlob(source: PatternInternal | PatternInternal[], options: OptionsInternal & EntryObjectPredicate): Promise<EntryInternal[]>;

Here is the libraries definition file

import { Options as OptionsInternal } from './settings';
import { Entry as EntryInternal, FileSystemAdapter as FileSystemAdapterInternal, Pattern as PatternInternal } from './types';

declare function FastGlob(source: PatternInternal | PatternInternal[], options: OptionsInternal & EntryObjectPredicate): Promise<EntryInternal[]>;
declare function FastGlob(source: PatternInternal | PatternInternal[], options?: OptionsInternal): Promise<string[]>;
declare namespace FastGlob {
    type Options = OptionsInternal;
    type Entry = EntryInternal;
    type Task = taskManager.Task;
    type Pattern = PatternInternal;
    type FileSystemAdapter = FileSystemAdapterInternal;
    function sync(source: PatternInternal | PatternInternal[], options: OptionsInternal & EntryObjectPredicate): EntryInternal[];
    function sync(source: PatternInternal | PatternInternal[], options?: OptionsInternal): string[];
    function stream(source: PatternInternal | PatternInternal[], options?: OptionsInternal): NodeJS.ReadableStream;
    function generateTasks(source: PatternInternal | PatternInternal[], options?: OptionsInternal): Task[];
    function isDynamicPattern(source: PatternInternal, options?: OptionsInternal): boolean;
    function escapePath(source: PatternInternal): PatternInternal;
}
export = FastGlob;

I've tried using variations of the following test but TS complains it can only find the functions within the namespace (sync, stream, etc...). Removing the string name of the function causes a different issue.

import * as FastGlob from 'fast-glob';
import { stub, SinonStub } from "sinon";
import { Pattern, Entry, Options } from "fast-glob";

(stub(FastGlob, "FastGlob") as unknown as SinonStub<[s: Pattern | Pattern[], o: Options], Promise<Entry[]>>).resolves([{test: '/test/'} as unknown as Entry])

The application code is being used like so

import * as glob from 'fast-glob';
const paths: Array<string> = await glob('./my/glob/**/*.ts', { absolute: true });

Solution

  • You need additional module to stub fast-glob, because the way it defined. For more info, you can look at this sinon issue.

    I can give you example if you can use additional module: proxyquire.

    I have this glob.ts.

    // File: glob.ts
    import glob from 'fast-glob';
    
    
    async function getPaths(input: string): Promise<Array<glob.Entry|string>> {
      return glob(input, { absolute: true });
    }
    
    export { getPaths };
    

    Test using spec file:

    // File: glob.spec.ts
    import * as FastGlob from 'fast-glob';
    import sinon from 'sinon';
    import proxyquire from 'proxyquire';
    import { expect } from 'chai';
    
    describe('Glob', () => {
      const fakeInput = './node_modules/**/settings.js';
      it('getPaths using first fast-glob definition', async () => {
        const fakeResult = [{ test: '/test/' } as unknown as FastGlob.Entry];
        const fakeFunc = sinon.fake.resolves(fakeResult);
        // Create stub using proxyquire.
        const glob = proxyquire('./glob', {
          'fast-glob': sinon.fake.resolves(fakeResult),
        });
        const paths = await glob.getPaths(fakeInput);
        expect(paths).to.deep.equal(fakeResult);
        expect(fakeFunc.calledOnceWithExactly(fakeInput));
      })
    
      it('getPaths using second fast-glob definition', async () => {
        const fakeResult = ['/test/'];
        const fakeFunc = sinon.fake.resolves(fakeResult);
        // Create stub using proxyquire.
        const glob = proxyquire('./glob', {
          'fast-glob': sinon.fake.resolves(fakeResult),
        });
        const paths = await glob.getPaths(fakeInput);
        expect(paths).to.deep.equal(fakeResult);
        expect(fakeFunc.calledOnceWithExactly(fakeInput));
      })
    });
    

    When you run it using ts-mocha and nyc from terminal:

    $ npx nyc ts-mocha glob.spec.ts 
    
    
      Glob
        ✔ getPaths using first fast-glob definition (137ms)
        ✔ getPaths using second fast-glob definition
    
    
      2 passing (148ms)
    
    --------------|---------|----------|---------|---------|-------------------
    File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    --------------|---------|----------|---------|---------|-------------------
    All files     |     100 |      100 |     100 |     100 |                   
     glob.spec.ts |     100 |      100 |     100 |     100 |                   
     glob.ts      |     100 |      100 |     100 |     100 |                   
    --------------|---------|----------|---------|---------|-------------------