docs/technical/crafting-git-tests.md
This document outlines the options available for creating tests around Git repositories and data.
The app/test/fixtures directory is a special folder on disk that can be used
to capture snapshots of a Git repository for testing. This is ideal for
situations where:
There are some downsides to this approach:
The manual steps to add a repository fixture for testing are:
.git directories
to _gitapp/test/fixtures named in a way that relates to
the tests being performed on it. The placeholder {your-new-folder}
in subsequent steps to represent whatever name you have chosen here.app/test/fixtures/{your-new-folder}app/test/fixtures/{your-new-folder}/_git/hooks/*.sample files
as these are not needed for testing (and will simplify the diff a bit)Once you have done that, in your test you need to use setupFixtureRepository
with the first parameter being the name of your test fixture folder.
This will return a temporary path to the repository, which is cleaned up after the test run is completed.
Here's an example of a test using setupFixtureRepository:
import {
setupFixtureRepository,
} from '../../helpers/repositories'
// ...
it('returns detached for arbitrary checkout', async () => {
const path = await setupFixtureRepository('{your-new-folder}')
const repository = new Repository(path, -1, null, false)
// act
// assert
})
The newer approach that we are experimenting with is to provide scaffolding APIs to declaratively get the repository into a state for testing. This approach is ideal for situations when:
Three patterns have been implemented to support workflows we are currently developing:
cloneRepository - make a copy of a test repository so that
push/pull/fetch can be emulated and tested without using the networkmakeCommit - express the changes to the working directory that should be committedswitchTo - a quick way to checkout (and create if needed) a branch in the
repositoryThis is an example test for pull behaviour which uses this scaffolding:
describe('ahead and behind of tracking branch', () => {
let repository: Repository
beforeEach(async () => {
const remoteRepository = await createRepository(featureBranch)
repository = await cloneRepository(remoteRepository)
// make a commits to both remote and local so histories diverge
const changesForRemoteRepository = {
commitMessage: 'Changed a file in the remote repository',
entries: [
{
path: 'README.md',
contents: '# HELLO WORLD! \n WORDS GO HERE! \nLOL',
},
],
}
await makeCommit(remoteRepository, changesForRemoteRepository)
const changesForLocalRepository = {
commitMessage: 'Added a new file to the local repository',
entries: [
{
path: 'CONTRIBUTING.md',
contents: '# HELLO WORLD! \nTHINGS GO HERE\nYES, THINGS',
},
],
}
await makeCommit(repository, changesForLocalRepository)
await fetch(repository, null, origin)
})
describe('by default', () => {
let previousTip: Commit
let newTip: Commit
beforeEach(async () => {
previousTip = await getTipOrError(repository)
await pull(repository, null, origin)
newTip = await getTipOrError(repository)
})
it('creates a merge commit', async () => {
const newTip = await getTipOrError(repository)
expect(newTip.sha).not.toBe(previousTip.sha)
expect(newTip.parentSHAs).toHaveLength(2)
})
})
})
Once you have a Repository initialized in a test, if you need to run
additional Git commands on the repository it is recommended to use
GitProcess.exec from dugite. We recommend this approach over reusing the Git
APIs created for Desktop in app/src/lib/git for a few reasons:
This is an example of how we use GitProcess.exec in our tests:
it('returns remotes sorted alphabetically', async () => {
const repository = await setupEmptyRepository()
// adding these remotes out-of-order to test how they are then retrieved
const url = 'https://github.com/desktop/not-found.git'
await exec(['remote', 'add', 'X', url], repository.path)
await exec(['remote', 'add', 'A', url], repository.path)
await exec(['remote', 'add', 'L', url], repository.path)
await exec(['remote', 'add', 'T', url], repository.path)
await exec(['remote', 'add', 'D', url], repository.path)
const result = await getRemotes(repository)
expect(result).toHaveLength(5)
expect(result[0].name).toEqual('A')
expect(result[1].name).toEqual('D')
expect(result[2].name).toEqual('L')
expect(result[3].name).toEqual('T')
expect(result[4].name).toEqual('X')
})