diff --git a/Scarb.lock b/Scarb.lock index b1269310..da42f857 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -335,6 +335,9 @@ version = "0.1.0" [[package]] name = "testing_how_to" version = "0.1.0" +dependencies = [ + "snforge_std", +] [[package]] name = "timelock" diff --git a/listings/getting-started/testing_how_to/Scarb.toml b/listings/getting-started/testing_how_to/Scarb.toml index d1e8295c..ca48b10e 100644 --- a/listings/getting-started/testing_how_to/Scarb.toml +++ b/listings/getting-started/testing_how_to/Scarb.toml @@ -7,7 +7,8 @@ edition.workspace = true starknet.workspace = true [dev-dependencies] -cairo_test.workspace = true +assert_macros.workspace = true +snforge_std.workspace = true [scripts] test.workspace = true diff --git a/listings/getting-started/testing_how_to/src/contract.cairo b/listings/getting-started/testing_how_to/src/contract.cairo index 00a232b4..fdacee5a 100644 --- a/listings/getting-started/testing_how_to/src/contract.cairo +++ b/listings/getting-started/testing_how_to/src/contract.cairo @@ -1,41 +1,73 @@ // [!region contract] #[starknet::interface] -pub trait ISimpleContract { - fn get_value(self: @TContractState) -> u32; - fn get_owner(self: @TContractState) -> starknet::ContractAddress; - fn set_value(ref self: TContractState, value: u32); +pub trait IInventoryContract { + fn get_inventory_count(self: @TContractState) -> u32; + fn get_max_capacity(self: @TContractState) -> u32; + fn update_inventory(ref self: TContractState, new_count: u32); +} + +/// An external function that encodes constraints for update inventory +fn check_update_inventory(new_count: u32, max_capacity: u32) -> Result { + if new_count == 0 { + return Result::Err('OutOfStock'); + } + if new_count > max_capacity { + return Result::Err('ExceedsCapacity'); + } + + Result::Ok(new_count) } #[starknet::contract] -pub mod SimpleContract { +pub mod InventoryContract { + use super::check_update_inventory; use starknet::{get_caller_address, ContractAddress}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; #[storage] - struct Storage { - pub value: u32, + pub struct Storage { + pub inventory_count: u32, + pub max_capacity: u32, pub owner: ContractAddress, } + #[event] + #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] + pub enum Event { + InventoryUpdated: InventoryUpdated, + } + + #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] + pub struct InventoryUpdated { + pub new_count: u32, + } + #[constructor] - pub fn constructor(ref self: ContractState, initial_value: u32) { - self.value.write(initial_value); + pub fn constructor(ref self: ContractState, max_capacity: u32) { + self.inventory_count.write(0); + self.max_capacity.write(max_capacity); self.owner.write(get_caller_address()); } #[abi(embed_v0)] - pub impl SimpleContractImpl of super::ISimpleContract { - fn get_value(self: @ContractState) -> u32 { - self.value.read() + pub impl InventoryContractImpl of super::IInventoryContract { + fn get_inventory_count(self: @ContractState) -> u32 { + self.inventory_count.read() } - fn get_owner(self: @ContractState) -> ContractAddress { - self.owner.read() + fn get_max_capacity(self: @ContractState) -> u32 { + self.max_capacity.read() } - fn set_value(ref self: ContractState, value: u32) { + fn update_inventory(ref self: ContractState, new_count: u32) { assert(self.owner.read() == get_caller_address(), 'Not owner'); - self.value.write(value); + + match check_update_inventory(new_count, self.max_capacity.read()) { + Result::Ok(new_count) => self.inventory_count.write(new_count), + Result::Err(error) => { panic!("{}", error); }, + } + + self.emit(Event::InventoryUpdated(InventoryUpdated { new_count })); } } } @@ -44,178 +76,24 @@ pub mod SimpleContract { // [!region tests] #[cfg(test)] mod tests { - // Import the interface and dispatcher to be able to interact with the contract. - use super::{SimpleContract, ISimpleContractDispatcher, ISimpleContractDispatcherTrait}; - - // Import the deploy syscall to be able to deploy the contract. - use starknet::syscalls::deploy_syscall; - use starknet::{get_contract_address, contract_address_const}; - - // Use starknet test utils to fake the contract_address - use starknet::testing::set_contract_address; - - // Deploy the contract and return its dispatcher. - fn deploy(initial_value: u32) -> ISimpleContractDispatcher { - // Declare and deploy - let (contract_address, _) = deploy_syscall( - SimpleContract::TEST_CLASS_HASH.try_into().unwrap(), - 0, - array![initial_value.into()].span(), - false, - ) - .unwrap(); - - // Return the dispatcher. - // The dispatcher allows to interact with the contract based on its interface. - ISimpleContractDispatcher { contract_address } - } + use super::check_update_inventory; #[test] - fn test_deploy() { - let initial_value: u32 = 10; - let contract = deploy(initial_value); - - assert_eq!(contract.get_value(), initial_value); - assert_eq!(contract.get_owner(), get_contract_address()); + fn test_check_update_inventory() { + let result = check_update_inventory(10, 100); + assert_eq!(result, Result::Ok(10)); } #[test] - fn test_set_as_owner() { - // Fake the contract address to owner - let owner = contract_address_const::<'owner'>(); - set_contract_address(owner); - - // When deploying the contract, the owner is the caller. - let contract = deploy(10); - assert_eq!(contract.get_owner(), owner); - - // As the current caller is the owner, the value can be set. - let new_value: u32 = 20; - contract.set_value(new_value); - - assert_eq!(contract.get_value(), new_value); + fn test_check_update_inventory_out_of_stock() { + let result = check_update_inventory(0, 100); + assert_eq!(result, Result::Err('OutOfStock')); } #[test] - #[should_panic] - fn test_set_not_owner() { - let owner = contract_address_const::<'owner'>(); - set_contract_address(owner); - let contract = deploy(10); - - // Fake the contract address to another address - let not_owner = contract_address_const::<'not owner'>(); - set_contract_address(not_owner); - - // As the current caller is not the owner, the value cannot be set. - let new_value: u32 = 20; - contract.set_value(new_value); - // Panic expected - } - - #[test] - #[available_gas(150000)] - fn test_deploy_gas() { - deploy(10); - } -} -// [!endregion tests] - -// [!region tests_with_state] -#[cfg(test)] -mod tests_with_states { - // Only import the contract and implementation - use super::SimpleContract; - use SimpleContract::SimpleContractImpl; - - use starknet::contract_address_const; - use starknet::testing::set_caller_address; - use core::num::traits::Zero; - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - - #[test] - fn test_standalone_state() { - let mut state = SimpleContract::contract_state_for_testing(); - - // As no contract was deployed, the constructor was not called on the state - // - with valueContractMemberStateTrait - assert_eq!(state.value.read(), 0); - // - with SimpleContractImpl - assert_eq!(state.get_value(), 0); - assert_eq!(state.owner.read(), Zero::zero()); - - // We can still directly call the constructor to initialize the state. - let owner = contract_address_const::<'owner'>(); - // We are not setting the contract address but the caller address here, - // as we are not deploying the contract but directly calling the constructor function. - set_caller_address(owner); - - let initial_value: u32 = 10; - SimpleContract::constructor(ref state, initial_value); - assert_eq!(state.get_value(), initial_value); - assert_eq!(state.get_owner(), owner); - - // As the current caller is the owner, the value can be set. - let new_value: u32 = 20; - state.set_value(new_value); - assert_eq!(state.get_value(), new_value); - } - - // But we can also deploy the contract and interact with it using the dispatcher - // as shown in the previous tests, and still use the state for testing. - use super::{ISimpleContractDispatcher, ISimpleContractDispatcherTrait}; - use starknet::{syscalls::deploy_syscall, testing::set_contract_address}; - - #[test] - fn test_state_with_contract() { - let owner = contract_address_const::<'owner'>(); - let not_owner = contract_address_const::<'not owner'>(); - - // Deploy as owner - let initial_value: u32 = 10; - set_contract_address(owner); - let (contract_address, _) = deploy_syscall( - SimpleContract::TEST_CLASS_HASH.try_into().unwrap(), - 0, - array![initial_value.into()].span(), - false, - ) - .unwrap(); - let mut contract = ISimpleContractDispatcher { contract_address }; - - // create the state - // - Set back as not owner - set_contract_address(not_owner); - let mut state = SimpleContract::contract_state_for_testing(); - // - Currently, the state is not 'linked' to the contract - assert_ne!(state.get_value(), initial_value); - assert_ne!(state.get_owner(), owner); - // - Link the state to the contract by setting the contract address - set_contract_address(contract.contract_address); - assert_eq!(state.get_value(), initial_value); - assert_eq!(state.get_owner(), owner); - - // Mutating the state from the contract changes the testing state - set_contract_address(owner); - let new_value: u32 = 20; - contract.set_value(new_value); - set_contract_address(contract.contract_address); - assert_eq!(state.get_value(), new_value); - - // Mutating the state from the testing state changes the contract state - set_caller_address(owner); - state.set_value(initial_value); - assert_eq!(contract.get_value(), initial_value); - - // Directly mutating the state allows to change state - // in ways that are not allowed by the contract, such as changing the owner. - let new_owner = contract_address_const::<'new owner'>(); - state.owner.write(new_owner); - assert_eq!(contract.get_owner(), new_owner); - - set_caller_address(new_owner); - state.set_value(new_value); - assert_eq!(contract.get_value(), new_value); + fn test_check_update_inventory_exceeds_capacity() { + let result = check_update_inventory(101, 100); + assert_eq!(result, Result::Err('ExceedsCapacity')); } } // [!endregion tests] diff --git a/listings/getting-started/testing_how_to/src/lib.cairo b/listings/getting-started/testing_how_to/src/lib.cairo index 6ccaa47d..1eccea76 100644 --- a/listings/getting-started/testing_how_to/src/lib.cairo +++ b/listings/getting-started/testing_how_to/src/lib.cairo @@ -1 +1,4 @@ mod contract; +pub use contract::{ + InventoryContract, IInventoryContractDispatcher, IInventoryContractDispatcherTrait, +}; diff --git a/listings/getting-started/testing_how_to/tests/test_contract.cairo b/listings/getting-started/testing_how_to/tests/test_contract.cairo new file mode 100644 index 00000000..22f76250 --- /dev/null +++ b/listings/getting-started/testing_how_to/tests/test_contract.cairo @@ -0,0 +1,149 @@ +// [!region tests] +// Import the interface and dispatcher to be able to interact with the contract. +use testing_how_to::{IInventoryContractDispatcher, IInventoryContractDispatcherTrait}; + +// Import the required traits and functions from Snforge +use snforge_std::{ContractClassTrait, DeclareResultTrait, declare}; +// And additionally the testing utilities +use snforge_std::{start_cheat_caller_address_global, stop_cheat_caller_address_global, load}; + +// Declare and deploy the contract and return its dispatcher. +fn deploy(max_capacity: u32) -> IInventoryContractDispatcher { + let contract = declare("InventoryContract").unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@array![max_capacity.into()]).unwrap(); + + // Return the dispatcher. + // It allows to interact with the contract based on its interface. + IInventoryContractDispatcher { contract_address } +} + +#[test] +fn test_deploy() { + let max_capacity: u32 = 100; + let contract = deploy(max_capacity); + + assert_eq!(contract.get_max_capacity(), max_capacity); + assert_eq!(contract.get_inventory_count(), 0); +} + +#[test] +fn test_as_owner() { + let owner = starknet::contract_address_const::<'owner'>(); + start_cheat_caller_address_global(owner); + + // When deploying the contract, the caller is owner. + let contract = deploy(100); + + // Owner can call update inventory successfully + contract.update_inventory(10); + assert_eq!(contract.get_inventory_count(), 10); + + // additionally, you can directly test the storage + let loaded = load( + contract.contract_address, // the contract address + selector!("owner"), // field marking the start of the memory chunk being read from + 1 // length of the memory chunk (seen as an array of felts) to read. Here, `u32` fits in 1 felt. + ); + assert_eq!(loaded, array!['owner']); +} + +#[test] +#[should_panic] +fn test_as_not_owner() { + let owner = starknet::contract_address_const::<'owner'>(); + start_cheat_caller_address_global(owner); + let contract = deploy(100); + + // Change the caller address to a not owner + stop_cheat_caller_address_global(); + + // As the current caller is not the owner, the value cannot be set. + contract.update_inventory(20); + // Panic expected +} +// [!endregion tests] + +// [!region tests_with_contract_state] +use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; +use testing_how_to::InventoryContract; +// To be able to call the contract methods on the state +use InventoryContract::InventoryContractImpl; +#[test] +fn test_with_contract_state() { + let owner = starknet::contract_address_const::<'owner'>(); + start_cheat_caller_address_global(owner); + + // Initialize the contract state and call the constructor + let mut state = InventoryContract::contract_state_for_testing(); + InventoryContract::constructor(ref state, 10); + + // Read storage values + assert_eq!(state.max_capacity.read(), 10); + assert_eq!(state.inventory_count.read(), 0); + assert_eq!(state.owner.read(), owner); + + // Update the inventory count by calling the contract method + let update_inventory = 10; + state.update_inventory(update_inventory); + assert_eq!(state.inventory_count.read(), update_inventory); + + // Or directly write to the storage + let user = starknet::contract_address_const::<'user'>(); + state.owner.write(user); + assert_eq!(state.owner.read(), user); +} +// [!endregion tests_with_contract_state] + +// [!region tests_with_direct_storage_access] +#[test] +fn test_as_owner_with_direct_storage_access() { + let owner = starknet::contract_address_const::<'owner'>(); + start_cheat_caller_address_global(owner); + let contract = deploy(100); + let update_inventory = 10; + contract.update_inventory(update_inventory); + + // You can directly test the storage + let owner_storage = load( + contract.contract_address, // the contract address + selector!("owner"), // field marking the start of the memory chunk being read from + 1 // length of the memory chunk (seen as an array of felts) to read. Here, `u32` fits in 1 felt. + ); + assert_eq!(owner_storage, array!['owner']); + + // Same for the inventory count: + // Here we showcase how to deserialize the value from it's raw felts representation to it's + // original type. + let mut inventory_count = load(contract.contract_address, selector!("inventory_count"), 1) + .span(); + let inventory_count: u32 = Serde::deserialize(ref inventory_count).unwrap(); + assert_eq!(inventory_count, update_inventory); +} +// [!endregion tests_with_direct_storage_access] + +// [!region tests_with_events] +use snforge_std::{spy_events, EventSpyAssertionsTrait}; +#[test] +fn test_events() { + let contract = deploy(100); + + let mut spy = spy_events(); + + // This emits an event + contract.update_inventory(10); + + spy + .assert_emitted( + @array![ + ( + contract.contract_address, + InventoryContract::Event::InventoryUpdated( + InventoryContract::InventoryUpdated { new_count: 10 }, + ), + ), + ], + ) +} +// [!endregion tests_with_events] + + diff --git a/pages/getting-started/testing/contract-testing.md b/pages/getting-started/testing/contract-testing.md deleted file mode 100644 index bb650962..00000000 --- a/pages/getting-started/testing/contract-testing.md +++ /dev/null @@ -1,92 +0,0 @@ -# Contract Testing - -Testing plays a crucial role in software development, especially for smart contracts. In this section, we'll guide you through the basics of testing a smart contract on Starknet with `scarb:{bash}`. - -Let's start with a simple smart contract as an example: - -```cairo -// [!include ~/listings/getting-started/testing_how_to/src/contract.cairo:contract] -``` - -Now, take a look at the tests for this contract: - -```cairo -// [!include ~/listings/getting-started/testing_how_to/src/contract.cairo:tests] -``` - -To define our test, we use scarb, which allows us to create a separate module guarded with `#[cfg(test)]`. This ensures that the test module is only compiled when running tests using `scarb test{:bash}`. - -Each test is defined as a function with the `#[test]` attribute. You can also check if a test panics using the `#[should_panic]` attribute. - -As we are in the context of a smart contract, you can also set up the gas limit for a test by using the `#[available_gas(X)]`. This is a great way to ensure that some of your contract's features stay under a certain gas limit! - -:::note -The term "gas" here refers to Sierra gas, not L1 gas. -::: - -Now, let's move on to the testing process: - -- Use the `deploy` function logic to declare and deploy your contract -- Use `assert` to verify that the contract behaves as expected in the given context - - You can also use assertion macros: `assert_eq!`, `assert_ne!`, `assert_gt!`, `assert_ge!`, `assert_lt!`, `assert_le!` - -## Using the contract state - -You can use the `Contract::contract_state_for_testing` function to access the contract state. This function is only available in the test environment and allows you to mutate and read the contract state directly. - -This can be useful for testing internal functions, or specific state mutations that are not exposed to the contract's interface. You can either use it with a deployed contract or as a standalone state. - -Here is an example of how to do the same previous test using the contract state: - -```cairo -// [!include ~/listings/getting-started/testing_how_to/src/contract.cairo:tests_with_state] -``` - -## Testing events - -In order to test events, you need to use the `starknet::pop_log` function. If the contract did not emit any events, the function will return `Option::None`. - -See the test for the [Events](/getting-started/basics/events) section: - -```cairo -// [!include ~/listings/getting-started/events/src/counter.cairo:test_events] -``` - -## Starknet Corelib Testing Module - -To make testing more convenient, the `testing` module of the corelib provides some helpful functions: - -- `set_caller_address(address: ContractAddress)` -- `set_contract_address(address: ContractAddress)` -- `set_block_number(block_number: u64)` -- `set_block_timestamp(block_timestamp: u64)` -- `set_account_contract_address(address: ContractAddress)` -- `set_sequencer_address(address: ContractAddress)` -- `set_version(version: felt252)` -- `set_transaction_hash(hash: felt252)` -- `set_chain_id(chain_id: felt252)` -- `set_nonce(nonce: felt252)` -- `set_signature(signature: felt252)` -- `set_max_fee(fee: u128)` -- `pop_log_raw(address: ContractAddress) -> Option<(Span, Span)>` -- `pop_log>(address: ContractAddress) -> Option` -- `pop_l2_to_l1_message(address: ContractAddress) -> Option<(felt252, Span)>` - -You may also need the `info` module from the corelib, which allows you to access information about the current execution context (see [syscalls](/getting-started/syscalls)): - -- `get_caller_address() -> ContractAddress` -- `get_contract_address() -> ContractAddress` -- `get_block_info() -> Box` -- `get_tx_info() -> Box` -- `get_block_timestamp() -> u64` -- `get_block_number() -> u64` - -You can find the full list of functions in the [Starknet Corelib repo](https://github.com/starkware-libs/cairo/tree/main/corelib/src/starknet). - -## Starknet Foundry - -Starknet Foundry is a powerful toolkit for developing smart contracts on Starknet. It offers support for testing Starknet smart contracts on top of `scarb{:bash}` with the `snforge{:bash}` tool. - -Testing with `snforge{:bash}` is similar to the process we just described, but simplified. Moreover, additional features are on the way, including cheatcodes and parallel test execution. We highly recommend exploring Starknet Foundry and incorporating it into your projects. - -For more detailed information about testing contracts with Starknet Foundry, check out the [Starknet Foundry Book - Testing Contracts](https://foundry-rs.github.io/starknet-foundry/testing/contracts.html). diff --git a/pages/getting-started/testing/testing-cairo-test.md b/pages/getting-started/testing/testing-cairo-test.md new file mode 100644 index 00000000..fa2a6ffb --- /dev/null +++ b/pages/getting-started/testing/testing-cairo-test.md @@ -0,0 +1,17 @@ +# Testing with Cairo-test + +:::warning +Starknet Foundry provides a more comprehensive testing runner with Snforge, specifically designed for Starknet smart contracts. +It is highly recommended to use it instead of Cairo-test, see [Testing with Snforge](/getting-started/testing/testing-snforge). +::: + +Cairo-test is the included testing framework from Cairo, and can be run with `scarb test{:bash}` (or `scarb cairo-test{:bash}`). +You need to add it as a dev dependency with the following line in your `Scarb.toml`: +```toml +[dev-dependencies] +cairo_test = "2.9.4" // Version should be same as your Starknet/Scarb version +``` + +Testing is done similarly as shown in the [Testing with Snforge](/getting-started/testing/testing-snforge) section, but all the snforge specific features are not available. + +You can learn more about using Cairo-test in the [Cairo Book - Testing Cairo Programs](https://book.cairo-lang.org/ch10-00-testing-cairo-programs.html#testing-cairo-programs). diff --git a/pages/getting-started/testing/testing-snforge.md b/pages/getting-started/testing/testing-snforge.md new file mode 100644 index 00000000..08f5458a --- /dev/null +++ b/pages/getting-started/testing/testing-snforge.md @@ -0,0 +1,89 @@ +# Testing with Starknet Foundry (Snforge) + +## Overview + +Starknet Foundry provides a robust testing framework specifically designed for Starknet smart contracts. Tests can be executed using the `snforge test` command. + +:::info +To use snforge as your default test runner, add this to your `scarb.toml{:md}`: +```toml +[scripts] +test = "snforge test" +``` +This will make `scarb test` use snforge under the hood. +::: + +Let's examine a sample contract that we'll use throughout this section: + +```cairo +// [!include ~/listings/getting-started/testing_how_to/src/contract.cairo:contract] +``` + +## Test Structure and Organization + +### Test Location +There are two common approaches to organizing tests: +1. **Integration Tests**: Place in the `tests/{:md}` directory, following your `src/{:md}` structure +2. **Unit Tests**: Place directly in `src/{:md}` files within a test module + +For unit tests in source files, always guard the test module with `#[cfg(test)]` to ensure tests are only compiled during testing: + +```cairo +// [!include ~/listings/getting-started/testing_how_to/src/contract.cairo:tests] +``` + +### Basic Test Structure + +Each test function requires the `#[test]` attribute. For tests that should verify error conditions, add the `#[should_panic]` attribute. + +Here's a comprehensive test example: + +```cairo +// [!include ~/listings/getting-started/testing_how_to/tests/test_contract.cairo:tests] +``` + +## Testing Techniques + +### Direct Storage Access + +For testing specific storage scenarios, snforge provides `load` and `store` functions: + +```cairo +// [!include ~/listings/getting-started/testing_how_to/tests/test_contract.cairo:tests_with_direct_storage_access] +``` + +### Contract State Testing + +Use `Contract::contract_state_for_testing` to access internal contract state: + +```cairo +// [!include ~/listings/getting-started/testing_how_to/tests/test_contract.cairo:tests_with_contract_state] +``` + +### Event Testing + +To verify event emissions: + +```cairo +// [!include ~/listings/getting-started/testing_how_to/tests/test_contract.cairo:tests_with_events] +``` + +:::info +For more details about events, visit the [Events](/getting-started/basics/events) section. +::: + +## Testing Best Practices + +1. **Test Environment**: snforge bootstraps a minimal blockchain environment for predictable test execution +2. **Assertions**: Use built-in assertion macros for clear test conditions: + - `assert_eq!`: Equal comparison + - `assert_ne!`: Not equal comparison + - `assert_gt!`: Greater than comparison + - `assert_ge!`: Greater than or equal comparison + - `assert_lt!`: Less than comparison + - `assert_le!`: Less than or equal comparison +3. **Test Organization**: Group related tests in modules and use descriptive test names + +## Next Steps + +For more advanced testing techniques and features, consult the [Starknet Foundry Book - Testing Contracts](https://foundry-rs.github.io/starknet-foundry/testing/contracts.html). diff --git a/pages/getting-started/testing/testing.mdx b/pages/getting-started/testing/testing.mdx new file mode 100644 index 00000000..34104054 --- /dev/null +++ b/pages/getting-started/testing/testing.mdx @@ -0,0 +1,19 @@ +# Testing Contracts + +Testing plays a crucial role in software development, especially for smart contracts. In this section, we'll guide you through the basics of creating test suites for your contracts. + +There's two main *test runners* for Cairo: + +
+| Test Runner | Scope | Best For | +|----------------------------------------------------|---------------------|-------------------------------| +| **[Snforge](./testing/testing-snforge.md)** | Starknet contracts | Full smart contract testing with blockchain state | +| **[Cairo Test](./testing/testing-cairo-test.md)** | Pure Cairo packages | Non-blockchain-state logic validation / testing | +
+ +:::warning[Recommendation] +Start with **Snforge** for Starknet contracts! + +When you project is getting more complex, try to separate the logic into pure Cairo packages. +You can then use **Cairo Test** to test these packages independently. +::: \ No newline at end of file diff --git a/routes.ts b/routes.ts index 479388a2..dfd1d7be 100644 --- a/routes.ts +++ b/routes.ts @@ -80,7 +80,17 @@ const config: Sidebar = [ }, { text: "Testing contracts", - link: "/getting-started/testing/contract-testing", + link: "/getting-started/testing/testing", + items: [ + { + text: "With Snforge", + link: "/getting-started/testing/testing-snforge", + }, + { + text: "With Cairo Test", + link: "/getting-started/testing/testing-cairo-test", + }, + ], }, { text: "Syscalls Reference",