Debugging and testing are crucial steps in ensuring that your Cairo programs function correctly and efficiently. Since Cairo is used for writing smart contracts on the StarkNet platform, robust testing ensures the correctness of your logic while debugging helps identify and fix issues.
Table of Contents
Debugging in Cairo
Debugging refers to the manner of figuring out, studying, and solving troubles (bugs) to your code. In Cairo, debugging can involve numerous techniques and equipment. Here’s an outline:
Common Debugging Tools
- StarkNet CLI: Provides feedback about contract deployments and transactions.
- VS Code Extension: Highlights syntax errors and provides insights while coding.
- Testing Frameworks: While testing, frameworks often reveal errors in code logic.
- Logs: Using
print
statements or custom logging to track variable values and execution paths.
Common Issues in Cairo
- Type Errors: Cairo has strict type requirements. Ensure all variables are declared and used correctly.
- Array Indexing: Arrays in Cairo require explicit management of indices.
- Incorrect Assertions: Ensure that your
assert
statements align with expected outputs. - Resource Limits: Check StarkNet’s transaction gas limits for efficiency.
- State Mutability: Verify that contract state changes are intended and follow StarkNet’s state update rules.
Debugging Process
- Step 1: Isolate the problematic function or module.
- Step 2: Use
print()
or logs to track intermediate states. - Step 3: Verify inputs, outputs, and internal logic for each function.
- Step 4: Test edge cases where errors are most likely to occur.
- Step 5: Use test-driven debugging (write a test that exposes the bug).
Writing Unit Tests in Cairo
Unit tests are small, focused tests that verify the correctness of individual components or functions in your Cairo program. Writing unit tests in Cairo helps ensure that your smart contracts and programs behave as expected.
Why Write Unit Tests in Cairo?
- Error Detection: Quickly identify bugs in isolated components.
- Code Reliability: Ensure individual functions work as expected.
- Development Confidence: Modify or extend code without fear of breaking existing functionality.
- Ease of Debugging: Pinpoint specific issues without testing the entire program.
Steps to Write Unit Tests in Cairo
- Set Up the Environment
- Use a testing framework like Cairo’s
pytest
integration. - Install Cairo and ensure you can compile and run contracts.
- Use a testing framework like Cairo’s
- Create a Test File
- Create a separate file for tests, typically named
test_<your_contract>.cairo
.
- Create a separate file for tests, typically named
- Import Necessary Modules
- Include the modules you need for the tests, such as the contract being tested and any utilities.
- Write Test Cases
- Use descriptive function names starting with
test_
. - Include
assert
statements to verify expected behavior.
- Use descriptive function names starting with
- Run the Tests
- Use Cairo’s
pytest
framework to execute tests and review results.
- Use Cairo’s
Basic Structure of a Unit Test
Here’s a breakdown of a simple unit test in Cairo:
# test_example.cairo
# Import the contract or functionality you want to test
from my_contract import addition
# Define a test function
@external
func test_addition{syscall_ptr: felt*}() -> ():
# Call the function and check its output
let result = addition(2, 3)
assert result == 5 # Check if the function works as expected
return ()
Best Practices for Writing Unit Tests
- Test Each Function Independently
- Focus on one function or logic block per test.
- Cover Edge Cases
- Include scenarios like:
- Zero or negative inputs.
- Maximum or minimum allowed values.
- Include scenarios like:
- Write Descriptive Test Names
- Example: Use
test_balance_updates_correctly
instead oftest_func1
.
- Example: Use
- Keep Tests Simple
- Avoid complex logic within test functions; they should be straightforward and easy to read.
- Validate All Possible Outcomes
- Test normal cases, edge cases, and expected failures.
Example of a Complete Unit Test File
Suppose you have a Cairo contract with a function to calculate the sum of two numbers:
Contract:
# my_contract.cairo
@external
func addition(a: felt, b: felt) -> (sum: felt):
return (sum=a + b)
Unit Test File:
# test_my_contract.cairo
# Import the function from the contract
from my_contract import addition
@external
func test_addition_positive_numbers{syscall_ptr: felt*}() -> ():
let result = addition(5, 7)
assert result == 12 # Check for correct addition
return ()
@external
func test_addition_zero{syscall_ptr: felt*}() -> ():
let result = addition(0, 0)
assert result == 0 # Zero addition case
return ()
@external
func test_addition_negative_numbers{syscall_ptr: felt*}() -> ():
let result = addition(-3, -7)
assert result == -10 # Negative number addition
return ()
How to Run Unit Tests in Cairo
- Compile the Test File Use the Cairo CLI to compile your test file:
cairo-compile test_my_contract.cairo --output test_my_contract.json
- Run the Tests Execute the tests using
pytest
or another Cairo-compatible framework:pytest test_my_contract.cairo
- Check Results
- Verify the output to confirm all tests are successful.
- When a test fails, review the error message to determine the root cause.
Common Errors in Unit Testing
- Type Mismatch
- Ensure the data types of inputs and outputs match the function definition.
- Assertion Failures
- If
assert
statements fail, verify the expected and actual outputs.
- If
- Uninitialized Variables
- Always initialize variables explicitly before use.
- Resource Mismanagement
- Manage Cairo’s resource (gas) limits, especially for complex tests.
Tips for Writing Effective Tests
- Mock Data: Use mock data for inputs that are hard to simulate in a test environment.
- Test Dependencies: If functions rely on other functions, test dependencies independently before testing interactions.
- Run Tests Frequently: Run your tests during development to catch bugs early.
Debugging Common Issues in Cairo
Debugging is a critical part of developing smart contracts or programs in Cairo. It involves identifying, analyzing, and fixing errors or unexpected behavior in your code. Cairo, being a unique programming language for zk-STARK-based computations, has its specific challenges and tools for debugging.
Common Issues in Cairo and How to Debug Them
- Type Mismatch Errors
- Cause: Incorrect data types used as function inputs or outputs. For example, passing a
felt
when an array is expected. - Solution:
- Check function signatures and ensure correct data types are used.
- Use explicit type casting if necessary.
- Example:
func add_numbers(a: felt, b: felt) -> (sum: felt): return (sum=a + b)
Error: Passing an array[1, 2]
instead of integers.
- Cause: Incorrect data types used as function inputs or outputs. For example, passing a
- Uninitialized Variables
- Cause: Accessing variables before initializing them.
- Solution:
- Always initialize variables explicitly.
- Use proper scoping to avoid referencing out-of-scope variables.
- Assertion Failures
- Cause: The
assert
statement fails when the actual output doesn’t match the expected value. - Solution:
- Review the logic in the function being tested.
- Print intermediate values to understand where the discrepancy arises.
- Cause: The
- Memory Allocation Issues
- Cause: Improper use of Cairo’s
alloc
or accessing memory out of bounds. - Solution:
- Ensure memory is allocated correctly before use.
- Use Cairo’s
alloc()
function to allocate memory explicitly.
- Cause: Improper use of Cairo’s
- Exceeding Gas Limits
- Cause: Cairo functions consuming too many computational resources.
- Solution:
- Optimize the logic to reduce computation.
- Minimize the use of redundant loops or recursive calls.
- Use efficient algorithms.
- Unexpected Function Behavior
- Cause: Logical errors in the implementation or misunderstanding of Cairo syntax.
- Solution:
- Break down the function into smaller parts and test each part independently.
- Use print debugging to log intermediate values.
- Array Index Out of Bounds
- Cause: Trying to access an index outside the bounds of an array.
- Solution:
- Ensure that array indices are validated prior to access.
- Use
len()
to check array length.
- Contract Interaction Errors
- Cause: Misconfigured external function calls or ABI mismatches.
- Solution:
- Ensure function selectors match between contracts.
- Verify data encoding and decoding for cross-contract interactions.
Debugging Techniques
- Print Debugging
- Use the
emit
statement or equivalent to log intermediate values during execution. - Example:
func example_debug() -> (): let x = 10 emit("Debug:", x) return ()
- Use the
- Use Cairo Debugger
- Run your code with a Cairo debugger to step through the execution process.
- Example:
cairo-run --program example.cairo --print_output --debug_info
- Analyze the trace to identify issues.
- Break the Code into Smaller Tests
- Isolate problematic parts of the code and test them independently.
- Simulate Inputs
- Use controlled inputs in unit tests to simulate edge cases and normal scenarios.
- Trace Logs
- Review trace logs generated during execution to pinpoint issues.
- Use
--tracer
with Cairo CLI for detailed execution logs.
- Code Reviews
- Have peers review your code for potential logical errors or syntax issues.
Example of Debugging an Issue
Problem: Function Returns Incorrect Output
Code:
func multiply(a: felt, b: felt) -> (product: felt):
return (product=a + b) # Error: Should multiply, not add
Steps to Debug:
- Write a Test Case:
func test_multiply() -> (): let result = multiply(2, 3) assert result == 6 # This will fail return ()
- Use Print Debugging:
func multiply(a: felt, b: felt) -> (product: felt): emit("Inputs:", a, b) return (product=a + b)
- Review Trace Logs:
- Run the test and check the logged values.
- Identify the mistake in the implementation.
- Fix the Code:
func multiply(a: felt, b: felt) -> (product: felt): return (product=a * b)
Best Practices for Debugging
- Write Unit Tests
- Address edge cases to identify potential problems early.
- Use a Modular Approach
- Split the code into smaller, modular functions that are easier to test.
- Comment Your Code
- Add comments to explain complex logic, making it easier to debug later.
- Keep Functions Simple
- Avoid overly complex functions to simplify debugging.
- Test Frequently
- Run tests during development to identify issues early.
- Leverage Tools
- Use Cairo’s debugging tools, such as
cairo-run
,pytest
, and external debuggers.
- Use Cairo’s debugging tools, such as
Integration Testing in Cairo
Integration testing verifies that the various components of a Cairo-based application function correctly together. Unlike unit tests that test individual functions or modules in isolation, integration tests focus on the interaction between multiple modules, smart contracts, or external services. This type of testing is crucial for validating real-world scenarios, such as cross-contract calls or contract-to-off-chain system interactions.
Key Objectives of Integration Testing
- Verify Component Interactions
Ensure that different Cairo functions or contracts communicate and work as intended. - Catch Edge Cases in Interactions
Detect issues arising from data mismatches, unexpected outputs, or incorrect assumptions. - Ensure System Reliability
Validate that the overall system remains functional under various scenarios, including boundary and stress cases.
Steps for Integration Testing in Cairo
1. Set Up the Environment
- Install necessary tools: Cairo CLI, testing frameworks (like
pytest
with Cairo bindings). - Deploy the involved contracts to a local testnet or a mock blockchain.
- Configure any off-chain systems (if applicable) to interact with the contracts.
2. Identify Test Scenarios
Plan scenarios that represent real-world use cases. For example:
- A user staking tokens and receiving rewards.
- A contract calling another contract to verify data or transfer tokens.
3. Prepare Test Inputs
- Create mock data that represents realistic inputs for the test scenarios.
- Use tools like
cairo-run
or libraries that support simulating contract inputs.
4. Execute the Tests
- Call functions in the sequence they would be executed in real-world scenarios.
- Use Cairo tools to simulate transactions and observe outputs.
5. Validate Outputs
- Compare outputs against expected results.
- Check for any unexpected state changes in the contracts or errors.
6. Log Results and Debug
- Analyze failures and pinpoint where the interaction fails.
- Utilize debugging tools to identify and resolve issues.
Tools for Integration Testing
- Cairo CLI
- Use
cairo-run
to execute programs and contracts with test inputs. - Example:
cairo-run --program my_contract.cairo --print_output --layout all
- Use
- StarkNet Devnet
- A local testing environment to deploy and interact with contracts as if they are on-chain.
- pytest with Cairo Bindings
- A Python-based testing framework extended to support Cairo.
Example: Integration Test for Cross-Contract Call
Scenario:
A user sends tokens to a contract (TokenContract
), which then interacts with another contract (RewardContract
) to calculate and distribute rewards.
Contracts:
TokenContract
func transfer(to: felt, amount: felt): // Logic for transferring tokens return ()
RewardContract
func calculate_rewards(user: felt, amount: felt) -> (reward: felt): // Logic for calculating rewards return (reward=amount * 2)
Test Script:
Deploy Contracts
starknet deploy --contract TokenContract.cairo starknet deploy --contract RewardContract.cairo
Integration Test (Python Example)
Def test_token_and_reward_integration(): # Deploy TokenContract and RewardContract token_contract = deploy_contract("TokenContract.Cairo") reward_contract = deploy_contract("RewardContract.Cairo") # Transfer tokens transfer_tx = token_contract.Capabilities["transfer"].Invoke(to=reward_contract.Cope with, quantity=one hundred) # Calculate rewards reward_tx = reward_contract.Features["calculate_rewards"].Invoke(person=token_contract.Deal with, quantity=100) # Validate reward output assert reward_tx.End result == (2 hundred,)
Best Practices for Integration Testing
- Test for Real-World Scenarios
Cover typical user flows and edge cases, such as contract failures or invalid inputs. - Use Mock Data Generators
Simulate realistic inputs to ensure robust testing. - Test for Scalability
Simulate high transaction volumes to test contract reliability under load. - Automate Testing
Use continuous integration pipelines to run integration tests after every change. - Isolate Failures
Test components in smaller groups to identify the source of failures.
Best Practices for Maintaining Clean Code in Cairo
Clean code is vital for growing maintainable, efficient, and error-unfastened Cairo clever contracts. It ensures that your code is readable, understandable, and easy to adjust or debug. Below are pleasant practices for writing clean code in Cairo.
1. Use Descriptive Naming Conventions
Why: Clear and descriptive names make your code extra readable and self-explanatory.
- Use descriptive names for variables, constants, and functions.
- ✅ Example: let token_balance = get_token_balance(user_id);
- Use snake_case for variable and function names as per Cairo conventions.
✅ Example:calculate_reward_amount
❌ Example:CalculateRewardAmount
2. Modularize Code
Why: Breaking code into smaller, reusable modules improves readability and reduces duplication.
- Divide large contracts or functions into smaller, well-defined functions.
✅ Example:func calculate_rewards(user: felt, stake: felt) -> (reward: felt): return (reward=stake * 2) func distribute_rewards(user: felt, reward: felt): # Distribution logic return ()
3. Use Constants and Configurable Parameters
Why: Hardcoding values makes code harder to understand and maintain.
- Define constants for fixed values like fee percentages or limits.
✅ Example:const MAX_STAKE: felt = 1000;
4. Comment Your Code Thoughtfully
Why: Comments clarify intent and help others understand complex logic.
- Avoid redundant comments; instead, explain why the code exists, not what it does.
✅ Example:# Check if the user has sufficient tokens to stake assert user_balance >= stake_amount, "Insufficient balance";
5. Handle Errors Gracefully
Why: Proper error handling prevents unexpected behavior and improves user experience.
- Use meaningful error messages in assertions.
✅ Example:assert reward > 0, "Reward must be greater than zero";
6. Optimize for Gas Efficiency
Why: Gas costs are critical in blockchain applications. Efficient code saves users money.
- Avoid unnecessary loops and operations.
- Use Cairo’s built-in functions for tasks where applicable.
✅ Example: Usefelt
over arrays where possible for lower complexity.
7. Follow Cairo Coding Standards
Why: Adhering to standard practices makes your code consistent and easier to work on in teams.
- Stick to Cairo’s recommended formatting, such as proper indentation.
- Use
@view
and@external
decorators correctly.
8. Keep Functions Focused
Why: Single-responsibility functions are easier to test, debug, and reuse.
- Each function must focus on performing a single task.
✅ Example:func add_stake(user: felt, amount: felt): # Logic for staking func calculate_rewards(user: felt) -> (reward: felt): # Logic for rewards
9. Write Comprehensive Tests
Why: Testing ensures code reliability and helps catch bugs early.
- Write unit tests for individual functions and integration tests for entire workflows.
- Test edge cases and unexpected inputs.
10. Use Version Control and Code Reviews
Why: Collaborating on code requires a clear history and feedback process.
- Employ Git to manage version control and track updates.
- Perform code reviews to maintain quality and consistency.
11. Avoid Code Duplication
Why: Redundant code increases maintenance efforts and the likelihood of bugs.
- Refactor the code to isolate common logic into reusable functions or modules.
✅ Example:func calculate_fee(amount: felt) -> (fee: felt): return (fee=amount / 100);
12. Document the Codebase
Why: Good documentation helps new developers quickly understand the project.
- Provide a README.Md with an overview of the task.
- Include information about the agreement’s cause, features, and expected inputs/outputs.
13. Refactor Regularly
Why: Refactoring improves code structure without changing its functionality.
- Remove unused variables and code.
- Combine similar functions or logic.
14. Use Libraries and Best Practices
Why: Reusing well-tested libraries saves time and reduces errors.
- Use existing Cairo libraries for common tasks like mathematical calculations or token standards.
15. Maintain a Clean Repository
Why: A cluttered repository makes it difficult to navigate the project.
- Organize files logically, e.g., separate
tests/
,contracts/
, andscripts/
. - Avoid including unnecessary files or temporary data in version control.
Example of Clean Code in Cairo
# Constants
const MAX_REWARD: felt = 1000;
# Function to calculate rewards
func calculate_rewards(user: felt, stake: felt) -> (reward: felt):
assert stake > 0, "Stake must be greater than zero";
let reward = min(stake * 2, MAX_REWARD);
return (reward=reward);
# Function to distribute rewards
func distribute_rewards(user: felt, reward: felt):
assert reward <= MAX_REWARD, "Reward exceeds maximum limit";
# Logic to send rewards
return ();
FAQs
Q. Why is clean code important in Cairo development? Clean code is easier to read, debug, and maintain, which is crucial for creating secure and efficient smart contracts.
Q. How can I ensure gas efficiency while maintaining clean code? Optimize loops, avoid unnecessary state changes, and use built-in functions. Profile gas usage with tools like StarkNet Devnet.
Q. Should I always comment my code? Yes, but focus on explaining the intent and logic rather than restating the obvious.
Q. How often should I refactor my code? Refactor regularly, especially after adding new features or identifying inefficiencies.
Q. What tools can help maintain clean code in Cairo? Use version control (Git), linters (if available), and testing frameworks like pytest
with Cairo bindings.
Q. Do I need a live blockchain for integration testing? Not necessarily. You can use local testnets like StarkNet Devnet or Cairo’s CLI tools for most integration tests.
Q. What if one part of the system fails during testing? Identify and isolate the failure using logs and debugging tools. Fix the issue and re-test the entire interaction.
Q. How do I simulate user behavior in integration tests? Use test scripts to replicate the sequence of transactions or interactions that a user would perform.
Q. Can integration tests replace unit tests? No. Both are necessary. Unit tests ensure individual functions work, while integration tests validate the overall system.
Q. What tools can I use to debug Cairo programs? You can use Cairo’s built-in debugging options like cairo-run
with --debug_info
, or external debugging tools integrated with your development environment.
Q. How do I debug gas consumption issues? Analyze the function’s logic to identify computationally expensive operations. Optimize loops and avoid unnecessary calculations.
Q. What is the best way to debug assertion failures? Use print debugging to log intermediate values and compare expected vs. actual outputs.
Q. Why is my Cairo program not compiling? Check for syntax errors, missing imports, and incorrect data type usage. Cairo requires strict adherence to its syntax rules.
Q. Can I debug Cairo contracts without deploying them? Yes, you can debug locally by running the contracts using the Cairo CLI tools without deploying on-chain.
Q. How is unit testing in Cairo different from other languages? Cairo’s testing focuses on its unique syntax and constraints, like managing memory and felt data types, while still following standard principles of testing.
Q. What should I do if my test fails? Review the failing test’s error message. Use print()
statements to debug and confirm intermediate values.
Q. Can I test multiple contracts together? Yes, integration testing allows you to test interactions between contracts, but unit testing focuses on individual functions or single contracts.
Q. Do I need to deploy a contract for unit testing? No, you can test functions locally without deploying the contract.
Q. What framework is best for running Cairo tests? pytest
with Cairo plugins is commonly used for testing Cairo programs.
Q. How do unit testing and integration testing differ?
Unit Testing: Tests individual functions or components.
- Integration Testing: Tests interactions between multiple components or functions.
Q. What tools are available for debugging Cairo? StarkNet CLI, VS Code Extension, and testing frameworks like pytest
.
Q. How can I ensure my tests are efficient?
- Write concise tests.
- Focus on edge cases.
- Use mock data to replicate real-world scenarios without incurring high costs.
Q. Why is gas optimization important in Cairo? Efficient code reduces transaction costs on StarkNet, benefiting users and developers.
Q. How do I handle large projects in Cairo?
- Modularize code.
- Use version control.
- Regularly test and debug during development.
Pingback: Chapter 10: Advanced Cairo Programming Topics - BlockSimplifier