git-js
git-js copied to clipboard
unit-testing git.js
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?
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
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)?
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
.
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
})
})
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 !
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 ?
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.
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, {}));
});
})
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,
};