Add Test

SkillDev tools

Use 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.

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 under tests/
  • Create __init__.py files in new tests/ 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.py automatically classifies conventional CUDA, renderer, and real-simulation tests. Add @pytest.mark.gpu, @pytest.mark.slow, or @pytest.mark.requires_sim explicitly 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 boundaryPrimary test location
Language schema, strict decoder, AST/compilertests/lab/task_program/
Semantic scene/profile/call/effect contractstests/lab/task_program/semantics/
Configured integration, catalog, simulation assemblytests/gym/envs/task_program/
Gym bridge and episode completiontests/gym/envs/task_program/, tests/gym/envs/test_embodied_env_task_program.py
Packaged task components/deploymentstests/test_task_program_package_data.py, task-layout/config tests
Lightweight RL environment/trainer routingtests/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_envs and device attributes (functors use these)
  • Mock only the sim methods the functor actually calls
  • Use MagicMock(uid="...") for SceneEntityCfg parameters

Steps

1. Identify What to Test

Ask the user:

  1. Which module/function? — determines file path
  2. Does it need a live simulation? — determines test style
  3. 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 namestest_<scenario> (descriptive, not just test_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

ConventionRule
File headerApache 2.0 copyright block (same 15 lines as source)
File namingtest_<module>.py
Function namingtest_<scenario>
from __future__Required after header
Magic numbersDefine as named constants with explanatory comments
Simulation testsInitialize/teardown in setup_method/teardown_method
CUDA coverageUse @pytest.mark.gpu; run with --run-gpu -m gpu
Long integrationUse @pytest.mark.slow; keep it out of normal PR runs
Pure-logic testsUse mock objects, no real sim
SceneEntityCfgUse MagicMock(uid="...") in tests
Assertionsassert, pytest.approx, torch.allclose, pytest.raises
Entry blockif __name__ == "__main__" for visual debugging support

Common Mistakes

MistakeFix
Missing Apache header on test fileCopy the 15-line copyright block
Using real SimulationManager for functor testsUse MockEnv/MockSim — much faster, no GPU needed
Hardcoded numbers without explanationDefine as EXPECTED_DISTANCE = 0.5 # cube at origin, target at (0.5, 0, 0)
Testing multiple concepts in one functionSplit into separate test_<scenario> functions
Forgetting cleanupCall self.sim.destroy() and SimulationManager.flush_cleanup_queue() in teardown
Using full matricesUse one full representative case and low-resource smoke coverage elsewhere
Not running black on test fileCI checks all files including tests

Quick Reference

ActionCommand
Run default testspytest tests/
Run GPU testspytest tests/ --run-gpu -m gpu
Run single filepytest tests/<path>/test_<name>.py -v
Run single testpytest tests/<path>::test_<name> -v
Run with print outputpytest -s tests/<path>/test_<name>.py
Formatblack 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