mirror of
https://github.com/desktop/desktop
synced 2024-10-31 11:07:25 +00:00
ea294ed2f5
Programatically to programmatically.
180 lines
6.1 KiB
Markdown
180 lines
6.1 KiB
Markdown
# 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:
|
|
|
|
- capturing the whole repository state (including working directory and
|
|
config), such as a regression test
|
|
- the manual work to create the repository from scratch is not worth performing
|
|
on every test run
|
|
|
|
There are some downsides to this approach:
|
|
|
|
- the entire repository is committed within the Desktop repository, so large
|
|
repositories are not suitable to be imported with this approach
|
|
- there is automated tooling for importing a repository currently
|
|
- updating a repository if a test case changes is not supported - you'll need
|
|
to do the import again with the new state
|
|
|
|
### Importing a Git repository
|
|
|
|
The manual steps to add a repository fixture for testing are:
|
|
|
|
- inside the Git repository you want to import, rename all `.git` directories
|
|
to `_git`
|
|
- create a new folder inside `app/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.
|
|
- copy the repository contents into `app/test/fixtures/{your-new-folder}`
|
|
- cleanup any `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`:
|
|
|
|
```ts
|
|
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:
|
|
|
|
- a baseline repository can differ slightly between tests, and it's easier to
|
|
programmatically apply the changes than create multiple test fixtures
|
|
- the workflows being developed may change over time, and the tests themselves
|
|
should be flexible (and be easy to identify how they evolve)
|
|
|
|
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 network
|
|
- `makeCommit` - express the changes to the working directory that should be committed
|
|
- `switchTo` - a quick way to checkout (and create if needed) a branch in the
|
|
repository
|
|
|
|
This is an example test for `pull` behaviour which uses this scaffolding:
|
|
|
|
```ts
|
|
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:
|
|
|
|
- the Git command line has lots of detailed documentation and options, which we
|
|
can use without needing to add this behaviour to Desktop's Git APIs
|
|
- test setup should not be coupled to application behaviour, in case that
|
|
application behaviour can change in the future
|
|
- by making the Git test setup explicit, we can focus on what Git operation is
|
|
being tested
|
|
|
|
This is an example of how we use `GitProcess.exec` in our tests:
|
|
|
|
```ts
|
|
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 GitProcess.exec(['remote', 'add', 'X', url], repository.path)
|
|
await GitProcess.exec(['remote', 'add', 'A', url], repository.path)
|
|
await GitProcess.exec(['remote', 'add', 'L', url], repository.path)
|
|
await GitProcess.exec(['remote', 'add', 'T', url], repository.path)
|
|
await GitProcess.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')
|
|
})
|
|
```
|