sn-testing
Skill8 starsChanged 7 months agoMITArchived repositoryView on GitHub
Testing strategy and conventions for SignNow MCP Server. Use when writing, reviewing, or modifying unit tests in this project. Covers per-layer testing patterns, mocking conventions (MagicMock for SignNowAPIClient, AsyncMock for Context), async test handling, parametrize usage, the analyze protocol (3 YES criteria), and verification steps. Do NOT use for implementation, spec writing, or planning.
In this repository8 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- git-commitSkill
- sn-architectureSkill
- sn-implementationSkill
- sn-planningSkill
- sn-spec-writingSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 5 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 15
Before writing tests, evaluate each change with the 3 YES criteria — at least one must be met:
- Distinctive: line 21
Do not write useless tests.
- Distinctive: line 131
Do NOT mock internal Python builtins or Pydantic internals.
- Distinctive: line 132
Do NOT mock the function under test — only external dependencies.
- Distinctive: line 173
Never declare "Done" until all pass.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Testing Standards
Analyze Protocol
Before writing tests, evaluate each change with the 3 YES criteria — at least one must be met:
- Business Logic: Does the change affect business logic?
- Regression Risk: Is it prone to regression?
- Complexity: Is it complex enough to benefit from tests?
Do not write useless tests. Quality over quantity.
Testing Strategy by Layer
A. Tool Business Logic (sn_mcp_server/tools/<feature>.py)
- Type: Unit. Isolation: Mock
SignNowAPIClientviaMagicMock. Mockfastmcp.ContextviaAsyncMockwhen function acceptsctx. - Focus: Data transformation, curated response fields, edge cases (empty data, missing optionals), error propagation, status normalization.
class TestFeatureName:
"""Test cases for _feature_function."""
@pytest.fixture
def mock_client(self):
"""Create a mock SignNowAPIClient."""
return MagicMock()
def test_feature_happy_path(self, mock_client):
"""Test successful execution with valid data."""
mock_client.api_method.return_value = SampleModel(...)
result = _feature_function(mock_client, "test_token", "param_value")
assert isinstance(result, ExpectedResponseModel)
assert result.field == "expected_value"
mock_client.api_method.assert_called_once_with("test_token", "param_value")B. Tool Response Models (sn_mcp_server/tools/models.py)
- Type: Unit. Focus: Construction, defaults,
from_*factories, status normalization, edge cases.
class TestResponseModel:
"""Test cases for ResponseModel."""
def test_model_construction_with_defaults(self):
"""Test model handles missing optional fields."""
model = ResponseModel(id="test123", name="Test")
assert model.optional_field is None
@pytest.mark.parametrize("raw_status,expected", [
("sent", "pending"),
("fulfilled", "completed"),
])
def test_status_normalization(self, raw_status, expected):
"""Test raw API status maps to unified status."""
result = InviteStatusValues.from_raw_status(raw_status)
assert result == expectedC. API Client Models (signnow_client/models/)
- Type: Unit. Focus: Pydantic validation, serialization (
by_alias=True), aliases, discriminators, coercion.
class TestApiModel:
"""Test cases for API model validation."""
def test_model_from_api_payload(self):
"""Test model validates raw API JSON payload."""
payload = {"field_name": "value", "nested": {"key": "val"}}
model = ApiModel(**payload)
assert model.field_name == "value"
def test_model_serializes_with_alias(self):
"""Test model serializes using API-expected field names."""
model = RequestModel(from_="sender@example.com")
dumped = model.model_dump(exclude_none=True, by_alias=True)
assert "from" in dumped
assert "from_" not in dumpedD. API Client Methods (signnow_client/client_*.py)
- Type: Unit. Isolation: Dummy subclass overriding
_get/_post/_putto capture calls. - Focus: URL construction, header passing, request serialization, response validation.
class _DummyClient(DocumentClientMixin):
def __init__(self):
self.last_post = None
def _post(self, url, headers=None, data=None, json_data=None, validate_model=None):
self.last_post = {"url": url, "headers": headers, "json_data": json_data}
if validate_model:
return validate_model.model_validate({"status": "ok"})
return None
def test_client_method_passes_correct_url():
client = _DummyClient()
client.some_method(token="t", document_id="doc123")
assert client.last_post["url"] == "/document/doc123/some-endpoint"Mocking Conventions
| Mock Target | Technique |
SignNowAPIClient | MagicMock() |
fastmcp.Context | AsyncMock(spec=Context) |
| API responses | Return real Pydantic model instances (not dicts) |
| API errors | side_effect with exception from signnow_client/exceptions.py |
Client internals (for client_*.py) | Dummy subclass overriding _get/_post |
Rules
- Mock
SignNowAPIClientmethods — never real HTTP calls. - Return real Pydantic model instances from mocks when logic expects validated models.
- Do NOT mock internal Python builtins or Pydantic internals.
- Do NOT mock the function under test — only external dependencies.
Async Tests
--asyncio-mode=auto in pytest.ini handles event loop automatically.
async def test_async_function(self, mock_client):
mock_context = AsyncMock(spec=Context)
mock_context.report_progress = AsyncMock()
result = await _async_feature(mock_context, "test_token", mock_client)
assert result is not NoneOutput Rules
- Location:
tests/unit/mirroring source:src/sn_mcp_server/tools/<feature>.py→tests/unit/sn_mcp_server/tools/test_<feature>.pysrc/signnow_client/client_documents.py→tests/unit/signnow_client/test_client_documents.py
- Naming: Files:
test_<module>.py. Classes:Test<Feature>. Methods:test_<what>_<scenario>. - Structure: AAA pattern (Arrange, Act, Assert).
- Modern pytest:
pytest.raises(ExceptionType, match=...),@pytest.mark.parametrize, fixtures oversetUp/tearDown, rawassert. - Docstrings: One-line for every test class and method. Module docstring:
"""Unit tests for <module> module.""" - No fluff: Output the test file directly. No explanations of why tests exist.
Constraints
- ❌ No config changes (
pyproject.toml,pytest.ini,AGENTS.md). - ❌ No network calls — all API mocked.
- ❌ No testing orchestrators (
tools/signnow.py) — test business logic intools/<feature>.py. - ✅ Each test independent. No shared mutable state.
Verification
Complete ALL before declaring done:
pytest tests/unit/ -v 2>&1— must pass. If fails, fix and retry.ruff check tests/ 2>&1— must pass.ruff format --check tests/ 2>&1— must pass.
Never declare "Done" until all pass.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

