| name | test |
| description | Write, review, or split PyMechanical tests. Use when writing pytest files under tests/ or tests/embedding/, reviewing existing tests for modularity, splitting monolithic tests, or adding test coverage for embedding, remote session, or CLI features. Covers test structure, assert style, fixture use, and pytest markers. |
PyMechanical Test Scripts
Tests live under tests/ (remote, CLI) and tests/embedding/ (embedding). They use pytest
with custom markers and fixtures defined in tests/conftest.py.
Test Categories
Embedding — in-process (@pytest.mark.embedding)
Run inside the same Python process using a shared embedded_app fixture. Use for testing
App methods, API behaviour, and object manipulation.
@pytest.mark.embedding
def test_feature_name(embedded_app):
"""Test brief description."""
result = embedded_app.some_method()
assert result == expected
Embedding — subprocess (@pytest.mark.embedding_scripts)
Run a separate Python script via subprocess. Use for scenarios requiring process isolation
(pythonnet warnings, global importer edge cases, gallery building).
@pytest.mark.embedding_scripts
def test_script_behavior(run_subprocess, rootdir, tmp_path):
"""Test brief description."""
script = rootdir / "tests" / "scripts" / "my_script.py"
result = run_subprocess(script)
assert result.returncode == 0
Remote session (@pytest.mark.remote_session_connect)
Validate the ansys.mechanical.core.Mechanical gRPC client against a running Mechanical server.
@pytest.mark.remote_session_connect
def test_feature_name(mechanical):
"""Test brief description."""
result = mechanical.run_python_script("2+3")
assert result == "5"
One Behaviour Per Test
Each test function must verify exactly one behaviour. If a test does more than one logical
thing, split it into separate functions.
Wrong — multiple concerns in one test:
def test_license_manager(embedded_app):
assert len(embedded_app.license_manager.get_all_licenses()) > 0
embedded_app.license_manager.set_license_status("Ansys Mechanical Premium", False)
assert embedded_app.license_manager.get_license_status(...) == ...Disabled
embedded_app.license_manager.move_to_index("Ansys Mechanical Premium", 0)
assert embedded_app.license_manager.get_all_licenses().index(...) == 0
Right — one concern per function:
TEST_LICENSE = "Ansys Mechanical Premium"
def test_get_all_licenses(embedded_app):
"""Test that at least one license is available."""
assert len(embedded_app.license_manager.get_all_licenses()) > 0
def test_set_license_status(embedded_app):
"""Test enabling and disabling a license."""
lm = embedded_app.license_manager
lm.set_license_status(TEST_LICENSE, False)
assert lm.get_license_status(TEST_LICENSE) == lm._license_status.Disabled
lm.set_license_status(TEST_LICENSE, True)
assert lm.get_license_status(TEST_LICENSE) == lm._license_status.Enabled
def test_move_to_index(embedded_app):
"""Test moving a license to position zero."""
lm = embedded_app.license_manager
original_index = lm.get_all_licenses().index(TEST_LICENSE)
assert original_index > 0
lm.move_to_index(TEST_LICENSE, 0)
assert lm.get_all_licenses().index(TEST_LICENSE) == 0
Asserts
- One assert concept per test where possible. Multiple asserts are fine when they form a
single logical check (for example, enable → assert enabled, disable → assert disabled).
- No inline assert messages unless the failure would be genuinely ambiguous without one.
pytest already shows the values on failure.
- Never assert against hardcoded environment-specific values (list indices, version strings,
file paths). Capture the value first and assert relative to it.
assert license_list.index(TEST_LICENSE) == 1
original = license_list.index(TEST_LICENSE)
assert original > 0
lm.move_to_index(TEST_LICENSE, 0)
assert lm.get_all_licenses().index(TEST_LICENSE) == 0
lm.reset_preference()
assert lm.get_all_licenses().index(TEST_LICENSE) != 0
Comments
- No inline comments inside test bodies. The test name and docstring provide context.
- No section dividers (
# Enable and disable specific license, etc.).
- A single one-line module docstring is sufficient for the file.
- Each test function has a one-line docstring describing what it verifies.
Naming
test_<thing>_<condition> — for example test_session_license_invalid_type.
- Use a module-level constant for repeated literal values:
TEST_LICENSE = "Ansys Mechanical Premium"
File Structure (in order)
- Module docstring — short one-line description, e.g.
"""License manager tests."""
- Imports — stdlib → pytest →
ansys.mechanical.core modules
- Module-level constants — repeated literal values
- Test functions — each decorated with the appropriate marker
Import Patterns
Embedding tests
import os
from pathlib import Path
import subprocess
import sys
import pytest
from ansys.mechanical.core.embedding.app import App
from ansys.mechanical.core.embedding.background import BackgroundApp
Remote tests
import json
import os
from pathlib import Path
import pytest
import ansys.mechanical.core as pymechanical
Import only what is needed. Group: stdlib → pytest → ansys modules.
Available Fixtures
| Fixture | Scope | Purpose |
|---|
embedded_app | session | Shared App instance (embedding tests) |
mechanical | session | Shared remote Mechanical session instance |
tmp_path | function | Temporary directory unique to each test |
assets | session | Path to tests/assets/ directory |
rootdir | session | Path to repository root |
run_subprocess | function | Helper to run scripts in isolated subprocess |
pytestconfig | session | Access to pytest configuration and CLI options |
capsys | function | Capture stdout/stderr — add only when needed |
- Prefer the narrowest fixture scope that still works.
- Do not call
embedded_app.new() manually — the make_app_reset autouse fixture resets it.
- Add
capsys only to tests that capture stdout.
Markers
@pytest.mark.embedding — all in-process embedding tests.
@pytest.mark.embedding_scripts — subprocess-isolated embedding tests.
@pytest.mark.remote_session_connect — remote gRPC session tests.
@pytest.mark.minimum_version(261) — tests requiring a minimum Mechanical version.
Common Assertion Patterns
Embedding — App repr
app_repr_lines = repr(embedded_app).splitlines()
assert app_repr_lines[0].startswith("Ansys Mechanical")
assert app_repr_lines[1].startswith("Product Version")
Embedding — save and open
from tempfile import NamedTemporaryFile
project_file = tmp_path / f"{NamedTemporaryFile().name}.mechdat"
embedded_app.save_as(str(project_file))
assert project_file.exists()
Embedding — version check
version = embedded_app.version
assert version is not None
Embedding — tree output
tree_output = embedded_app.print_tree()
assert "Project" in tree_output
Embedding — deprecation warning
with pytest.warns(UserWarning):
deprecated_object.old_property
Remote — run Python script
result = mechanical.run_python_script("2+3")
assert result == "5"
Remote — run Python script returning object
result = mechanical.run_python_script("ExtAPI.DataModel.Project.Name")
assert result != ""
Remote — upload file
mechanical.upload(file_name=str(assets / "geometry.agdb"), file_location_destination=".")
Remote — download file
mechanical.download("file.mechdat", target_dir=str(tmp_path))
assert (tmp_path / "file.mechdat").exists()
Remote — multi-line script
script = """
import json
model = ExtAPI.DataModel.Project.Model
output = json.dumps({"name": model.Name, "bodies": model.Geometry.Bodies.Count})
output
"""
result = mechanical.run_python_script(script)
data = json.loads(result)
assert data["bodies"] > 0
Background App Testing
For BackgroundApp tests use the subprocess pattern. Helper scripts go in tests/scripts/.
@pytest.mark.embedding_scripts
def test_background_app(run_subprocess, rootdir):
"""Test background app lifecycle."""
script = rootdir / "tests" / "scripts" / "background_app_test.py"
result = run_subprocess(script)
assert result.returncode == 0
assert "error" not in result.stderr.lower()
Pytest Custom Flags (embedding)
Pass --log-embedding=<level> to enable the Mechanical embedding logger without changing
any test code:
pytest -m embedding -k test_set_license_status --ansys-version=261 --log-embedding=debug
Accepted levels: debug, info, warning, error. Omitting the flag leaves the embedding
logger unconfigured.
Anti-patterns to Avoid
| Anti-pattern | Fix |
|---|
| Monolithic test covering all public methods | One test per behaviour |
assert x == 1 where 1 is environment-specific | Capture value; assert relative |
| Inline comments explaining each step | Self-explaining test name + docstring |
assert condition, "message" when pytest output is clear | Plain assert condition |
| Catching broad exceptions to avoid test failure | Let the exception propagate |
Calling embedded_app.new() inside a test | Rely on make_app_reset autouse fixture |