Add unit-tests
This commit is contained in:
23
tests/test_cli.py
Normal file
23
tests/test_cli.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import sys
|
||||||
|
from io import StringIO
|
||||||
|
from unittest import mock
|
||||||
|
from redmine_reporter.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.dict("os.environ", {
|
||||||
|
"REDMINE_URL": "https://red.eltex.loc/",
|
||||||
|
"REDMINE_USER": "x",
|
||||||
|
"REDMINE_PASSWORD": "y"
|
||||||
|
})
|
||||||
|
@mock.patch("redmine_reporter.client.fetch_issues_with_spent_time")
|
||||||
|
def test_cli_smoke(mock_fetch):
|
||||||
|
mock_fetch.return_value = []
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
sys.stdout = captured = StringIO()
|
||||||
|
try:
|
||||||
|
code = main(["--date", "2026-01-01--2026-01-31"])
|
||||||
|
assert code == 0
|
||||||
|
output = captured.getvalue()
|
||||||
|
assert "Total issues: 0" in output
|
||||||
|
finally:
|
||||||
|
sys.stdout = old_stdout
|
||||||
36
tests/test_client.py
Normal file
36
tests/test_client.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest import mock
|
||||||
|
from redmine_reporter.client import fetch_issues_with_spent_time
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("redmine_reporter.client.Redmine")
|
||||||
|
def test_fetch_issues_with_spent_time(mock_redmine_class):
|
||||||
|
# Подготовка моков
|
||||||
|
mock_redmine = mock_redmine_class.return_value
|
||||||
|
mock_user = mock.MagicMock()
|
||||||
|
mock_user.id = 123
|
||||||
|
mock_redmine.user.get.return_value = mock_user
|
||||||
|
|
||||||
|
# Два time entry на одну задачу
|
||||||
|
mock_entry1 = mock.MagicMock()
|
||||||
|
mock_entry1.issue.id = 101
|
||||||
|
mock_entry1.hours = 2.0
|
||||||
|
mock_entry2 = mock.MagicMock()
|
||||||
|
mock_entry2.issue.id = 101
|
||||||
|
mock_entry2.hours = 1.5
|
||||||
|
mock_redmine.time_entry.filter.return_value = [mock_entry1, mock_entry2]
|
||||||
|
|
||||||
|
# Мок задачи
|
||||||
|
mock_issue = mock.MagicMock()
|
||||||
|
mock_issue.id = 101
|
||||||
|
mock_issue.project = "Проект X"
|
||||||
|
mock_issue.subject = "Тестовая задача"
|
||||||
|
mock_issue.status = "New"
|
||||||
|
mock_redmine.issue.filter.return_value = [mock_issue]
|
||||||
|
|
||||||
|
result = fetch_issues_with_spent_time("2026-01-01", "2026-01-31")
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert len(result) == 1
|
||||||
|
issue, total_hours = result[0]
|
||||||
|
assert total_hours == 3.5
|
||||||
25
tests/test_config.py
Normal file
25
tests/test_config.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from unittest import mock
|
||||||
|
from redmine_reporter.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, {
|
||||||
|
"REDMINE_URL": "https://red.eltex.loc/",
|
||||||
|
"REDMINE_USER": "test",
|
||||||
|
"REDMINE_PASSWORD": "secret"
|
||||||
|
})
|
||||||
|
def test_config_valid():
|
||||||
|
Config.validate() # не должно быть исключения
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_config_missing():
|
||||||
|
with pytest.raises(ValueError, match="REDMINE_URL"):
|
||||||
|
Config.validate()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, {"REDMINE_AUTHOR": "Иванов И.И."})
|
||||||
|
def test_get_author():
|
||||||
|
assert Config.get_author("") == "Иванов И.И."
|
||||||
|
assert Config.get_author("Петров П.П.") == "Петров П.П."
|
||||||
44
tests/test_report_builder.py
Normal file
44
tests/test_report_builder.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import pytest
|
||||||
|
from redmine_reporter.report_builder import build_grouped_report, STATUS_TRANSLATION
|
||||||
|
from redmine_reporter.utils import get_version
|
||||||
|
|
||||||
|
|
||||||
|
class MockIssue:
|
||||||
|
def __init__(self, project, subject, status, fixed_version=None):
|
||||||
|
self.project = project
|
||||||
|
self.subject = subject
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
if fixed_version is not None:
|
||||||
|
self.fixed_version = fixed_version
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_translation():
|
||||||
|
assert STATUS_TRANSLATION["Closed"] == "Закрыто"
|
||||||
|
assert STATUS_TRANSLATION["New"] == "В работе"
|
||||||
|
assert STATUS_TRANSLATION["Resolved"] == "Решена"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_grouped_report():
|
||||||
|
issues = [
|
||||||
|
(MockIssue("Камеры", "Фича A", "New", "v2.5.0"), 2.0),
|
||||||
|
(MockIssue("Камеры", "Баг B", "Resolved", "v2.5.0"), 1.5),
|
||||||
|
(MockIssue("ПО", "Доки", "Pending", None), 4.0),
|
||||||
|
]
|
||||||
|
rows = build_grouped_report(issues)
|
||||||
|
|
||||||
|
assert len(rows) == 3
|
||||||
|
# Первая строка — полное название проекта и версии
|
||||||
|
assert rows[0]["display_project"] == "Камеры"
|
||||||
|
assert rows[0]["display_version"] == "v2.5.0"
|
||||||
|
# Вторая — пустые display_* из-за совпадения
|
||||||
|
assert rows[1]["display_project"] == ""
|
||||||
|
assert rows[1]["display_version"] == ""
|
||||||
|
# Третья — новый проект
|
||||||
|
assert rows[2]["display_project"] == "ПО"
|
||||||
|
assert rows[2]["display_version"] == "<N/A>"
|
||||||
|
|
||||||
|
# Проверка перевода и времени
|
||||||
|
assert rows[0]["status_ru"] == "В работе"
|
||||||
|
assert rows[0]["time_text"] == "2ч"
|
||||||
|
assert rows[1]["time_text"] == "1ч 30м"
|
||||||
27
tests/test_utils.py
Normal file
27
tests/test_utils.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import pytest
|
||||||
|
from redmine_reporter.utils import hours_to_human, get_month_name_from_range, get_version
|
||||||
|
|
||||||
|
|
||||||
|
def test_hours_to_human():
|
||||||
|
assert hours_to_human(0) == "0ч"
|
||||||
|
assert hours_to_human(1.0) == "1ч"
|
||||||
|
assert hours_to_human(2.5) == "2ч 30м"
|
||||||
|
assert hours_to_human(0.75) == "45м"
|
||||||
|
assert hours_to_human(3.1666) == "3ч 10м" # ≈ 3ч 10м
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_month_name_from_range():
|
||||||
|
assert get_month_name_from_range("2026-01-01", "2026-01-31") == "Январь"
|
||||||
|
assert get_month_name_from_range("2025-12-01", "2026-02-15") == "Февраль" # берётся to_date
|
||||||
|
assert get_month_name_from_range("invalid", "also_invalid") == "Январь" # fallback
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_version():
|
||||||
|
class MockIssue:
|
||||||
|
pass
|
||||||
|
issue_with = MockIssue()
|
||||||
|
issue_with.fixed_version = "v2.5.0"
|
||||||
|
assert get_version(issue_with) == "v2.5.0"
|
||||||
|
|
||||||
|
issue_without = MockIssue()
|
||||||
|
assert get_version(issue_without) == "<N/A>"
|
||||||
Reference in New Issue
Block a user