Teste de Simulação (Rust)

13 min read
To Share and +4 nLEARNs

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  neartestnetalice. 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_methodchange_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_callfunction_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.

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.
  • #[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.
16
Scroll to Top