git-js icon indicating copy to clipboard operation
git-js copied to clipboard

unit-testing git.js

Open gamegee opened this issue 7 years ago • 9 comments

Hi,

Is there a way to test git with mocks? For example I would like to unit test:

  • A git init command (can be done using mock-fs)
  • A new commit (can be done using mock-fs)
  • A push command (require a fake repo)

One way to do this, could be to mock the HTTP request, or to stub the push method. Someone has an idea?

gamegee avatar Jun 19 '17 15:06 gamegee

Are you asking about how to test a library that depends on simple-git or how to to test simple-git itself?

In the unit tests for simple-git there is a mocked version of Buffer and ChildProcess to fake the values the library sees. The library doesn't expose any way to configure these dependencies from outside of the library for the purposes of testing other projects depending on simple-git. In this scenario I would recommend injecting the simple-git instance into your class/function as an argument rather than using require in the module itself, for example instead of:

// foo.js
const git = require('simple-git/promise');

module.exports = function (path) {
  return git(path).pull();
};

Switch to something like:

// foo.js

module.exports = function (git) {
  return function (path) {
    return git(path).pull();
  };
}

There are a number of libraries available that can help with dependency injection - the best search would be https://www.npmjs.com/browse/keyword/IoC

steveukx avatar Jun 22 '17 19:06 steveukx

Yes sorry I mean test a module that depend on git-js. I use dependency injection, and would like to test a wrapper that use git-js as a dependency. Here's a simple example of a function using git module:

function GitWrapper(opts) {
  this.client = opts.git;
}


GitWrapper.prototype.initialPush = function(remoteUrl, remoteName, localPath, cb) {
  let git = this.client(localPath)
  git.init(0)
  .commit('initial commit')
  .addRemote(remoteUrl, remoteName)
  .push(remoteName, function(err) {
    cb();
  });
}

I guess you've already tested your library, but I wanted to know if there is an in memory mode or if mocks exist for each of the methods. I could also stubs the methods myself.

Anyway thanks for that so useful module :) When will the 2.0 be available (with promise support)?

gamegee avatar Jun 25 '17 21:06 gamegee

Same question here. I have a script that performs either a push or a clone depending on an algorithm, and I'd like to unit-test it. The regular way would be to stub the clone method, but I couldn't get it to work with sinonjs. Tried to stub on the prototype, etc, but I could not get it to work, as simple-git itself starts with a new.

Treyone avatar Nov 14 '17 09:11 Treyone

The ideal way to test your code's use of this library is to unit test a function that is passed a simple-git rather than one that explicitly knows how to instantiate simple-git, for example:

const git = require('simple-git/promise');

function whichBranch (dir) {
   return git(dir).branch().then(summary => summary.current);
}

becomes:

function whichBranch (git, dir) {
   return git(dir).branch().then(summary => summary.current);
}

or where that adds too much overhead you could use a "libraries" / "util" import:

// in libs.js you would have
module.exports.git = require('simple-git');

// in the file to be tested you would replace the actual require with:
const { git } = require('./libs');
...

If you really need to stub the prototype functions of the simple-git library, you can also use (assumes jasmine for spies):

const gitProto = require('simple-git/src/git').prototype;

describe(() => {
  let pushSpy;
  beforeEach(() => {
    pushSpy = spyOn(gitProto, 'push');
  });

  it('pushes', () => {
    pushSpy.and.returnValue(Promise.resolve({ ... }));
    //... your test code goes here knowing calls to git().push(...) will return the resolved promise
  })
})

steveukx avatar Nov 15 '17 21:11 steveukx

I can't inject, because my lib needs to instantiate multiple and unpredictable times, but I didn't try to stub on this prototype, I'll definitely do that. Thanks a lot !

Treyone avatar Nov 16 '17 14:11 Treyone

Additional difficulty, I'm using clone as a promise. Considering the way you create them on the fly in promise.js, stubbing the prototype does not seem to be an option. Did you already have this case ?

Treyone avatar Nov 16 '17 15:11 Treyone

I would recommend injecting a factory function to the library so that you can make use of it as many times as you need but fully replace it with a mock when testing.

module.exports = MyApp;

function MyApp () {
   var gitP = require('simple-git/promise');

   this.clone = function (where) {
      return gitP().clone(where);
   }
}

becomes:

module.exports = MyApp;

function MyApp (gitP) {
   this.clone = function (where) {
      return gitP().clone(where);
   }
}

Dependency injection in this way removes the need for your class/function under test from also knowing how to create its own dependency chain.

A fairly ugly alternative is to simply attach the simple-git/promise function to a common utility that is configured to be a mock function in your tests.

steveukx avatar Nov 16 '17 15:11 steveukx

This is a old thread, but at this moment, I have to struggle to make a unit test for a function that use several simple-git capabilities. I use proxyquire to create a stubbed version of my module, and replace the imports of simple-git/promise with my custom stubs. Basically, this is the way that I use to test my module mocking simple-git:

// myModule.js
const git = require('simple-git/promise');

async function doSomethingWithSimpleGit() {
    await git().clone('some-remote-repo');
    
    return {};
}
// myModule.test.js
const { assert } = require('chai');
const proxyquire = require('proxyquire');

describe('my test', () => {
    let result;

    before(async () => {
      const myStubbedModule = proxyquire('./myModule', {
        'simple-git/promise': () => ({
          clone: () => Promise.resolve(),
        })
      });

      result = await myStubbedModule.doSomethingWithSimpleGit();
    });

    it('should return something', () => assert.deepEqual(result, {}));
  });
})

sagrath23 avatar May 08 '20 23:05 sagrath23

I ended using the dependency injection approach as others solutions were not working for me. I also find it to give me more granular control, as I wanted to test both the happy path and the failing scenario.

Below are the tests and code for reference.

versioning.test.ts

Details
import simpleGit, { SimpleGit } from 'simple-git';
import Versioning from '../versioning';

describe('Verify Versioning methods', () => {
  let git: SimpleGit;
  beforeAll(() => {
    git = simpleGit();
    // we assume we are in a git repository
  });

  it('getRevision(): should return HEAD revision', async () => {
    const ABBREVIATED_SHA_LENGTH = 7;

    const revision = await Versioning.getRevision(git);

    expect(revision.length).toBe(ABBREVIATED_SHA_LENGTH);
  });

  it('getBranch(): should return HEAD revision', async () => {
    const branchName = await Versioning.getBranch(git);

    const EMPTY = 0;
    expect(branchName.length).toBeGreaterThan(EMPTY);
  });

  describe('Errors on failure', () => {
    let mockGit;
    beforeAll(() => {
      mockGit = {
        branch: jest.fn().mockImplementation(() => {
          throw new Error('mock branch() implementation');
        }),
      };
    });
    it('getRevision(): should throw error about repo existence', () => {
      expect(async () => {
        await Versioning.getRevision(mockGit);
      }).rejects.toThrowError(Versioning.EXCEPTIONS.NOT_A_GIT_REPO_ERROR);
    });

    it('getBranch(): should throw error about branch', () => {
      expect(async () => {
        await Versioning.getBranch(mockGit);
      }).rejects.toThrowError(Versioning.EXCEPTIONS.NO_GIT_BRANCH_ERROR);
    });
  });
});

versioning.ts

Details
const EXCEPTIONS = {
  NO_GIT_BRANCH_ERROR: "Couldn't find a git branch, is this a git directory?",
  NOT_A_GIT_REPO_ERROR:
    "Couldn't find a git commit hash, is this a git directory?",
};

type ShortSHA = string;
const getRevision = async (git): Promise<ShortSHA> => {
  try {
    return await git.revparse(['--short', 'HEAD']);
  } catch (error) {
    throw new TypeError(EXCEPTIONS.NOT_A_GIT_REPO_ERROR);
  }
};

type BranchName = string;
const getBranch = async (git): Promise<BranchName> => {
  try {
    return (await git.branch()).current as BranchName;
  } catch (error) {
    throw new TypeError(EXCEPTIONS.NO_GIT_BRANCH_ERROR);
  }
};

export default {
  EXCEPTIONS,
  getBranch,
  getRevision,
};

edouard-lopez avatar Jan 03 '22 14:01 edouard-lopez