Add Test
SkillDev toolsUse when writing tests for EmbodiChain modules, including observation functors, reward functors, solvers, sensors, environments, or any Python module
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Add Test skill
What this skill tells your AI
The instructions your AI receives, as published by dexforce/embodichain in .agents/skills/add-test/SKILL.md and read by ahel’s review.
Write tests following EmbodiChain's conventions and patterns.
When to Use
- User asks to "add a test", "write tests for X", "test this module"
- A new public module or function needs test coverage
- PR checklist requires tests
Test File Location
Tests mirror the source tree under tests/:
embodichain/lab/sim/solvers/pytorch_solver.py → tests/sim/solvers/test_pytorch_solver.py
embodichain/lab/gym/envs/managers/rewards.py → tests/gym/envs/managers/test_reward_functors.py
embodichain/toolkits/graspkit/pg_grasp/foo.py → tests/toolkits/test_pg_grasp.py
embodichain_tasks/embodichain_tasks/manipulation/push_cube.py → tests/gym/envs/tasks/test_push_cube.py
Rules:
- File name:
test_<module>.py - Directory path mirrors
embodichain/structure undertests/ - Create
__init__.pyfiles in newtests/subdirectories if needed
Two Test Styles
pytest Style — For Pure-Python Logic (No Sim)
Use when: testing functors, utility functions, pure math, config validation — anything that doesn't need a SimulationManager.
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# ...
# ----------------------------------------------------------------------------
from __future__ import annotations
import pytest
import torch
from embodichain.my_module import my_function
def test_expected_output():
result = my_function(input_value)
assert result == expected_value
def test_edge_case():
result = my_function(edge_input)
assert result is not None
Class Style — For Sim-Dependent or Ordered Tests
Use when: tests need SimulationManager, GPU setup, or must run in a specific order. Share state via setup_method/teardown_method.
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# ...
# ----------------------------------------------------------------------------
from __future__ import annotations
import pytest
import torch
from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
class TestMySimComponent:
def setup_method(self):
config = SimulationManagerCfg(headless=True, sim_device="cpu")
self.sim = SimulationManager(config)
# ... setup ...
def teardown_method(self):
self.sim.destroy()
SimulationManager.flush_cleanup_queue()
def test_basic_behavior(self):
result = self.sim.do_something()
assert result == expected_result
def test_raises_on_bad_input(self):
with pytest.raises(ValueError):
self.sim.do_something(bad_input)
Resource-Aware Test Classification
Use the narrowest test type that proves the behavior:
- Prefer mocks or CPU-only inputs for pure logic and shape validation.
tests/conftest.pyautomatically classifies conventional CUDA, renderer, and real-simulation tests. Add@pytest.mark.gpu,@pytest.mark.slow, or@pytest.mark.requires_simexplicitly only when the test's node id/source cannot reveal that requirement (for example, a hidden CUDA helper or an end-to-end toolkit test).
GPU tests are skipped by default to keep normal test runs within the shared VRAM budget. Run them explicitly and serially:
# Default suite: GPU-marked tests are skipped.
pytest tests/
# Dedicated GPU suite. Do not add -n unless pytest-xdist is installed.
pytest tests/ --run-gpu -m gpu
For backend/device matrices, run the complete contract on one representative
configuration and use small (one environment, low-resolution) smoke tests for
the remaining configurations. Always destroy a real SimulationManager and
flush its cleanup queue in teardown.
Task Program and task-configuration tests
Choose the test surface by ownership layer:
| Changed boundary | Primary test location |
|---|---|
| Language schema, strict decoder, AST/compiler | tests/lab/task_program/ |
| Semantic scene/profile/call/effect contracts | tests/lab/task_program/semantics/ |
| Configured integration, catalog, simulation assembly | tests/gym/envs/task_program/ |
| Gym bridge and episode completion | tests/gym/envs/task_program/, tests/gym/envs/test_embodied_env_task_program.py |
| Packaged task components/deployments | tests/test_task_program_package_data.py, task-layout/config tests |
| Lightweight RL environment/trainer routing | tests/learning/ |
For JSON/YAML configuration, test through the same strict loader and component
composition used by production. A yaml.safe_load() assertion alone does not
prove closed fields, relative component paths, physical scene targets,
contracts, catalog coverage, or program preflight.
Add negative coverage for the earliest ownership boundary being changed:
unknown fields, missing component files, duplicate inline/component ownership,
absent simulation_uid, contract mismatch, unknown Semantic Calls, or missing
registered lowerers. Keep live-simulation qualification separate from
provider-free decode/compiler tests.
For a complete configured Task Program deployment, use
$add-task-program's read-only inspector as a focused smoke check before
adding a heavier environment test.
Mocking Patterns for Functor Tests
Most functor tests don't need a live simulation. Use mock objects following the pattern in tests/gym/envs/managers/test_reward_functors.py:
from unittest.mock import MagicMock, Mock
class MockSim:
"""Mock simulation for functor tests."""
def __init__(self, num_envs: int = 4):
self.num_envs = num_envs
self.device = torch.device("cpu")
self._rigid_objects: dict = {}
def get_rigid_object(self, uid: str):
return self._rigid_objects.get(uid)
def add_rigid_object(self, obj):
self._rigid_objects[obj.uid] = obj
class MockEnv:
"""Mock environment for functor tests."""
def __init__(self, num_envs: int = 4):
self.num_envs = num_envs
self.device = torch.device("cpu")
self.sim = MockSim(num_envs)
Key points for mock objects:
- Set
num_envsanddeviceattributes (functors use these) - Mock only the sim methods the functor actually calls
- Use
MagicMock(uid="...")forSceneEntityCfgparameters
Steps
1. Identify What to Test
Ask the user:
- Which module/function? — determines file path
- Does it need a live simulation? — determines test style
- Key behaviors to verify — happy path, edge cases, error cases
2. Determine Test File Path
Map the source path to test path:
embodichain/<subpath>/<module>.py → tests/<subpath>/test_<module>.py
Check if the test file already exists — append new test classes/functions if so.
3. Choose Test Style
digraph test_style {
rankdir=LR;
"Needs SimulationManager?" -> "Class style" [label="yes"];
"Needs SimulationManager?" -> "pytest style" [label="no"];
"Tests share state/order?" -> "Class style" [label="yes"];
"Tests share state/order?" -> "pytest style" [label="no"];
}
4. Write the Test
Use the appropriate template (pytest or class style above).
Rules:
- Apache 2.0 header — required on every test file
from __future__ import annotations— after header, before imports- No magic numbers — define expected values as named constants or comment their origin
- Test function names —
test_<scenario>(descriptive, not justtest_foo) - One assertion concept per test — don't bundle unrelated checks
5. Add if __name__ == "__main__" Block
Include this for tests that support optional visual/interactive debugging:
if __name__ == "__main__":
# For visual debugging: set is_visual=True when calling env methods
test_obj = TestMyComponent()
test_obj.setup_method()
# ... manually run test logic ...
6. Run the Test
# Single file
pytest tests/<subpath>/test_<module>.py -v
# Single test function
pytest tests/<subpath>/test_<module>.py::test_expected_output -v
# GPU-specific test
pytest tests/<subpath>/test_<module>.py --run-gpu -m gpu -v
# Single test class method
pytest tests/<subpath>/test_<module>.py::TestMyClass::test_basic_behavior -v
7. Run black
black tests/<subpath>/test_<module>.py
Conventions Summary
| Convention | Rule |
|---|---|
| File header | Apache 2.0 copyright block (same 15 lines as source) |
| File naming | test_<module>.py |
| Function naming | test_<scenario> |
from __future__ | Required after header |
| Magic numbers | Define as named constants with explanatory comments |
| Simulation tests | Initialize/teardown in setup_method/teardown_method |
| CUDA coverage | Use @pytest.mark.gpu; run with --run-gpu -m gpu |
| Long integration | Use @pytest.mark.slow; keep it out of normal PR runs |
| Pure-logic tests | Use mock objects, no real sim |
SceneEntityCfg | Use MagicMock(uid="...") in tests |
| Assertions | assert, pytest.approx, torch.allclose, pytest.raises |
| Entry block | if __name__ == "__main__" for visual debugging support |
Common Mistakes
| Mistake | Fix |
|---|---|
| Missing Apache header on test file | Copy the 15-line copyright block |
Using real SimulationManager for functor tests | Use MockEnv/MockSim — much faster, no GPU needed |
| Hardcoded numbers without explanation | Define as EXPECTED_DISTANCE = 0.5 # cube at origin, target at (0.5, 0, 0) |
| Testing multiple concepts in one function | Split into separate test_<scenario> functions |
| Forgetting cleanup | Call self.sim.destroy() and SimulationManager.flush_cleanup_queue() in teardown |
| Using full matrices | Use one full representative case and low-resource smoke coverage elsewhere |
Not running black on test file | CI checks all files including tests |
Quick Reference
| Action | Command |
|---|---|
| Run default tests | pytest tests/ |
| Run GPU tests | pytest tests/ --run-gpu -m gpu |
| Run single file | pytest tests/<path>/test_<name>.py -v |
| Run single test | pytest tests/<path>::test_<name> -v |
| Run with print output | pytest -s tests/<path>/test_<name>.py |
| Format | black tests/<path>/test_<name>.py |
Signals
- GitHub stars
- 223
- Forks
- 24
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
add-test-dexforce- Source
- github.com/dexforce/embodichain