Antes de começarmos
Este artigo não mostra as possibilidades que um framework de teste de simulação pode fazer; ele discute sobre como criar uma estrutura de teste de simulação MVP para seu projeto. Para alterações mais avançadas, faça sua própria pesquisa (DYOR) e confira alguns links incluídos neste artigo.
NOTA: Os testes de simulação ficarão obsoletos quando o Sandbox surgir. Confira workspaces-rs e workspaces-js .
Breve introdução
A diferença entre teste de unidade e teste de simulação é: o primeiro é restrito a testes dentro da biblioteca; enquanto o último simula uma chamada de fora para dentro. Os testes de unidade testam principalmente os frameworks internos para a lógica correta; enquanto o teste de simulação tenta simular as ações do usuário e verificar se as coisas acontecem como o desenvolvedor espera. Trate cada função como uma simulação.
Se você imaginou com Um teste Uma afirmação, por favor, não faça isso. Mesmo para teste de unidade, não se concorda com uma afirmação por estrutura de função de teste; o que diz que testes de simulação requerem mais de uma afirmação dentro de uma única função/simulação.
Como sempre, o exemplo mais simples é o aplicativo Greeting; então vamos usar isso e escrever um teste de simulação para ele.
Teste de simulação do Greeting
Você não precisa escrever seu próprio aplicativo de saudação: basta executar este comando:
npx create-near-app --contract=rust <your_project_name>
Pronto! Agora você tem um exemplo de aplicativo de saudação com um frontend simples. Siga as instruções para implantar o contrato e testá-lo até se familiarizar antes de prosseguir. Verificando as últimas linhas da saída.
We suggest that you begin by typing:
cd <your_project_name>
yarn dev
Copie o comando e teste como o aplicativo funciona até se familiarizar com ele. Após o teste, pare a implantação temporariamente: é muito chato reimplantar toda vez que você faz uma(s) pequena(s) alteração(ões) do ponto de vista de alguém, se não precisarmos do frontend.
Criar pasta de teste de simulação
Sem considerar NEAR, o teste de simulação também é chamado de teste de integração em (puro) Rust. Se você quiser saber mais, consulte O Livro Rust ou Exemplos de Rust para verificar como realizar testes de integração.
Em suma, você precisa de uma pasta totalmente fora do src chamado tests. Então vamos fazer isso agora. Supondo que você já esteja cd no diretório do seu projeto, execute isto:
cd contract
mkdir tests
mkdir tests/sim
touch tests/sim/main.rs
Navegue e abra <your_project_name>/contract/tests/sim/main.rs e abra-o no editor de sua escolha.
Agora, você pode despejar tudo em um arquivo ou dividi-lo. Geralmente, você tem utils.rs para fazer funções que não sejam de teste que são reutilizadas. Você pode ter outro arquivo .rs para vários testes curtos. Se você tiver testes mais longos, como os testes de simulação do contrato de bloqueio , sugere-se um arquivo por teste; e nomeie seu arquivo de acordo. Você não gostaria que seus leitores vissem um arquivo com mais de 1.000 linhas de código.
Agora, mesmo que o Greeting não tenha longos testes de simulação, você vai mostrar o que se quer dizer com um teste por arquivo separando-os deliberadamente. Você não tem que fazer isso, no entanto.
Incluir dependências
Precisamos de uma biblioteca chamada near-sdk-sim. Este exemplo usa near-sdk v3.1.0, então usaremos a near-sdk-sim v3.2.0 correspondente (não se sabe por que eles não combinam). Tenha em mente, no entanto, se a versão não corresponder, os testes de simulação não serão executados, porque diz Import near_sdk::some_fn is not equal to near_sdk::some_fn, o que é confuso (mas na verdade near_sdk::some_fn é diferente de near_sdk::some_fn: eles têm versões diferentes!)
Na verdade, gostamos de escrever com near-sdk v4.0.0-pre.4, mas isso requer algumas alterações no contrato para que ele seja executado. As mudanças não são complicadas, principalmente sobre AccountId não é mais String e algumas outras coisas menores; mas esse não é o objetivo deste artigo, então vamos ficar com o 3.1.0 e talvez atualizar este artigo no futuro se ele for atualizado. (Além disso, há algum bug com a simulação v4, sobre o qual falaremos mais tarde).
Vamos para Cargo.toml: agora deve ficar assim: (veja nova seção dev-dependencies). É importante garantir que rlib está presente também!
[package]
name = "greeter"
version = "0.1.0"
authors = ["Near Inc <[email protected]>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
near-sdk = "3.1.0"
[dev-dependencies]
near-sdk-sim = "3.2.0"
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []
Anteriormente, escreveu-se um guia sobre a redução do tamanho do contrato ; incluindo a remoção de “rlib”. Para usar testes de simulação, no entanto, é necessário “rlib”, então é uma troca entre o tamanho do contrato e fazer testes de simulação.
Se você parou em yarn dev, agora compile o contrato uma vez para baixar a biblioteca. Em contract, execute:
cargo build
Certifique-se de ter muito espaço em disco (recomendado 20 GB de espaço livre), pois isso ocupa bastante espaço!
Para uma experiência tranquila durante a primeira compilação , tente compilar em uma máquina com 16 vCPU e 8 GB de RAM. Especialmente librocksdb-sys levará muito tempo para compilar. Não está claro se a compilação é transferível (certamente não no sistema operacional, mas não tenho certeza se no mesmo sistema operacional). Como alguém alugou uma VM no Azure, pode-se facilmente alterar o tamanho temporariamente e alterá-lo de volta para um tamanho menor (e mais barato) após a compilação, portanto, sem conflito.
Vamos voltar para escrever os testes de simulação em main.rs.
Prepare o arquivo wasm
Esteja ciente : toda vez que você fizer alterações em seu contrato, precisará reconstruir o wasm. Fizemos um script aqui para construir seu arquivo wasm e movê-lo para a pasta res do diretório superior.
Então execute isto: (do diretório de contrato)
mkdir res
touch build.sh
Em seguida, inclua o conteúdo abaixo contract/build.sh para que você possa executar bash build.sh (dentro da pasta contract) em vez de digitar o comando todas as vezes.
#!/bin/bash
set -e
export WASM=greeter.wasm
RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/$WASM res/
Importações
Isso é fácil: tratamos a biblioteca como uma biblioteca. Lembre-se, aqueles que estão pub(crate) originalmente na biblioteca não podem ser usados em testes de simulação (já que está fora). É como se você compilasse e alguém estivesse usando seu código. Certifique-se de importar as funções que você precisa.
Se verificarmos o src.lib.rs, veremos que há (apenas) uma estrutura Welcome. Ao importá-lo, você adiciona a palavra-chave Contract atrás. Por exemplo:
use greeter::{ WelcomeContract };
Para o contrato de bloqueio , sua estrutura principal é chamada LockupContract, portanto, quando eles importam, é LockupContractContract. Não se sabe por que fizeram assim, talvez para não ter conflito; basta adicionar!
Incluir arquivos de contrato
A próxima coisa antes de testarmos se funciona é incluir o arquivo wasm. Esta é uma obrigação. Além disso, se você tiver um utils.rs, isso NÃO deve ser colocado lá; caso contrário, você precisa pensar muito em como torná-lo detectável de outros arquivos. Para não pensar muito, colocamos em main.rs:
// Directories are relative to top-level Cargo directory. near_sdk_sim::lazy_static_include_bytes! { GREETER_WASM_BYTES => "res/greeter.wasm" // other wasm bytes. }
O que se entende por “diretório de carga de nível superior” significa o diretório contract. Certamente você pode descobrir coisas fora dele ../../some_file se precisar. Por exemplo, se você não usar o res mas o yarn dev, o out/main.wasm estará fora do diretório do contrato. Para importar, fazemos:
near_sdk_sim::lazy_static_include_bytes! { GREETER_WASM_BYTES => "../out/main.wasm" }
Como esta é uma macro , certifique- se de não colocar acidentalmente uma “vírgula” (“,””) após o último item ; caso contrário, você pode receber mensagens de erro estranhas e o Rust se recusa a compilar.
Infelizmente, não pudemos testar essa função enquanto ela funciona até que criamos a função auxiliar (um MVP completo) e um teste de MVP.
Função de inicialização
Para não repetir a função de configuração, nós os incluímos dentro de basic_setup(). Verifique o contrato basic_setup() de bloqueio para outro exemplo (que inclui a implantação de outros contratos além do contrato de teste principal). Aqui, também faremos o mesmo, mas não temos outro contrato para configurar, então vamos pular isso e apenas incluir as funções necessárias em basic_setup().
Faça um utils.rs:
touch tests/sim/utils.rs
Dentro de utils.rs, inserimos o conteúdo:
use crate::*; /// 300 TGas pub const MAX_GAS: u64 = 300_000_000_000_000; /// 1 NEAR (just a random number) pub const MIN_BALANCE_FOR_STORAGE: u128 = 1_000_000_000_000_000_000_000_000; pub const GREETER_ACCOUNT_ID: &str = "greeter"; pub(crate) fn basic_setup() -> (UserAccount, UserAccount) { let mut genesis_config = GenesisConfig::default(); genesis_config.block_prod_time = 0; let root = init_simulator(Some(genesis_config)); let alice = root.create_user( "alice".to_string(), to_yocto("200") ); (root, alice) }
Há algumas coisas importantes a serem observadas aqui. O primeiro é este bloco de código:
let mut genesis_config = GenesisConfig::default(); genesis_config.block_prod_time = 0; let root = init_simulator(Some(genesis_config));
O Genesis é o primeiro bloco do blockchain. No entanto, gênese e tempo de gênese não são realmente os mesmos. Por exemplo, o root representa o próprio blockchain. Não é a conta near de testnet nível superior: é a blockchain. No entanto, se você verificar o explorer em testnet , veremos que ele foi criado durante o Genesis. Então, o root vem primeiro, depois é empacotado com algumas das contas durante o Genesis time. Criamos um simulador do Genesis chamado root.
Aqui, o que queremos dizer com Gênesis não é o tempo da gênese, mas a “raiz última”. É a “conta pai” de todas as contas de nível superior.
Normalmente, não precisamos modificar o GenesisConfig; e se precisar, este é um exemplo.
Se você precisar fazer alterações no genesis, confira os documentos para valores que você pode alterar. Em seguida, você pode modificá-la na linha 2 no bloco de código acima, atribuindo um valor a cada campo. Finalmente, você precisa inicializar o simulador com init_simulator.
Se você não precisa de modificação, você pode inicializar um simulador sem configuração (que usará o padrão) assim:
let root = init_simulator(None);
Em seguida, temos a criação root de uma conta chamada “alice” para nós. O primeiro argumento é o nome da conta, o segundo é quantos NEAR dar à conta.
Por root ser o Genesis, ele só pode criar contas de nível superior como near, testnet, alice. Ele não pode criar subcontas como alice.near: apenas a conta pai near pode criar alice.near, não o Genesis.
Uma coisa que não temos aqui é a implantação com root. Para nosso contrato, usamos a deploy! macro que faremos na função de teste em vez de aqui. Mas se você tiver outro arquivo wasm, como o contrato de bloqueio, eles não podem usar a deploy! macro, então foi assim que eles fizeram.
Por exemplo, no contrato da lista de permissões; ele é implantado na raiz assim:
let _whitelist = root.deploy_and_init( &WHITELIST_WASM_BYTES, STAKING_POOL_WHITELIST_ACCOUNT_ID.to_string(), "new", &json!({ "foundation_account_id": foundation.valid_account_id(), }).to_string().into_bytes(), to_yocto("30"), MAX_GAS, );
Como o requisito de uma função init ser chamada uma vez durante a implantação é tão comum, existe uma função deploy_and_init. Se o contrato não tiver uma função de implantação (supondo que a lista de permissões não tenha uma aqui), podemos fazer isso.
let _whitelist = root.deploy( &WHITELIST_WASM_BYTES, STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), to_yocto("30") );
e na realidade não há função deploy_and_init, então a chamamos manualmente. Para fazer isso, precisamos de uma conta para chamá-lo que tenha a capacidade de fazê-lo. Para o bloqueio, é a base
let foundation = root.create_user("foundation".to_string(), to_yocto("10000")); foundation.call( STAKING_POOL_WHITELIST_ACCOUNT_ID.to_string(), "new", &json!({ "foundation_account_id": foundation.valid_account_id() }).to_string().into_bytes(), MAX_GAS, NO_DEPOSIT, ).assert_success();
Notamos que a realidade e a simulação possuem algumas diferenças.
Por fim, não se esqueça de importá-lo main.rs e importar as funções necessárias:
use near_sdk::Balance; use near_sdk_sim::runtime::GenesisConfig; use near_sdk_sim::{init_simulator, to_yocto, UserAccount}; pub(crate) mod utils; use utils::*;
Construindo a primeira função de teste
Estamos prontos para construir a primeira função de teste. Primeiro, importe as funções necessárias em main.rs:
use near_sdk_sim::{deploy};
Esta é a macro de implantação.
Crie um arquivo para o teste:
touch tests/sim/test_set_and_get_greeting.rs
Como venho de um background em Python, gosto de nomear funções começando com test. Você não precisa. Aqui, adoto nomear o nome do arquivo começando com test_; enquanto a função de teste de ação dentro sem. Exemplo, teremos uma set_and_get_greeting() função dentro test_set_and_get_greeting.rs de (arquivo).
Importe o arquivo main.rs antes que esqueçamos:
mod test_set_and_get_greeting;
Não precisamos de pub(crate) como utils, pois não precisa compartilhar nada com outros arquivos.
A primeira coisa que precisamos na função set_and_get_greeting é implantar o contrato.
let greeter_amount = to_yocto("1000"); let (root, alice) = basic_setup(); let greeter = deploy!( contract: WelcomeContract, contract_id: GREETER_ACCOUNT_ID.to_string(), bytes: &GREETER_WASM_BYTES, signer_account: root, deposit: MIN_BALANCE_FOR_STORAGE + greeter_amount );
Se tivermos um método personalizado #[init], incluímos estes após os argumentos deposit:
gas: MAX_GAS, init_method: <method_name>(method_args)
No entanto, se não tivermos, nós os removemos. Para um monte de características que correspondem à macro, verifique os documentos . Você precisa combinar pelo menos um deles; caso contrário, Rust se recusa a compilar.
(Pergunta: A ordem importa? Ou apenas o bando de kwargs precisa corresponder a uma das características?)
Observe que, diferentemente da realidade, a implantação é feita root novamente (você pode ver em signer_account). Na realidade, é feito por alguma conta responsável por isso.
Em seguida, vamos definir uma saudação e obter a saudação e afirmar que estão conforme o esperado.
Parece que as pessoas gostam de atribuir uma chamada de variável res que é reutilizada repetidamente. Não é a maneira mais clara; mas certamente podemos fazer isso para não sobrecarregar nossa cabeça e pensar em nomes de variáveis. res significa apenas “resultados” retornados de uma chamada de função específica.
É uma boa prática atribuir seu res um tipo (independentemente de Rust poder inferir o tipo ou não), para que você saiba qual tipo é retornado.
Lembre-se que temos view_method e change_method em contrato inteligente. Para o contrato implantado deploy! (que é o contrato inteligente que você pode importar e aquele que você está testando principalmente), podemos usar view_method_call e function_call respectivamente. Falaremos daqui a pouco se tivermos como chamar externo.
Nosso set_greeting é um change_method, então usaremos uma function_call. Uma .function_call leva em a um PendingContractTx, Gas e Deposit.
O PendingContractTx é apenas a função, e outros argumentos são fáceis de interpretar o que é. Vamos ver o nosso set_greeting:
let greeting: String = "Hello, NEAR!".to_owned(); alice.function_call( greeter.contract.set_greeting(greeting), MAX_GAS, 0 ).assert_success();
Certifique-se de passar os respectivos argumentos na função. Também chamamos assert_success() no final para garantir que a Promessa seja cumprida. O acima está imitando o near-cli:
near call $GREETER_CONTRACT set_greeting '{
"message": "Hello, NEAR!"
}' --gas=$MAX_GAS --amount=0 --accountId=$ALICE_ACCOUNT_ID
Então, podemos ter a chamada da função view. Se você verificar a função, get_greeting pega um ID de conta do tipo String e retorna um String.
let res: String = alice.view_method_call( greeter.contract.get_greeting(alice.account_id().to_string()) ).unwrap_json();
Suspeita-se que você não precisa .account_id().to_string(). Apenas .account_id() é suficiente. Aqui, estamos apenas tornando-o explícito porque ele recebe uma String. Se AccountId receber , podemos simplesmente chamar .account_id() sem qualquer confusão. (Especialmente quando AccountId não é mais igual a String iniciar near-sdk-rs v4.)
Como o resultado retornado é um JSON, nós o desempacotamos com unwrap_json().
Então, poderíamos fazer asserções sobre o resultado.
assert_eq!(res, greeting);
Recall greeting é uma variável que atribuímos anteriormente, que é “Olá, NEAR!”.
Executando o teste de integração
Se você quiser apenas executar o teste de integração, execute
cargo test –tests sim (porque está na pasta tests/sim). Se você deseja executar todos os testes, incluindo testes de unidade, execute cargo test.
Observe que, por algum motivo, leva 30 segundos ou mais (independentemente de quantos núcleos de CPU você possui); você tem que esperar antes mesmo do teste começar.
Um lembrete novamente: se você fizer alterações no contrato, precisará reconstruí-lo; caso contrário, você se perguntará por que não funciona e acredita que funcionará agora, então…
Código completo
Você pode encontrar o código completo aqui: https://github.com/Wabinab/NEAR_greeter_integration_tests
Conclusão
Agora, você pode repetir outros testes (se houver) criando um novo arquivo, vinculá-lo usando mod para main.rs, escrever os testes dentro. É um exercício divertido: quanto mais você escreve, mais você entende.
Uma nota sobre a atualização para v4.0.0-pre.4
A função de contrato da lista de permissões deploy_and_init precisa de alterações nestes:
AccountId não é mais String
- Então substitua tudo “alice”.to_string() por “alice”.parse().unwrap(). Se a substituição estiver dentro de uma função que não pode parse, você precisa criar uma variável. (Isso é especialmente verdadeiro na deploy! macro, que não tem inferência de tipo).
let alice_account: AccountId = "alice".parse().unwrap(); // pass it to the function.
- valid_account_id é preterido. Use account_id() em vez disso. Isso ocorre em json!.
- Qualquer número inteiro passado para json! requer especificação. Exemplo: v3.2.0 permite 10, mas v4.0.0-pre.4 não permite: você deve dizer 10u64 ou qualquer outro tipo.
- A #[quickcheck] macro tem um bug e falha no teste com v4. Um arquivo um erro no Github ; até o momento, a equipe de desenvolvimento ainda não respondeu.