Testing is an essential part of developing new functionality for the Hiero Python SDK. As a contributor, you are required to provide both unit tests and integration tests for any features you implement. This ensures code quality, prevents regressions, and maintains the reliability of the SDK.
Unit tests are lightweight, fast tests that verify individual components in isolation. They can be easily run on your local machine without requiring network connectivity.
Integration tests interact with an actual Hedera network (or a local Solo network) to verify that SDK components work correctly end-to-end. When you push a branch as a pull request, these tests automatically run against a Solo network in our CI pipeline.
This guide will walk you through:
- Understanding what unit and integration tests are
- Setting up your local testing environment
- Writing effective tests
- Running tests locally and in CI
- Best practices and common patterns
- Explaining Unit Tests
- Explaining Integration Tests
- Setting Up a Local Testing Suite
- Running Integration Tests
- Writing Your First Test
- Test Patterns and Best Practices
- Common Testing Utilities
- Troubleshooting
Unit tests are automated tests that verify the behavior of individual functions, classes, or methods in isolation. They test the smallest testable parts of your code without external dependencies like network calls, databases, or file systems.
- Fast: Run in milliseconds
- Isolated: Test one component at a time
- No External Dependencies: Use mocks or stubs for dependencies
- Deterministic: Always produce the same result for the same input
- Independent: Can run in any order
import pytest
from hiero_sdk_python.hbar import Hbar
def test_hbar_conversion_to_tinybars():
"""Test that Hbar correctly converts to tinybars."""
hbar = Hbar(1)
assert hbar.to_tinybars() == 100_000_000
def test_hbar_from_tinybars():
"""Test that Hbar can be created from tinybars."""
hbar = Hbar.from_tinybars(200_000_000)
assert hbar.to_tinybars() == 200_000_000Write unit tests for:
- Data transformations and calculations
- Validation logic
- Utility functions
- Object constructors and property setters
- Serialization/deserialization logic
- Error handling paths
Unit tests are located in the tests/unit/ directory, mirroring the structure of the main codebase:
hiero-sdk-python/
├── src/hiero_sdk_python/
│ ├── account/
│ └── account_create_transaction.py
└── tests/
└── unit/
└── test_account_create_transaction.py
Integration tests verify that multiple components work together correctly and that the SDK successfully interacts with a Hedera network. These tests execute actual transactions and queries against a running network.
- Slower: Take seconds or minutes to run
- Network-Dependent: Require connection to a Hedera network
- End-to-End: Test complete workflows
- Resource-Consuming: Use real HBAR, gas, and network resources
- Sequential: Some tests may depend on network state
import pytest
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
from hiero_sdk_python.crypto.private_key import PrivateKey
from hiero_sdk_python.hbar import Hbar
from hiero_sdk_python.response_code import ResponseCode
from tests.integration.utils import IntegrationTestEnv
@pytest.mark.integration
def test_integration_account_create_transaction_can_execute():
"""Test that an account can be created on the network."""
env = IntegrationTestEnv()
try:
new_account_private_key = PrivateKey.generate()
new_account_public_key = new_account_private_key.public_key()
initial_balance = Hbar(2)
transaction = AccountCreateTransaction(
key=new_account_public_key,
initial_balance=initial_balance,
memo="Test Account"
)
transaction.freeze_with(env.client)
receipt = transaction.execute(env.client)
assert receipt.account_id is not None, "Account ID should be present"
assert receipt.status == ResponseCode.SUCCESS
finally:
env.close()Write integration tests for:
- Transaction execution (create, update, delete operations)
- Query operations (balance, info queries)
- Complex workflows involving multiple steps
- Network-specific behavior
- Error responses from the network
- Fee calculations
Integration tests are located in the tests/integration/ directory:
hiero-sdk-python/
└── tests/
└── integration/
├── account_create_transaction_e2e_test.py
├── account_delete_transaction_e2e_test.py
├── contract_execute_transaction_e2e_test.py
└── utils_for_test.py
Integration test files should follow the pattern: <feature>_e2e_test.py
For example:
account_allowance_e2e_test.pytoken_associate_transaction_e2e_test.pycontract_call_query_e2e_test.py
Before setting up your testing environment, ensure you have:
- Python 3.10+ installed
- uv package manager installed (recommended)
- Git for version control
- A code editor (VS Code recommended)
git clone https://github.com/hiero-ledger/hiero-sdk-python.git
cd hiero-sdk-python# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or on macOS with Homebrew
brew install uv
# Install project dependencies
uv sync
# Generate protobuf files
uv run python generate_proto.pyThe uv sync command automatically:
- Downloads the correct Python version
- Creates a virtual environment
- Installs all dependencies including
pytestand testing tools
Create a .env file in the project root with your Hedera testnet credentials:
# Required
OPERATOR_ID=0.0.1234567
OPERATOR_KEY=302e020100300506032b657004220420...
NETWORK=testnet
# Optional (for specific tests)
ADMIN_KEY=302e020100300506032b657004220420...
SUPPLY_KEY=302a300506032b6570032100...
FREEZE_KEY=302a300506032b6570032100...
RECIPIENT_ID=0.0.7891011
TOKEN_ID=0.0.1234568
TOPIC_ID=0.0.1234569Note: A sample .env.example file is provided. If you don't have a testnet account, create one at Hedera Portal.
Some unit and integration tests (notably those covering Ethereum / EVM functionality) rely on optional ETH-related dependencies. These dependencies are not installed by default.
If these dependencies are missing ETH-related unit tests may fail with import errors.
These dependencies are provided via the eth extra.
When working on the SDK locally and running the full test suite:
uv sync --dev --extra ethThis installs:
- All standard development dependencies (pytest, ruff, mypy, etc.)
- All ETH-related optional dependencies required for tests and examples
If you are using pip instead of uv:
pip install -e ".[eth]"This ensures all ETH-related test code paths execute correctly during development.
VS Code provides excellent Python testing support with built-in test discovery and debugging.
Install the official Python extension by Microsoft from the VS Code marketplace.
- Open the Command Palette (
Cmd+Shift+Pon macOS,Ctrl+Shift+Pon Windows/Linux) - Type "Python: Configure Tests"
- Select pytest as the test framework
- Select tests as the directory containing tests
- Click the Testing icon in the Activity Bar (flask icon)
- VS Code will automatically discover all tests marked with
@pytest.mark.* - Tests will be organized by file and function
Run All Tests:
- Click "Run All Tests" button in the Testing panel
Run Individual Test:
- Hover over a test function
- Click the green play button
Run Test File:
- Right-click on a test file
- Select "Run Tests"
Debug Tests:
- Click the debug icon next to any test
- Set breakpoints in your code
- Step through execution
- Test results appear in the Testing panel
- Failed tests show error messages and stack traces
- Click on a failed test to jump to the code
Add to .vscode/settings.json:
{
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": [
"tests"
],
"python.testing.autoTestDiscoverOnSaveEnabled": true
}You can run tests directly from the command line, which is useful for CI/CD and quick local verification.
# Using uv (recommended)
uv run pytest
# Or activate the virtual environment first
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pytestuv run pytest tests/unit/uv run pytest tests/integration/
# Or using the marker:
uv run pytest -m integrationuv run pytest tests/unit/hbar_test.pyuv run pytest tests/unit/hbar_test.py::test_hbar_conversion_to_tinybarsuv run pytest -vuv run pytest -suv run pytest -luv run pytest --cov=hiero_sdk_python --cov-report=htmlThis generates an HTML coverage report in htmlcov/index.html.
# Install pytest-xdist first
uv pip install pytest-xdist
# Run tests in parallel
uv run pytest -n auto| Option | Description |
|---|---|
-v or --verbose |
Increase verbosity |
-s |
Don't capture output (show print statements) |
-x |
Stop on first failure |
--lf |
Run last failed tests only |
--ff |
Run failures first, then the rest |
-k <expression> |
Run tests matching expression |
-m <marker> |
Run tests with specific marker |
--collect-only |
Show available tests without running |
Integration tests require connection to a Hedera network. There are three ways to run them:
Requirements:
- Testnet account with HBAR balance
- Valid
.envconfiguration
Command (Recommended - use Solo network):
# Start your local Solo network first
solo network start
# Then run integration tests
uv run pytest tests/integration/ -m integrationConsiderations:
- Uses real HBAR (small amounts)
- Tests run against actual Hedera testnet
- Slower due to consensus time
- May fail if network is congested
When you push a branch and create a pull request, integration tests automatically run via GitHub Actions using the Hiero Solo Action.
Workflow:
- Push your branch:
git push origin your-branch-name - Create a pull request
- GitHub Actions automatically:
- Starts a Solo network
- Runs all integration tests
- Reports results in the PR
Viewing Results:
- Go to your PR on GitHub
- Click "Checks" tab
- View test results and logs
CI Configuration is in .github/workflows/ (you don't need to modify this).
Let's walk through creating both unit and integration tests for a hypothetical new feature.
File: You may look at an already-created unit test file for better clarity: token_pause_transaction_test.py
File: You may look at an already-created unit test file for better clarity: token_pause_transaction_e2e_test.py
# Run unit tests
uv run pytest tests/unit/tokens/token_transfer_test.py -v
# Run integration tests
uv run pytest tests/integration/token_transfer_e2e_test.py -vStructure your tests clearly:
def test_example():
# Arrange - Set up test data and preconditions
account_id = AccountId(0, 0, 1001)
initial_balance = Hbar(10)
# Act - Perform the action being tested
result = calculate_new_balance(account_id, initial_balance)
# Assert - Verify the outcome
assert result.to_tinybars() == 1_000_000_000Each test should verify a single behavior:
# Good - Tests one specific behavior
def test_hbar_to_tinybars_conversion():
hbar = Hbar(1)
assert hbar.to_tinybars() == 100_000_000
# Bad - Tests multiple behaviors
def test_hbar_everything():
hbar = Hbar(1)
assert hbar.to_tinybars() == 100_000_000
assert hbar.from_tinybars(200_000_000).to_tinybars() == 200_000_000
assert Hbar(5).to_tinybars() == 500_000_000Test names should describe what is being tested:
# Good
def test_account_create_fails_with_insufficient_balance():
pass
# Bad
def test_account():
passTests should not depend on each other:
# Bad - Tests depend on execution order
account = None
def test_create_account():
global account
account = create_account()
def test_update_account():
update_account(account) # Depends on previous test
# Good - Each test is independent
def test_create_account():
account = create_account()
assert account is not None
def test_update_account():
account = create_account() # Create fresh account
result = update_account(account)
assert result is Truefrom unittest.mock import Mock, patch
def test_transaction_execution_calls_network():
"""Test that transaction execution calls the network correctly."""
mock_client = Mock()
mock_client.execute.return_value = {"status": "SUCCESS"}
transaction = SomeTransaction()
result = transaction.execute(mock_client)
mock_client.execute.assert_called_once()
assert result["status"] == "SUCCESS"def test_hbar_handles_zero():
hbar = Hbar(0)
assert hbar.to_tinybars() == 0
def test_hbar_handles_large_values():
hbar = Hbar(1_000_000)
assert hbar.to_tinybars() == 100_000_000_000_000
def test_hbar_handles_negative_values():
with pytest.raises(ValueError):
Hbar(-1)import pytest
@pytest.fixture
def sample_account_id():
"""Provide a sample account ID for tests."""
return AccountId(0, 0, 1001)
@pytest.fixture
def sample_token_id():
"""Provide a sample token ID for tests."""
return TokenId(0, 0, 12345)
def test_with_fixtures(sample_account_id, sample_token_id):
"""Test using pytest fixtures."""
transfer = create_transfer(sample_account_id, sample_token_id, 100)
assert transfer.account_id == sample_account_idThe env fixture from utils_for_test.py provides a configured test environment:
from tests.integration.utils import env
@pytest.mark.integration
def test_with_env_fixture(env):
"""Test using the env fixture."""
# env.client is already configured
# env.operator_id and env.operator_key are available
account = env.create_account() # Helper method
assert account.id is not None@pytest.mark.integration
def test_with_cleanup(env):
"""Test with proper cleanup."""
account = None
try:
account = env.create_account()
# Perform test operations
assert account.id is not None
finally:
# Clean up if necessary
if account:
# Delete account or release resources
pass@pytest.mark.integration
def test_account_create_succeeds(env):
"""Test successful account creation."""
receipt = create_account(env)
assert receipt.status == ResponseCode.SUCCESS
@pytest.mark.integration
def test_account_create_fails_with_invalid_key(env):
"""Test that account creation fails with invalid key."""
receipt = create_account_with_invalid_key(env)
assert receipt.status == ResponseCode.INVALID_SIGNATUREExtract common setup into helper functions:
def _create_and_associate_token(env, account):
"""Helper to create token and associate it with account."""
token_receipt = TokenCreateTransaction().execute(env.client)
token_id = token_receipt.token_id
TokenAssociateTransaction().set_account_id(account.id).add_token_id(token_id).execute(env.client)
return token_id
@pytest.mark.integration
def test_token_transfer(env):
sender = env.create_account()
receiver = env.create_account()
token_id = _create_and_associate_token(env, receiver)
# Continue with test...Always check that transactions succeed:
receipt = transaction.execute(env.client)
assert receipt.status == ResponseCode.SUCCESS, (
f"Transaction failed with status: {ResponseCode(receipt.status).name}"
)import pytest
from hiero_sdk_python.exceptions import PrecheckError
def test_invalid_account_raises_error():
"""Test that invalid account ID raises appropriate error."""
with pytest.raises(PrecheckError, match="INVALID_ACCOUNT_ID"):
query_invalid_account()@pytest.mark.integration
def test_insufficient_balance_error(env):
"""Test that insufficient balance returns correct response code."""
receipt = transfer_more_than_balance(env)
assert receipt.status == ResponseCode.INSUFFICIENT_ACCOUNT_BALANCEThe tests/integration/utils_for_test.py file provides essential testing utilities:
from tests.integration.utils import IntegrationTestEnv, env
# Create environment manually
env = IntegrationTestEnv()
try:
# Use env.client, env.operator_id, env.operator_key
pass
finally:
env.close()
# Or use the pytest fixture (recommended)
@pytest.mark.integration
def test_example(env):
# env is automatically created and cleaned up
account = env.create_account()Key Methods:
env.create_account()- Creates a new test account with 1 HBARenv.client- Configured Hedera clientenv.operator_id- Operator account IDenv.operator_key- Operator private keyenv.close()- Cleanup (automatic with fixture)
from tests.integration.utils import (
create_fungible_token,
create_nft_token,
env
)
@pytest.mark.integration
def test_with_helpers(env):
# Create a fungible token with default settings
token_id = create_fungible_token(env)
# Create an NFT token
nft_id = create_nft_token(env)
# Use custom configuration with lambdas
token_id = create_fungible_token(env, [
lambda tx: tx.set_decimals(8),
lambda tx: tx.set_initial_supply(1000000)
])Use markers to categorize tests:
# Mark as integration test
@pytest.mark.integration
def test_network_operation(env):
pass
# Mark as slow test
@pytest.mark.slow
def test_long_running_operation():
pass
# Skip test conditionally
@pytest.mark.skipif(condition, reason="Reason for skipping")
def test_conditional():
passRun specific markers:
# Run only integration tests
uv run pytest -m integration
# Run everything except slow tests
uv run pytest -m "not slow"Test multiple inputs efficiently:
@pytest.mark.parametrize("amount,expected", [
(1, 100_000_000),
(5, 500_000_000),
(10, 1_000_000_000),
(0, 0),
])
def test_hbar_conversions(amount, expected):
"""Test Hbar to tinybar conversions with different values."""
hbar = Hbar(amount)
assert hbar.to_tinybars() == expectedSymptoms:
- Running
pytestshows "no tests ran" - VS Code doesn't show tests in Testing panel
Solutions:
- Ensure test files start with
test_or end with_test.py - Ensure test functions start with
test_ - Check that
pytestis installed:uv pip list | grep pytest - Verify you're in the correct directory:
pwdshould show project root - Clear pytest cache:
uv run pytest --cache-clear
Symptoms:
ConnectionError: Failed to connect to network
Solutions:
- Verify
.envfile exists and contains valid credentials - Check network connectivity:
ping testnet.hedera.com - Verify
NETWORKenvironment variable is set correctly - Ensure operator account has sufficient HBAR balance
- Try using a different network node
Symptoms:
PrecheckError: Transaction failed precheck with status: INSUFFICIENT_TX_FEE
Solutions:
- Check operator account balance
- Increase max transaction fee if needed
- Verify transaction is properly configured
- Check if network fees have increased
Symptoms:
- Tests pass on your machine
- Same tests fail in GitHub Actions
Solutions:
- Check CI logs for specific error messages
- Verify environment variables are set in CI
- Check for timing issues (add waits if needed)
- Ensure Solo network is properly configured
- Check for network-specific behavior differences
Symptoms:
- Tests take minutes to run
- Integration tests timeout
Solutions:
- Run only unit tests during development:
uv run pytest tests/unit/ - Use pytest-xdist for parallel execution:
uv run pytest -n auto - Mark slow tests and exclude them:
uv run pytest -m "not slow" - Use Solo network instead of testnet for faster consensus
- Optimize test setup - reuse accounts/tokens when possible
Symptoms:
ModuleNotFoundError: No module named 'hiero_sdk_python'
Solutions:
- Ensure virtual environment is activated
- Reinstall in editable mode:
uv pip install -e . - Generate protobuf files:
uv run python generate_proto.py - Check Python path:
python -c "import sys; print(sys.path)"
Symptoms:
- Tests pass sometimes, fail other times
- Inconsistent results
Solutions:
- Add proper waits for network consensus
- Don't assume immediate state changes
- Query network state to verify before assertions
- Avoid hard-coded delays, use polling instead
- Ensure proper test isolation
Symptoms:
- Mocked functions are being called for real
- Mock assertions fail
Solutions:
- Ensure you're patching the correct path (where it's used, not where it's defined)
- Use
patchas decorator or context manager correctly - Verify mock is applied before the code runs
- Check import statements in test file
Example:
# Wrong - patches where defined
@patch('hiero_sdk_python.client.Client.execute')
def test_wrong():
pass
# Correct - patches where used
@patch('my_module.Client.execute')
def test_correct():
passSymptoms:
- Tests can't find operator credentials
Nonevalues for env variables
Solutions:
- Verify
.envfile is in project root - Check file permissions:
ls -la .env - Ensure
python-dotenvis installed - Load manually in test if needed:
from dotenv import load_dotenv
load_dotenv()Symptoms:
- Tests fail when run together but pass individually
- Test order matters
Solutions:
- Use unique identifiers for each test
- Clean up resources in
finallyblocks - Use pytest fixtures with appropriate scope
- Avoid global state
- Create fresh accounts/tokens per test
For unit tests that need to simulate network behavior:
from unittest.mock import Mock, patch
import pytest
from hiero_sdk_python.response_code import ResponseCode
def test_transaction_handles_network_error():
"""Test that transaction properly handles network errors."""
mock_client = Mock()
mock_client.execute.side_effect = Exception("Network timeout")
transaction = SomeTransaction()
with pytest.raises(Exception, match="Network timeout"):
transaction.execute(mock_client)
def test_transaction_retries_on_failure():
"""Test that transaction retries on transient failures."""
mock_client = Mock()
# First call fails, second succeeds
mock_client.execute.side_effect = [
{"status": ResponseCode.BUSY},
{"status": ResponseCode.SUCCESS}
]
transaction = SomeTransaction()
result = transaction.execute_with_retry(mock_client, max_retries=2)
assert result["status"] == ResponseCode.SUCCESS
assert mock_client.execute.call_count == 2If your SDK has async functionality:
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async_transaction():
"""Test asynchronous transaction execution."""
client = await create_async_client()
transaction = AsyncTransaction()
result = await transaction.execute(client)
assert result.status == ResponseCode.SUCCESSUse hypothesis for property-based testing:
from hypothesis import given, strategies as st
@given(st.integers(min_value=0, max_value=1000000))
def test_hbar_conversion_property(amount):
"""Test that Hbar conversion is consistent for any valid amount."""
hbar = Hbar(amount)
tinybars = hbar.to_tinybars()
hbar2 = Hbar.from_tinybars(tinybars)
assert hbar.to_tinybars() == hbar2.to_tinybars()For complex output verification:
def test_transaction_serialization_snapshot(snapshot):
"""Test that transaction serialization hasn't changed."""
transaction = create_test_transaction()
serialized = transaction.to_proto()
# Compare against saved snapshot
snapshot.assert_match(serialized)Test performance characteristics:
import time
def test_transaction_performance():
"""Test that transaction creation is fast."""
start = time.time()
for _ in range(1000):
transaction = AccountCreateTransaction()
duration = time.time() - start
assert duration < 1.0, f"Transaction creation took {duration}s, expected < 1s"Check test coverage:
# Generate coverage report
uv run pytest --cov=hiero_sdk_python --cov-report=html --cov-report=term
# View in browser
open htmlcov/index.htmlAim for:
- 80%+ overall coverage
- 100% coverage for critical paths (transaction execution, signing)
- 90%+ coverage for new features
Before submitting a pull request, ensure:
- All new functions/methods have unit tests
- Edge cases are covered
- Error handling is tested
- Tests use mocks for external dependencies
- Tests run in < 5 seconds total
- All unit tests pass:
uv run pytest tests/unit/
- E2E workflow tests are included
- Both success and failure cases are tested
- Tests are marked with
@pytest.mark.integration - Tests clean up resources properly
- Tests use the
envfixture - All integration tests pass:
uv run pytest tests/integration/
- Tests follow naming conventions
- Test functions have clear docstrings
- Code is formatted with
ruff:uv run ruff format - No linting errors:
uv run ruff check - Test coverage is adequate
-
CHANGELOG.mdis updated underUNRELEASED - Complex test logic is commented
- Test fixtures are documented
- Commits are signed:
git commit -S -s -m "chore: message"(Add a scope prefix to your chore: commit message to match the project’s commit message style guide) - Commits are verified:
git log --show-signature - Branch is up to date with
main
File: account_id_test.py
File: account_balance_query_e2e_test.py
See the provided tests/integration/account_allowance_e2e_test.py for examples of:
- Setting up complex test scenarios
- Using helper functions
- Testing multiple related operations
- Verifying both success and failure paths
- Testing allowances, approvals, and delegations
- Write tests for all new features
- Test both success and failure paths
- Use descriptive test names
- Keep tests focused and independent
- Use fixtures for common setup
- Mock external dependencies in unit tests
- Clean up resources in integration tests
- Verify transaction status codes
- Document complex test logic
- Run tests before committing
- Sign your commits
- Update CHANGELOG.md
- Don't skip writing tests
- Don't test multiple things in one test
- Don't create test dependencies
- Don't use hard-coded sleeps
- Don't commit commented-out tests
- Don't ignore failing tests
- Don't forget to test error cases
- Don't leak resources in integration tests
- Don't use real production credentials
- Don't forget to update documentation
If you encounter issues or have questions:
- Check this guide - Most common scenarios are covered
- Review existing tests - Look at similar tests in the codebase
- Check documentation - See
/docs/sdk_developers/ - Ask in Discord - Ask on the Linux Foundation Decentralized Trust Discord
- Open an issue - For bugs or unclear documentation
- Contributing Guide: CONTRIBUTING.md
- Commit Signing Guide: Signing Guide
- pytest Documentation: https://docs.pytest.org/
- Hedera Documentation: https://docs.hedera.com/
- Hiero Solo: https://github.com/hiero-ledger/solo
Testing is a critical part of SDK development. By writing comprehensive unit and integration tests, you ensure:
- Code Quality - Catch bugs early
- Maintainability - Refactor with confidence
- Documentation - Tests show how code should be used
- Reliability - Users can trust the SDK
Remember:
- Unit tests verify individual components in isolation
- Integration tests verify end-to-end workflows with real network
- Run tests locally before pushing
- All tests must pass in CI before merging
Thank you for contributing to the Hiero Python SDK! Your tests make the SDK better for everyone.