Desktop Material

Crafting Git Tests

This document outlines the options available for creating tests around Git repositories and data.

Repository Fixtures

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:

Importing a Git repository

The manual steps to add a repository fixture for testing are:

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
})

Repository Scaffolding

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:

This 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)
    })
  })
})

Additional Git operations

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')
})