Chris Hansen
Chris Hansen

Reputation: 8635

How to stub function that returns a promise?

I'm trying to stub a function using sinon. The function has the following signature

export function getIndexDocument(
    svc: MetaHTTPService | ServiceConfig
): MetaPromise<RepoResponseResult<IndexDocument>> {

Is this the right way to sub it

sandbox.stub(getIndexDocument).resolves({} as RepoResponseResult)

I tried that but it returns an error.

Here's how this function is called.

I have a class called AssetsController with the following functions

 public async exploreIndexDocument(): Promise<Asset | undefined> {      
    // it makes an HTTP request and returns a promise that resolves with the following info { repoId: "", assetId: "" }

     const {
            result: { assignedDirectories }
     } = await getIndexDocument(this.serviceConfig).catch(err => {
        throw new Error(`Bad repsonse`);
     });

     return {
        repoId: result.repoId;
        assetId: result.assetId
     }

}

public async function copyAsset(asset) {
   const res = await this.exploreIndexDocument();
   const repoId = res.repoId;
   return asset.copy(repoId);
}

I'm trying to test the function copyAsset, but it calls exploreIndexDocument which calls getIndexDocument. getIndexDocument is imported at the top of the file and lives in the module @ma/http. getIndexDocument makes an HTTP request.

How can I test copyAsset given that it calls getIndexDocument which makes an HTTP request?

Upvotes: 1

Views: 2360

Answers (2)

Brenden
Brenden

Reputation: 2097

I think most of your problems can be solved by revisiting your architecture. For example, instead of creating an explicit dependency on getIndexDocument within your AssetController class you can simply inject it in. This will allow you to swap implementations depending on the context.

type IndexDocumentProvider = (svc: MetaHTTPService | ServiceConfig) => MetaPromise<RepoResponseResult<IndexDocument>>;

interface AssetControllerOptions {
  indexDocumentProvider: IndexDocumentProvider
}

class AssetController {
  private _getIndexDocument: IndexDocumentProvider;

  public constructor(options: AssetControllerOptions) {
    this._getIndexDocument = options.indexDocumentProvider;
  }
}

Then you can use this._getIndexDocument wherever and not worry about how to make the original implementation behave like you want in your tests. You can simply provide an implementation that does whatever you'd like.

describe('copyAsset', () => {
  it('fails on index document error.', () => {
    const controller = new AssetController({
      indexDocumentProvider: () => Promise.reject(new Error(':('));
    });
    ....
  });

  it('copies asset using repo id.', () => {
    const controller = new AssetController({
      indexDocumentProvider: () => Promise.resolve({ repoId: "420" })
    });
    ...
  });
});

You can obviously use stubs instead of just functions or whatever if you need something fancy.

Above we removed an explicit dependency to an implementation and instead replaced it with a contract that must be provided to the controller. The is typically called Inversion of Control and Dependency Injection

Upvotes: 1

Alex Wayne
Alex Wayne

Reputation: 186994

According to the docs, you can't stub an existing function.

You can:

// Create an anonymous sstub function
var stub = sinon.stub();

// Replaces object.method with a stub function. An exception is thrown
// if the property is not already a function.
var stub = sinon.stub(object, "method");

// Stubs all the object’s methods.
var stub = sinon.stub(obj);

What you can't do is stub just a function like:

var stub = sinon.stub(myFunctionHere);

This makes sense because if all you have is a reference to a function, then you can just create a new function to use instead, and then pass that into where ever your test needs it to go.


I think you just want:

const myStub = sandbox.stub().resolves({} as RepoResponseResult)

In your update it sounds like you want to put the stub on the AssetsController class. See this answer for more info on that, but in this case I think you want:

const myStub = sandbox
  .stub(AssetsController.prototype, 'exploreIndexDocument')
  .resolves({} as RepoResponseResult)

Now anytime an instance of AssetsController calls its exploreIndexDocument method, the stub should be used instead.

Playground

Upvotes: 1

Related Questions