Help comes from knowledge of the git
protocols
Actually, you don't need to mock anything. You can test all the functionality, i.e. local git operations, like: add, commit, checkout, rebase or cherry-pick and remote operations, like fetch, push or pull without setting up a git server.
Local and remote repositories exchange data using one of those protocols:
local protocol
(no network used, in unixes it's denoted as file://
)
http(s)://
protocol
ssh://
protocol
git://
protocol
More on this in git protocols explained
Creating test environment for git operations
The local protocol
is being used - when you specify path to other repository in the local file system. So the only thing you have to do in order to perform tests in a clean and isolated environment - is to arrange two repositories. The convention is to set up the "remote" as bare repository
.
Then the other should have upstream set by path to the first one, and voilà!
Since now, you have fully functional test setup. Say thanks to Linus Torvalds.
Pytest implementation of fake git repository fixture
This uses a built-in tmp_path
fixture that takes care of creating (and cleaning up) the repositories in a temporary folder, in my case it's in:
/tmp/pytest-of-mikaelblomkvistsson/pytest-current/test_preparing_repo0
import datetime
from pathlib import Path
import pytest
from git import Actor, Remote, Repo
@pytest.fixture
def fake_repo(tmp_path) -> "Helper":
return Helper(tmp_path)
class Helper:
"""The main purpose of defining it as a class is to gather all the variables
under one namespace, so that we don't need to define 6 separate pytest fixtures.
You don't need git server to test pull/push operations. Since git offers
"local protocol" - plain bare repository in your filesystem is fully
compatible with http(s), ssh and git protocol (Neglecting authentication functionality).
"""
def __init__(self, tmp_path_fixture: Path):
self.local_repo_path: Path = tmp_path_fixture / "local-repo"
remote_repo_path: Path = tmp_path_fixture / "remote-repo"
remote_repo_path.mkdir()
self.remote_repo: Repo = Repo.init(str(remote_repo_path), bare=True)
self.repo: Repo = Repo.init(str(self.local_repo_path))
self.remote_obj: Remote = self.repo.create_remote("origin", str(remote_repo_path))
# do initial commit on origin
commit_date = self.tz_datetime(2023, 10, 1, 11, 12, 13)
self.repo.index.commit("Initial commit", author_date=commit_date, commit_date=commit_date)
self.remote_obj.push("master")
def local_graph(self) -> str:
return self.repo.git.log("--graph --decorate --pretty=oneline --abbrev-commit".split())
@classmethod
def tz_datetime(cls, *args, **kwargs):
tz_info = datetime.datetime.utcnow().astimezone().tzinfo
return datetime.datetime(*args, **kwargs, tzinfo=tz_info)
def do_commit(self, *files_to_add, msg: str = "Sample commit message.", author: str = "author") -> None:
author = Actor(author, f"{author}@example.com")
# constant date helps to make git hashes reproducible, since the date affects commit sha value
date = self.tz_datetime(2023, 10, 4, 15, 45, 13)
self.repo.index.add([str(file_) for file_ in files_to_add])
self.repo.index.commit(msg, author=author, committer=author, author_date=date, commit_date=date)
def test_preparing_repo(fake_repo):
file_1 = fake_repo.local_repo_path / "file_1.txt"
file_1.write_text("Initial file contents")
fake_repo.do_commit(file_1, msg="First commit.")
fake_repo.repo.git.checkout("-b", "new_branch")
file_1.write_text("Changed file contents")
fake_repo.do_commit(file_1, msg="Second commit.")
fake_repo.repo.git.checkout("-b", "other_branch")
file_1.write_text("Another change")
fake_repo.do_commit(file_1, msg="Change on other_branch.")
assert (
fake_repo.repo.git.branch("-a")
== """\
master
new_branch
* other_branch
remotes/origin/master"""
)
assert (
fake_repo.local_graph()
== """\
* 1743bd6 (HEAD -> other_branch) Change on other_branch.
* 2696781 (new_branch) Second commit.
* 5ea439d (master) First commit.
* 04fc02f (origin/master) Initial commit"""
)
Mock
- have you tried that? – Greensboro