我有一个 python 项目,它使用 GitPython 对远程 Git 存储库执行克隆和拉取功能。
举个简单的例子:
import git
from git import Git
from git import Repo
def clone_and_checkout(full_dir, git_url, repo_ver):
repo = Repo.clone_from(
url=git_url,
to_path=full_dir
)
# Trigger re-create if repository is bare
if repo.bare:
raise git.exc.InvalidGitRepositoryError
# Set origin and pull
origin = repo.remotes.origin
origin.pull()
# Check out desired version of repository
g = Git(full_dir)
g.checkout(repo_ver)
我希望能够为此功能编写单元测试,但显然这需要接触当前的外部系统。
我很好奇是否有人有模拟这种外部交互的经验,就像使用 Mock 模拟 HTTP 调用一样。我希望能够以一种可以在测试时模拟的方式执行这些任务,而无需调用实际的 Git 远程。
我应该如何为此编写测试?
编辑:为了更清楚地了解我的要求,我应该提到我是 Mock 的新手,并且正在努力理解如何 Mock 这些类的实例而不是类本身。我的问题应该表述得更好 - 类似于“如何使用 Mock 来设置实例特定的属性,例如裸露?”
我已经了解了很多关于 Mock 的知识,并且已经弄清楚如何做到这一点,所以我将为我自己的问题提供答案。
这似乎是对 Mock 理解不完全,或者使用 Patch 方法的常见结果。
要做的第一件事是阅读模拟文档中的“修补位置”部分。有了这些信息,您应该能够使用 patch 函数来模拟上述函数中使用的 GitPython 对象。这些装饰器将出现在您的单元测试函数上方。
@mock.patch('gitter.Repo')
@mock.patch('gitter.Git')
为了为这些模拟对象之一的实例提供返回值,您可以使用 PropertyMock。这是利用此功能的单元测试的完整示例:
import gitter # file containing our clone function
import mock
import unittest
class test_gitter(unittest.TestCase):
@mock.patch('gitter.Repo')
@mock.patch('gitter.Git')
def runTest(self, mock_git, mock_repo):
# Set the "bare" attribute of the Repo instance to be False
p = mock.PropertyMock(return_value=False)
type(mock_repo.clone_from.return_value).bare = p
gitter.clone_and_checkout(
'/tmp/docker',
'[email protected]:docker/docker.git',
'master'
)
mock_git.checkout.called_once_with('master')
git
协议知识其实,你不需要嘲笑任何东西。您可以测试所有功能,即本地 git 操作,例如:添加、提交、签出、rebase 或cherry-pick 以及远程操作,例如 fetch、push 或 pull,而无需设置 git 服务器。
本地和远程存储库使用这些协议之一交换数据:
local protocol
(不使用网络,在unix中表示为file://
)http(s)://
协议ssh://
协议git://
协议更多相关信息请参见 git 协议解释
当您指定本地文件系统中其他存储库的路径时,正在使用
local protocol
。因此,为了在干净且隔离的环境中执行测试,您唯一要做的就是安排两个存储库。惯例是将“遥控器”设置为bare repository
。
然后另一个应该通过到第一个的路径设置上游,瞧!
从现在起,您就拥有了功能齐全的测试设置。对 Linus Torvalds 表示感谢。
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"""
)