diff --git a/docs/customization/testing_plugins.md b/docs/customization/testing_plugins.md new file mode 100644 index 000000000..369156631 --- /dev/null +++ b/docs/customization/testing_plugins.md @@ -0,0 +1,156 @@ +# Testing your Commitizen plugin + +Adding a test suite to your plugin helps prevent accidental regressions when you update +commit rules, regex patterns, or changelog templates. This guide shows how to test the +most common plugin behaviors using [pytest](https://docs.pytest.org/). + +## Setup + +Install the testing dependencies in your plugin project: + +```bash +pip install commitizen pytest +``` + +## Testing commit message rules + +### Testing `bump_pattern` and `bump_map` + +Use `commitizen.bump.find_increment` to verify that your regex correctly maps commit +messages to the expected version increment (`MAJOR`, `MINOR`, `PATCH`, or `None`). + +```python title="tests/test_my_plugin.py" +import pytest +from commitizen import bump +from commitizen.git import GitCommit + +from my_plugin import MyCommitizen # replace with your plugin import + + +def make_commits(*messages: str) -> list[GitCommit]: + return [GitCommit(rev="abc123", title=msg) for msg in messages] + + +@pytest.mark.parametrize( + ("messages", "expected"), + [ + # patch — bug fixes should produce a PATCH bump + (["fix: correct off-by-one error"], "PATCH"), + # minor — new features should produce a MINOR bump + (["feat: add dark mode"], "MINOR"), + # major — breaking changes should produce a MAJOR bump + (["feat!: rename public API"], "MAJOR"), + # no relevant commits — no bump + (["chore: update CI config"], None), + ], +) +def test_bump_increment(messages, expected): + commits = make_commits(*messages) + result = bump.find_increment( + commits, + regex=MyCommitizen.bump_pattern, + increments_map=MyCommitizen.bump_map, + ) + assert result == expected +``` + +### Testing `changelog_pattern` and `commit_parser` + +Verify that only relevant commits appear in the changelog and that the parser +extracts fields (type, scope, message) correctly. + +```python title="tests/test_changelog_rules.py" +import re + +from my_plugin import MyCommitizen + + +@pytest.mark.parametrize( + ("message", "should_match"), + [ + ("feat(api): add pagination", True), + ("fix: handle null pointer", True), + ("chore: bump dev dependency", False), + ("docs: update README", False), + ], +) +def test_changelog_pattern(message, should_match): + pattern = re.compile(MyCommitizen.changelog_pattern) + assert bool(pattern.match(message)) is should_match + + +@pytest.mark.parametrize( + ("message", "expected_groups"), + [ + ( + "feat(api): add pagination", + {"change_type": "feat", "scope": "api", "message": "add pagination"}, + ), + ( + "fix: handle null pointer", + {"change_type": "fix", "scope": None, "message": "handle null pointer"}, + ), + ], +) +def test_commit_parser(message, expected_groups): + pattern = re.compile(MyCommitizen.commit_parser) + match = pattern.match(message) + assert match is not None + for key, value in expected_groups.items(): + assert match.group(key) == value +``` + +### Testing `message()` output + +Ensure your `message()` method produces the correct commit string from user answers. + +```python title="tests/test_message.py" +from commitizen.config import BaseConfig + +from my_plugin import MyCommitizen + + +def test_message_with_scope(): + cz = MyCommitizen(BaseConfig()) + msg = cz.message({"type": "feat", "scope": "api", "subject": "add pagination"}) + assert msg == "feat(api): add pagination" + + +def test_message_without_scope(): + cz = MyCommitizen(BaseConfig()) + msg = cz.message({"type": "fix", "scope": "", "subject": "handle null pointer"}) + assert msg == "fix: handle null pointer" +``` + +## Testing schema validation + +If your plugin overrides `schema_pattern()`, test that valid and invalid commit +messages are accepted and rejected as expected. + +```python title="tests/test_schema.py" +import re + +from my_plugin import MyCommitizen + + +def test_valid_commit_passes_schema(): + pattern = re.compile(MyCommitizen().schema_pattern()) + assert pattern.match("feat(api): add pagination") + + +def test_invalid_commit_fails_schema(): + pattern = re.compile(MyCommitizen().schema_pattern()) + assert not pattern.match("random text without type") +``` + +## Running the tests + +```bash +pytest tests/ -v +``` + +## See also + +- [Customizing through a Python class](python_class.md) — full plugin API reference +- [Third-Party Commitizen Plugins](../third-party-plugins/about.md) — examples of published plugins +- [Commitizen's own test suite](https://github.com/commitizen-tools/commitizen/tree/master/tests) — for more advanced testing patterns diff --git a/docs/faq.md b/docs/faq.md index 568ec2fbe..89e1a0f6c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -76,6 +76,50 @@ legacy_tag_formats = [ ] ``` +## A dependency has the same version as my project — how do I prevent it from being bumped? + +When using [`version_files`](config/bump.md#version_files) to track your project version, +Commitizen searches for the current version string and replaces it with the new one. +If a dependency in the same file happens to share the exact same version number, it will +also be updated, which is usually undesirable. + +There are two ways to avoid this: + +### Option 1 — Anchor the pattern with `^` + +Prefix the file entry with `^` to match only lines that *start* with `version`: + +```toml +[tool.commitizen] +version_files = ["pyproject.toml:^version"] +``` + +This ensures only lines like `version = "1.2.3"` are matched, not dependency +specifications that happen to contain the same version string. + +### Option 2 (recommended) — Use a `version_provider` + +Instead of `version_files`, use the appropriate +[`version_provider`](config/version_provider.md) for your project type. Commitizen +will then update exactly the right field without any regex-based text replacement. + +For example, if you use `pyproject.toml` with a `[project]` table (PEP 621): + +```toml +[tool.commitizen] +version_provider = "pep621" +``` + +Or for Poetry users: + +```toml +[tool.commitizen] +version_provider = "poetry" +``` + +See the [version providers reference](config/version_provider.md) for all available +options. + ## How to avoid warnings for expected non-version tags? You can explicitly ignore them with [`ignored_tag_formats`](config/bump.md#ignored_tag_formats). diff --git a/mkdocs.yml b/mkdocs.yml index 21d6376c5..852eedbc0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,6 +59,7 @@ nav: - Advanced Customization: - Customize via config file: "customization/config_file.md" - Customized Python Class: "customization/python_class.md" + - Testing Plugins: "customization/testing_plugins.md" - Changelog Template: "customization/changelog_template.md" - Tutorials: - Commit Message Best Practices: "tutorials/writing_commits.md"