-
Notifications
You must be signed in to change notification settings - Fork 9
feat(test): add unit tests for config, output, and dubbing modules #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||||||||||||||||||||
| """Tests for narrator_ai.config module.""" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||
| from unittest.mock import patch | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from narrator_ai.config import ( | ||||||||||||||||||||||||||||
| DEFAULT_CONFIG, | ||||||||||||||||||||||||||||
| get_app_key, | ||||||||||||||||||||||||||||
| get_server, | ||||||||||||||||||||||||||||
| get_timeout, | ||||||||||||||||||||||||||||
| load_config, | ||||||||||||||||||||||||||||
| save_config, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_default_config_values(): | ||||||||||||||||||||||||||||
| assert DEFAULT_CONFIG["server"] == "https://openapi.jieshuo.cn" | ||||||||||||||||||||||||||||
| assert DEFAULT_CONFIG["app_key"] == "" | ||||||||||||||||||||||||||||
| assert DEFAULT_CONFIG["timeout"] == 30 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_load_config_returns_defaults_when_no_file(tmp_path): | ||||||||||||||||||||||||||||
| with patch("narrator_ai.config.CONFIG_FILE", tmp_path / "nonexistent.yaml"): | ||||||||||||||||||||||||||||
| cfg = load_config() | ||||||||||||||||||||||||||||
| assert cfg["server"] == DEFAULT_CONFIG["server"] | ||||||||||||||||||||||||||||
| assert cfg["timeout"] == DEFAULT_CONFIG["timeout"] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_save_and_load_config(tmp_path): | ||||||||||||||||||||||||||||
| config_file = tmp_path / "config.yaml" | ||||||||||||||||||||||||||||
| config_dir = tmp_path | ||||||||||||||||||||||||||||
| with patch("narrator_ai.config.CONFIG_FILE", config_file), \ | ||||||||||||||||||||||||||||
| patch("narrator_ai.config.CONFIG_DIR", config_dir): | ||||||||||||||||||||||||||||
| save_config({"server": "https://test.example.com", "app_key": "test-key"}) | ||||||||||||||||||||||||||||
| cfg = load_config() | ||||||||||||||||||||||||||||
| assert cfg["server"] == "https://test.example.com" | ||||||||||||||||||||||||||||
| assert cfg["app_key"] == "test-key" | ||||||||||||||||||||||||||||
| assert cfg["timeout"] == 30 # default preserved | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_get_server_from_env(): | ||||||||||||||||||||||||||||
| with patch.dict(os.environ, {"NARRATOR_SERVER": "https://env.example.com"}): | ||||||||||||||||||||||||||||
| assert get_server() == "https://env.example.com" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_get_server_strips_trailing_slash(): | ||||||||||||||||||||||||||||
| with patch.dict(os.environ, {"NARRATOR_SERVER": "https://example.com/"}): | ||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+50
|
||||||||||||||||||||||||||||
| def test_get_server_strips_trailing_slash(): | |
| with patch.dict(os.environ, {"NARRATOR_SERVER": "https://example.com/"}): | |
| def test_get_server_strips_trailing_slash(tmp_path): | |
| with patch("narrator_ai.config.CONFIG_FILE", tmp_path / "nonexistent.yaml"), \ | |
| patch.dict(os.environ, {"NARRATOR_SERVER": "https://example.com/"}): |
Copilot
AI
Apr 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test name says it "raises", but it currently asserts the default server is returned and explicitly notes it won't raise. Either rename it to reflect the behavior being tested, or adjust the setup to exercise the actual error path (e.g., create a config file with server: "" and clear env so get_server() raises SystemExit).
| with patch("narrator_ai.config.CONFIG_FILE", tmp_path / "nonexistent.yaml"), \ | |
| patch.dict(os.environ, {}, clear=True): | |
| # DEFAULT_CONFIG has server set, so this won't raise | |
| server = get_server() | |
| assert server == DEFAULT_CONFIG["server"].rstrip("/") | |
| config_file = tmp_path / "config.yaml" | |
| config_dir = tmp_path | |
| with patch("narrator_ai.config.CONFIG_FILE", config_file), \ | |
| patch("narrator_ai.config.CONFIG_DIR", config_dir): | |
| save_config({"server": ""}) | |
| with patch.dict(os.environ, {}, clear=True): | |
| with pytest.raises(SystemExit): | |
| get_server() |
Copilot
AI
Apr 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test calls get_app_key() without isolating CONFIG_FILE. If a local config file exists, it can change behavior (or fail parsing) even though the env var is set. Patch narrator_ai.config.CONFIG_FILE to a tmp/nonexistent path here for hermetic tests.
| def test_get_app_key_from_env(): | |
| with patch.dict(os.environ, {"NARRATOR_APP_KEY": "env-key-123"}): | |
| def test_get_app_key_from_env(tmp_path): | |
| with patch("narrator_ai.config.CONFIG_FILE", tmp_path / "nonexistent.yaml"), \ | |
| patch.dict(os.environ, {"NARRATOR_APP_KEY": "env-key-123"}): |
Copilot
AI
Apr 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test calls get_timeout() without isolating CONFIG_FILE. load_config() may read a real user config (or invalid YAML) before applying the env override, which makes the test non-hermetic. Patch narrator_ai.config.CONFIG_FILE to a tmp/nonexistent path in this test.
| def test_get_timeout_from_env(): | |
| with patch.dict(os.environ, {"NARRATOR_TIMEOUT": "60"}): | |
| def test_get_timeout_from_env(tmp_path): | |
| with patch("narrator_ai.config.CONFIG_FILE", tmp_path / "nonexistent.yaml"), \ | |
| patch.dict(os.environ, {"NARRATOR_TIMEOUT": "60"}): |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||
| """Tests for narrator_ai.commands.dubbing — voice list filtering logic.""" | ||||||
|
|
||||||
| from typer.testing import CliRunner | ||||||
|
|
||||||
| from narrator_ai.commands.dubbing import DUBBING_LIST, app | ||||||
|
|
||||||
| runner = CliRunner() | ||||||
|
|
||||||
|
|
||||||
| def test_dubbing_list_not_empty(): | ||||||
| assert len(DUBBING_LIST) > 0 | ||||||
|
|
||||||
|
|
||||||
| def test_dubbing_list_has_required_fields(): | ||||||
| for voice in DUBBING_LIST: | ||||||
| assert "name" in voice | ||||||
| assert "id" in voice | ||||||
| assert "type" in voice | ||||||
| assert "tag" in voice | ||||||
|
|
||||||
|
|
||||||
| def test_dubbing_list_json_output(): | ||||||
| result = runner.invoke(app, ["list", "--json"]) | ||||||
| assert result.exit_code == 0 | ||||||
| import json | ||||||
| data = json.loads(result.output) | ||||||
| assert isinstance(data, list) | ||||||
| assert len(data) > 0 | ||||||
|
|
||||||
|
|
||||||
| def test_dubbing_list_filter_by_lang(): | ||||||
| result = runner.invoke(app, ["list", "--lang", "英语", "--json"]) | ||||||
| assert result.exit_code == 0 | ||||||
| import json | ||||||
| data = json.loads(result.output) | ||||||
| assert all(v["type"] == "英语" for v in data) | ||||||
|
||||||
| assert all(v["type"] == "英语" for v in data) | |
| assert all("英语" in v["type"] for v in data) |
Copilot
AI
Apr 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The command filters tags with substring matching (tag in d["tag"]), but this test asserts strict equality. Align the assertion with the actual filter semantics (e.g., tag in v["tag"]) to reduce brittleness if tag strings expand in the future.
| assert all(v["tag"] == "通用男声" for v in data) | |
| assert all("通用男声" in v["tag"] for v in data) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||
| """Tests for narrator_ai.output module.""" | ||||||
|
|
||||||
| import json | ||||||
| from io import StringIO | ||||||
| from unittest.mock import patch | ||||||
|
Comment on lines
+4
to
+5
|
||||||
| from io import StringIO | |
| from unittest.mock import patch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests call
get_server()without isolating the on-disk config. If a developer/CI machine has an existing~/.narrator-ai/config.yaml(or invalid YAML),load_config()can affect or break this test even though the env var is set. Patchnarrator_ai.config.CONFIG_FILEto a nonexistent path (or tmp_path) within this test to make it hermetic.