IMO the best way is to split canister interface and canister logic so you can test the logic without a canister interface. Have a look at how we do it for the asset canister: interface and canister logic
Another option is to use use the StateMachine to run your canister on a replica with a Rust interface. I don’t know of a public example of StateMachine tests off the top of my head