Como desenvolvo e teste APIs com minha “bicicleta” PieceofScript

PieceofScript é uma linguagem simples para escrever scripts para teste automático da API HTTP JSON.

O PieceofScript permite:

  • descrever métodos de API no formato YAML, com o nome do método em uma linguagem quase natural, o que é conveniente para a leitura de testes
  • flexível o suficiente para descrever modelos no formato YAML e gerar dados aleatórios a partir deles
  • escreva scripts de chamada de API complexos em uma linguagem fácil de ler com sintaxe simples
  • obtenha resultados de teste nos formatos JUnit e HTML

Eu escrevi essa "bicicleta" porque a interface do SoapUI me derrubou. Eu queria descrever de forma simples e clara os testes em um editor de texto sem uma GUI especial. Além disso, o git não digere o enorme arquivo xml que o SoapUI emite, por isso é difícil colocar testes para uma tarefa específica no mesmo ramo em que a tarefa foi realizada. A interface do Postman é muito melhor, mas durante o desenvolvimento, leva muito tempo para compor / modificar solicitações lá e repeti-las na sequência desejada. Eu queria automatizar isso. Também estudei outras ferramentas de teste, cada uma com uma " falha fatal "; portanto, no contexto da síndrome do NIH, abri uma IDE.

Aqui está o que veio disso.



O intérprete é escrito em PHP e é um arquivo phar; requer a versão PHP 7.2, embora também possa funcionar no 7.1. Código-fonte e documentação https://github.com/maximw/PieceofScript . Documentação em desenvolvimento. Como se viu, essa é a parte mais difícil e entediante.

Projeto de teste, sua estrutura e lançamento
Script de teste
Métodos de teste da API
Chamada de método da API
Geração de modelos e dados de teste
Funções incorporadas
Casos de teste
Variáveis ​​e escopos
Tipos e Operações
Salvando dados entre execuções
Saída para stdout e relatórios
Exemplos - palavras suficientes, mostre o código!
Comentários e planos para o futuro, se houver

Projeto de teste, sua estrutura e lançamento


O projeto é um diretório com um conjunto de arquivos de script, arquivos de descrição do método API e geradores de dados de teste.

Na versão mínima, o projeto fica assim:

./tests endpoints.yaml -  API generators.yaml -  start.pos -    

O arquivo de inicialização é o script a partir do qual o processo de teste começa. É definido na inicialização:

 pos.phar run ./start.pos --junit=junit_report.xml -vvv --config=config.yaml 

Todos os caminhos relativos são lidos no diretório de trabalho que contém o arquivo inicial.
O arquivo de configuração pode ser especificado na linha de comandos com a opção --config ou coloque config.yaml no diretório de trabalho. A configuração é opcional, você precisa subir lá conforme necessário. Mais sobre a configuração .

Script de teste


Decidi escrever scripts em arquivos com a extensão .pos, para que você possa fazer configurações de destaque de código no IDE com uma extensão de extensão. Mas o intérprete é completamente indiferente à extensão.

Aqui está um exemplo de script simples para um fórum imaginário em que o teste de criação e leitura de uma postagem por diferentes usuários é realizado.

 require "./globals.pos" //    ,  $domain include "./globals_local.pos" //     ,    include "./user/*.pos" //       ./user  ./post include "./post/*.pos" //       var $author = User() var $reader = User() var $banned = User() var $post = Post() Register $author //   API,     must $response.code == 201 //     Register $reader must $response.code == 201 Register $banned must $response.code == 201 Add $banned to blacklist of $author //$banned      $author Create post $post by $author //   API   must $response.code == 201 //    API   201 // ...,   ,       assert $response.body.post.content == $post.content var $postId = $response.body.post.id // Id     Read post $postId by $author //     must $response.code == 200 assert $response.body.post.content == $post.content Read post $postId by $reader //      must $response.code == 200 assert $response.body.post.content == $post.content Read post $postId by $banned //        assert $response.code == 404 

Sim, não parece muito bom sem luz de fundo.

Cada linha do script começa com um operador ou é uma chamada para um método de API. Se de repente o nome do método da API começar com uma palavra que corresponda a um dos operadores, você poderá usar o símbolo " > ":

 >Include $user to group $userGroup 

Os operadores não diferenciam maiúsculas de minúsculas. assert, ASSERT ou aSsErT (mas por que escrever assim?) funcionará.
Cada instrução ou chamada de método da API deve estar em uma linha separada. Mas a quebra de linha também é possível se o último caractere da string for \ (hello, Python).

Detalhes desinteressantes sobre quebras de linha e recuos
Se a quebra de linha for usada em um comentário, a próxima linha também será considerada parte do comentário. Ao agrupar linhas dentro de blocos (caso de teste , se , enquanto , foreach ), é importante recuar para que a próxima linha caia no mesmo bloco.

 var $n = 20 var $i = 2 var $fib1 = 1; \ $fib2 = 1 while $i <= $n var $fib_sum = \ $fib2 + $fib1 print toString($i) + "  :" + \ toString($fib_sum) var $fib1 = $fib2 var $fib2 = $fib_sum var $i = $i + 1 

Ao executar instruções de bloco ( testcase , if , while , foreach ), um bloco é determinado pela indentação de suas linhas. O recuo é contado como o número de espaços em branco no início de uma linha. O espaço e a guia contam como um caractere, mas as guias geralmente são exibidas nos editores como vários espaços. Portanto, para evitar confusão, é melhor usar guias ou espaços, mas não todos juntos.

Lista completa de operadores


exigir nome do arquivo - anexe o arquivo ao local em que o operador é chamado. O arquivo anexado será iniciado imediatamente a partir da primeira linha. Após a conclusão, o intérprete retorna para a próxima linha do arquivo de origem. Se o arquivo solicitado não estiver legível, um erro será gerado. O caminho relativo é calculado a partir do diretório ativo.

include fileMask - semelhante ao requerido, mas se o arquivo solicitado não for legível, não haverá erro. Isso é conveniente, por exemplo, para criar configurações para diferentes ambientes de teste. Além disso, o include pode conectar todos os arquivos por máscara de uma vez. Portanto, por exemplo, você pode baixar diretórios inteiros de arquivos que contêm casos de teste. Mas, ao mesmo tempo, qualquer ordem de download de arquivos não é garantida.

var $ variável1 = expressão1 ; $ variável2 = expressão2 ; ...; $ variableN = expressionN - atribua valores às variáveis. Se a variável ainda não existir, ela será criada no contexto atual.

deixe $ variável1 = expressão1 ; $ variável2 = expressão2 ; ...; $ variableN = expressionN - atribua valores às variáveis. Se a variável não estiver no contexto atual, haverá uma tentativa de criar ou modificar a variável no contexto global.

const $ const1 = expressão1 ; $ const2 = expressão2 ; ...; $ constN = expressionN - define o valor das constantes no contexto atual. A diferença entre constantes e variáveis ​​é apenas que elas não podem ser alteradas; quando você tenta atribuir um valor a uma constante, um aviso será emitido após a declaração. Se já houver uma variável com o mesmo nome, será gerado um erro ao tentar declará-la uma constante. Caso contrário, tudo o que é verdadeiro para variáveis ​​também é verdadeiro para constantes.

import $ variable1 ; $ variável2 ; ...; $ variableN - copia variáveis ​​do contexto global para o atual. Pode ser útil se você precisar operar com o valor de uma variável global, mas não alterá-lo.

testcase testCaseName - anuncia um caso de teste, que pode ser chamado como uma unidade com a instrução run . Leia mais sobre casos de teste posteriormente neste artigo .

afirmar expressão - verifique se a expressão é verdadeira ; caso contrário, imprima um relatório sobre o teste que falhou.

expressão deve ser a mesma que afirmar , somente se o teste falhar, o caso de teste atual será interrompido. E fora do contexto do caso de teste, o script será finalizado por completo. Pode ser usado se for detectado um erro com o qual verificações adicionais não fazem sentido.

executar testCaseName - execute o caso de teste especificado para execução. executar sem especificar o nome do caso de teste iniciará todos os casos de teste declarados que não exigem argumentos na ordem de sua declaração.

while expression - um loop, enquanto expression for true, executa instruções com linhas recuadas mais do que while .

foreach $ array ; $ element - passa pela matriz, o corpo da malha é executado para cada próximo elemento da matriz. Também é possível obter a chave para cada array $ ; chave $ ; elemento $ . As variáveis $ key e $ element são criadas / substituídas no contexto atual.

se expressão - se expressão for verdadeira, executa instruções com linhas recuadas mais do que se

print expression1 ; expression2 ; ... expressionN - imprime o valor de expressionM em stdout. Ele pode ser usado para depuração, funciona apenas com o nível de "capacidade de conversação" --verbosity = 1 ou -v e mais.

expressão do sono - pausa para um determinado número, opcionalmente um número inteiro, segundos. Às vezes, você precisa dar uma pausa na API testada.

expressão de pausa - não no modo interativo (opção de linha de comando -n ) é semelhante ao modo de suspensão . A expressão é opcional; nesse caso, não haverá pausa. E no modo interativo, faça uma pausa antes de pressionar Enter.

cancele o teste final. O intérprete termina o trabalho, cria relatórios.

Métodos de teste da API


Na verdade, é isso que você precisa testar - ligue com determinados parâmetros e verifique se a resposta atende às expectativas.

Os métodos de API são descritos no formato YAML. Por padrão, as descrições devem estar no arquivo endpoints.yaml do diretório atual e / ou nos arquivos * .yaml em seu subdiretório ./endpoints . Antes do teste, o intérprete tentará ler todos esses arquivos de uma vez.

Exemplo de estrutura endpoints.yaml :

 Auth $user: method: "POST" url: $domain + "/login" headers: Content-Type: "application/json" format: "json" data: login: $user.login password: $user.password after: - assert $response.code == 200 - let $user.auth_token = $response.body.auth_token Create post $post by $user: method: "POST" url: $domain + "/posts" format: "json" data: $post headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert $response.code == 201 Read post $postId by $user: method: "GET" url: $domain + "/posts/" + $postId headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert ($response.code == 200) || ($response.code == 404) Create comment $comment on $post by $user: method: "POST" url: $domain + "/comments/create/" + $post.id format: "json" data: $comment headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert $response.code == 201 

O nome do método da API (o nível superior da estrutura YAML) pelo qual ele pode ser chamado é uma sequência em um formato quase arbitrário.

Os argumentos podem ser especificados em qualquer lugar do nome. Eles devem ser separados por espaços do restante das palavras. Por exemplo, $ comment , $ post e $ user no último método.

Além disso, em qualquer lugar do nome, você pode especificar valores opcionais do método entre colchetes duplos.

 Get comments of $post {{$page=1; $perPage=$defaultGlobalPageSize}}: method: "GET" url: $domain + "/comments/" + $post.id query: page: $page per_page: $perPage headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert $response.code == 200 

Nas expressões que especificam valores opcionais, variáveis ​​de contexto global estão disponíveis.
Os valores opcionais podem ser úteis para que você não os especifique sempre que chamar o método da API. Se o tamanho da página precisar ser alterado em apenas um lugar, por que indicá-lo em todos os outros lugares? Exemplos de chamadas para este método:

 Get comments of $newPost //     $page  $perPage Get comments of $newPost {{$page=$currentPage+1}} Get comments of {$newPost} {{$perPage=10;$page=100}} 

O restante das variáveis ​​usadas ( $ domain no exemplo acima) serão retiradas do contexto global. Mais adiante falarei sobre os contextos .

Parece-me conveniente fornecer nomes de legibilidade humana aos métodos de API em uma linguagem natural, para que o script de teste seja mais fácil de ler. Os nomes não diferenciam maiúsculas de minúsculas, ou seja, o método Auth $ User pode ser chamado como auth $ User e como AUTH $ User . No entanto, os nomes de variáveis ​​diferenciam maiúsculas de minúsculas, mais sobre as variáveis abaixo.

Nota importante. O formato YAML permite que você não inclua seqüências de caracteres entre aspas. Mas para o intérprete, uma sequência sem aspas é uma expressão que precisa ser avaliada. Por exemplo, declarar um campo url: http://example.com/login resultará em um erro de sintaxe durante a execução. Portanto, ele estará correto: url: "http://example.com/login" ou url: "http://"+$domain+"/login"

Campos de descrição do método da API


method - método HTTP necessário

url - o URL real, necessário

headers - lista de cabeçalhos HTTP, opcional

cookies - lista de cookies opcional

auth - dados para autenticação HTTP, opcional

 auth: login: $login password: $password type: "basic" //  "digest"  "ntlm", - "basic" 

query - uma lista de parâmetros de URL, opcional

formato - um dos valores:

  • none - a solicitação não tem corpo
  • json - enviar para JSON
  • bruto - envie a string "como está"
  • form - no formato application / x-www-form-urlencoded
  • multipart - no formato multipart / data-form

Opcional, nenhum padrão

data - request body, será enviado no formato especificado no formato , opcional

  • Para nenhum - o formato dos dados pode estar ausente, se presente, será ignorado
  • Para o formato json , qualquer valor
  • Para o formato bruto , qualquer valor escalar
  • Para o formato do formulário , uma matriz cujas chaves são nomes de campos:

     data: login: "Bob" password: $password remember_me: 1 
  • Para o formato multipartes , uma matriz da seguinte estrutura:

     data: user_id: value: 42 headers: X-Baz: "bar" avatar: file: "/path/to/file" photo: file: "http://url.to/file" filename: "custom_filename.jpg" 

Os arquivos especificados nos campos do arquivo devem ser legíveis. Se uma URL for especificada, o allow_url_fopen deverá ser ativado no php.ini

before - instruções que serão executadas antes da solicitação HTTP, opcional

after - instruções que serão executadas após a solicitação HTTP, opcional

A idéia dos bloqueios antes e depois na execução de verificações ou no processamento de quaisquer dados necessários sempre que antes ou depois da execução da solicitação HTTP é ditada não tanto pelas necessidades de teste quanto pela lógica de negócios. Por exemplo, copiando o token de autorização emitido no campo da estrutura $ user para chamar todos os métodos de API subseqüentes em nome desse usuário. Ou para verificar o status HTTP da resposta, para não verificar todas as vezes após uma chamada no script.

Chamada de método da API


Para chamar o método API no script, você precisa especificar seu nome e parâmetros, se necessário. Aqui está um exemplo de chamada do último método de API da descrição acima:

 Create comment $comments.1 on {$newPost} by {$postAuthor} 

Se o parâmetro estiver entre colchetes, ele será passado por valor - dessa forma, você pode passar qualquer expressão. Se você especificar um parâmetro sem colchetes, ele será passado por referência - pode ser apenas variáveis ​​e acessos estáticos aos elementos da matriz (por um período, mas não por colchetes []).

 Create comment {$comments[$i]} on $posts.0 by $users.1 Read post {123} by $user Get comments of $users.1.id {{$page = 2}} 

Cada vez que o método API é chamado no contexto da própria chamada (nas listas de instruções antes e depois ) e no contexto em que foi chamado, as variáveis $ request e $ response são criadas . Estes são nomes reservados, não recomendo usá-los para outros fins. $ request está disponível nos blocos antes e depois , e $ response está somente em depois , em antes que seu valor se torne Nulo . No contexto de chamada, essas variáveis ​​estão disponíveis até a próxima chamada de método da API, onde serão reinicializadas.

$ Estrutura de solicitação


$ request.method - String - método HTTP
$ request.url - String - o URL solicitado
$ request.query - Array - uma lista dos parâmetros GET
$ request.headers - Matriz - lista de cabeçalhos de solicitação
$ request.cookies - Matriz - lista de cookies
$ reuqest.auth - Matriz ou Nulo - dados para autenticação HTTP
$ request.format - String - solicita o formato dos dados
$ request.data - digite qualquer - o que foi calculado no campo de dados

Estrutura de resposta


$ response.network - Boolean - false se o erro ocorreu no nível da rede abaixo de HTTP
$ response.code - Number or Null - código de resposta, por exemplo, 200 ou 404
$ response.status - String ou Null - status da resposta, por exemplo, “204 No Content” ou “401 Unauthorized”
$ response.headers - Array - lista de cabeçalhos de resposta, nomes de cabeçalho em minúsculas
$ response.cookies - Matriz - lista de cookies
$ response.body - qualquer tipo de corpo de resposta processado como JSON; se houver um erro durante a análise, o elemento body não existirá : @response.body == null ( sobre a verificação da existência de variáveis )
$ response.raw - String ou Null - o corpo de resposta bruto
$ response.duration - type Number - duração da solicitação em segundos

Geração de modelos e dados de teste


Geradores são usados ​​para descrever modelos e gerar dados de teste a partir deles. As descrições no formato YAML devem estar no arquivo generators.yaml no diretório de trabalho e / ou nos arquivos * .yaml no subdiretório ./generators .

 User: body: login: Faker\login() name: Faker\name() email: Faker\email() password: Faker\text(16) child: Child() birthday: dateFormat(Faker\datetime(), "U") settings: notifications_enabled: Faker\boolean() Child: body: name: Faker\name() gender: Faker\integer(1, 2) age: Faker\integer(0, 18) Comment($user): body: content: "Hi! I'm " + $user.name tags: - "tag1" - "tag2" 

No exemplo acima, os três geradores User () , Child () e Comment () são declarados. Nesse caso, o último possui o argumento $ user e pode usar esses dados ao gerar. Argumentos para geradores sempre são passados ​​por valor. Além disso, o exemplo usa várias outras funções internas : Faker \ name () , Faker \ email () , dateFormat () etc. Seção sobre funções internas .

Ao chamar o gerador User () do exemplo acima, será gerada uma estrutura parecida com esta no JSON:

 { "login": "fgadrkq", "name": "Lucy Cechtelar", "email": "tkshlerin@collins.com", "password": "gbnaueyaaf", "child": { "name": "Adaline Reichel", "gender": 2, "age": 12 }, "birthday": 318038400, "settings": { "notifications_enabled": true } } 

O valor do campo filho é o resultado do gerador Child () .

Como na descrição dos métodos da API, quaisquer seqüências de caracteres que não estejam entre aspas são tratadas como expressões a serem avaliadas. Isso pode ser não apenas uma chamada para outro gerador, mas uma expressão arbitrária, por exemplo, no gerador Comment ($ user) , o campo content representa a concatenação da string Hi! Eu sou e o nome passou para $ user

Os nomes dos geradores não diferenciam maiúsculas de minúsculas e devem começar com uma letra latina; eles podem conter letras latinas, números, sublinhados e barras invertidas.

Como a sintaxe para chamar geradores e funções internas é a mesma, eles compartilham um espaço para nome comum. Por convenção, sugiro o uso de uma barra invertida como um separador para especificar um "fornecedor" ou uma biblioteca de funções internas , como as funções Faker \ something (), com base na biblioteca github.com/fzaninotto/Faker .

As nuances do uso de geradores, você não pode ler
Usando geradores, você pode compor estruturas de dados:

 # Userredentials     $user Userredentials($user): body: login: $user.email password: $user.password #    .     ,    GlobalSearchResult($posts, $comments, $users): body: posts: title: " " list: $posts comments: title: " " list: $comments users: title: " " list: $users 

GlobalSearchResult não são dados de teste enviados na solicitação para o método API, mas um modelo de resposta que pode ser verificado com o que a API enviará, por exemplo, usando as funções similar () ou idêntica () .

O gerador pode alterar a estrutura obtida no corpo usando estruturas calculadas nos campos substituir e remover . Eu vou te mostrar um exemplo.

Suponha que você já tenha um gerador User () que cria a estrutura de dados correta para o usuário. Agora você precisa verificar como a API responderá se você fornecer dados incorretos. Você pode seguir de duas maneiras:

  • Crie um gerador de usuários "errado" do zero. Porém, obteremos duplicação de código e, mais tarde, por exemplo, ao adicionar um novo campo ao usuário de acordo com as necessidades da lógica de negócios, você precisará fazer alterações em dois locais. SECA!
  • Você pode "herdá-lo" da estrutura User () existente, configurando-a no corpo . E em substituir e remover, defina os campos que serão adicionados / alterados e excluídos.

 #      ,    , #    InvalidUser($user): body: $user replace: email: Faker\String(6, 15) #   password: Faker\String(1, 5) #    new_field: "      ,  " remove: name: size($user.name) < 10 #  ,    10  #      , #        InvalidNewUser: body: User() replace: login: "!@#$%^&*" #   remove: about: true settings: notifications: 100500 #       , #      true 

Quando o gerador está funcionando, a estrutura de dados no corpo é calculada primeiro, depois é substituída e complementada com elementos de replace e , em seguida, os campos especificados em remove são excluídos se seu valor for equivalente a true . Se o resultado do cálculo do corpo , substituir ou remover não for uma matriz, não haverá erro, mas também não há sentido nisso, pois não haverá campos que possam ser substituídos e excluídos.

Funções incorporadas


Lista completa de funções internas . Por exemplo, darei apenas alguns deles.
Após o nome da função e a lista de argumentos, o tipo do valor de retorno é indicado, se um estiver definido.

Operações com variáveis:


similar ($ var, $ sample, $ checkTypes) Booleano - retorna true se os argumentos forem do mesmo tipo, se $ var for uma matriz, todas as chaves de string que estão em $ sample devem estar em $ var , se $ checkTypes é true, então os tipos dos elementos correspondentes devem corresponder. Em outras palavras, os elementos da matriz $ var são um subconjunto dos elementos de $ sample .
idêntico ($ var, $ sample, $ checkTypes) Boolean é um análogo de similar () , com o inverso adicional, no caso de matrizes, todas as chaves de string em $ var também devem estar em $ sample . Em outras palavras, os elementos da matriz $ var são iguais aos elementos da matriz $ sample até o tipo de elemento.
max ($ var1, $ var2, ... $ varN) - o máximo dos valores passados ​​(se puderem ser comparados).
min ($ var1, $ var2, ... $ varN) - o mínimo dos valores passados.
if ($ condition, $ var1, $ var2) - Se $ condition == true, retornará $ var1, caso contrário, $ var2. Substituindo o operador de coaching (Olá MySQL).
escolha ($ condition1, $ var1, $ condition2, $ var2, ..., $ conditionN, $ varN) - Retornará o primeiro encontrado $ varK se $ conditionK == true.

Trabalhar com strings:


size ($ string) Number - o comprimento da string na codificação UTF-8.
regex ($ string, $ regex) Booleano - verifica a expressão regular da string .
regexMatch ($ string, $ regex) Array - retornará uma matriz de strings - combina com os grupos regulares $ regex .

Processamento de matriz:


matriz ($ var1, $ var2, ... $ varN) Matriz - cria uma matriz a partir dos elementos passados.
size ($ array) Number - o número de elementos na matriz.
keys ($ array) Matriz - uma lista de chaves na matriz.
fatia ($ array, $ offset, $ length) Matriz - parte da matriz de $ offset of length $ length, ( mais ).
append ($ array, $ value) Array - adicione um elemento ao final do array.
prepend ($ array, $ value) Matriz - adicione um elemento ao início da matriz.

Data de Processamento:


dateFormat ($ date, $ format) String - formatação de data, ( mais sobre formatos ).
dateModify ($ date, $ formato) Data - altere a data, conveniente para usar com os Formatos relativos .

Geração de dados de teste aleatório:


Faker \ número inteiro ($ min, $ max) Número - número inteiro aleatório de $ min a $ max inclusive
Faker \ ipv4 () String - IPv4 aleatório
Faker \ arrayElement ($ array) String - elemento aleatório da matriz
Faker \ name () String - nome aleatório
Faker \ email () String - email aleatório

Agora não há muitas funções internas. Adicionei apenas o que me parece necessário durante o teste. Você pode adicionar novos recursos conforme necessário em novas versões. E no futuro, se houver demanda, adicionarei a capacidade de criar funções conectadas dinamicamente implementadas como classes especiais em PHP.

Casos de teste


Um caso de teste é uma sequência de instruções que podem ser chamadas como uma unidade. Alguns análogos do procedimento em linguagens de programação.

Um caso de teste é criado pela instrução testcase , seguido pelo nome do caso de teste, com uma sintaxe semelhante aos nomes dos métodos da API . Casos de teste aninhados são proibidos.

 testcase Registration $device //     var $user = User() Register $user on $device assert $response.code == 201 //        var $user = User() //       InvalidUser() var $user.email = "some_bad_email" Register $user on $device assert $response.code == 400 

A instrução run pode chamar um caso de teste separado ou todos os casos de teste que não requerem argumentos.

 run Get all users //   -,    run Get user $user_id //   -   run //   -,     

A idéia desse lançamento é que os casos de teste possam ser usados ​​como testes independentes separados de uma parte da lógica de negócios e como procedimentos para evitar a duplicação de código em cenários de teste complexos.

Os argumentos são passados ​​para o caso de teste por referência ou por valor na analogia completa de transmissão de argumentos para métodos de API.

Variáveis ​​e escopos


Os nomes de variáveis diferenciam maiúsculas de minúsculas e começam com um sinal de $ (sim, sim, sou um pshpshnik).

Se o tipo de variável da matriz , em seguida, o acesso a campos ou elementos de valor individuais é produzido através do ponto: $users.12.password. Entre os pontos, apenas números ou letras latinas, sublinhados e números com a primeira letra latina são permitidos. Os nomes de campo também diferenciam maiúsculas de minúsculas.

Um acesso dinâmico a um elemento da matriz é possível:$post.comments[$i + 1].content

Existem quatro tipos de contextos - o escopo das variáveis.

Contexto global - criado no início, contém todas as variáveis ​​declaradas ao executar instruções fora dos casos de teste e fora das chamadas do método API.

Contexto do caso de teste - um novo é criado toda vez que o caso de teste é executado com a instrução run .

Contexto do método da API - é criado quando o método da API é chamado, quando os operadores especificados nas seções antes e depois são executados .

Contexto do gerador- não há possibilidade nos geradores de criar variáveis ​​novas ou alterar existentes; portanto, as variáveis ​​e argumentos do contexto global são somente leitura. As variáveis ​​sempre são passadas por valor para o contexto do gerador.

Nota importante. Em todos os contextos, as variáveis ​​de contexto global estão disponíveis se seus nomes não forem criados no contexto atual.

Exemplos de operadores para trabalhar com variáveis:

 const $a = 1 let $a = 2 //    var $b = 1 const $b = 3 //    const $c = 3; $c = 4 //   , $c      

 let $a = 1; $a = $a + 1; $a = $a + 2 print $a // 4 

 Testcase Context example changes $argument1, $argument2 and $argument3 var $a = "changed" let $b = "changed" const $c = "changed" import $i let $i = "changed" var $argument1 = "changed" var $argument2 = "changed" var $argument3 = "changed" // ,     var $a = "original" var $b = "original" var $i = "original" const $c = "original"; var $paramByRef = "original" var $paramByVal = "original" const $paramConst = "original" run Context example changes $paramByRef, {$paramByVal} and $paramConst //  $a     - print $a // "original" // $b     -,  let    $b print $b // "changed" // $i      print $i // "original" //      ,  var   print $c // "original" //     print $paramByRef // "changed" //     print $paramByVal // "original" //     ,     print $paramConst // "original" 

Tipo Sistema e Operações


A digitação dinâmica relaxada é usada.

Os valores são armazenados em wrappers nos tipos PHP correspondentes. Para uma melhor compreensão, consulte o sistema de tipos PHP. Fiz um pouco menos de liberdade na conversão de tipo dinâmico. Por exemplo, ao adicionar uma string e um número "2" + 2, um erro será gerado e o PHP executará a adição silenciosamente. Talvez eu precise revisar as regras da digitação dinâmica no futuro, mas até agora tentei encontrar um equilíbrio entre a conveniência e o rigor exigidos para testes confiáveis.

Tipos de dados disponíveis no PieceofScript:

NumberÉ um número. Por motivos de simplicidade, não criei tipos separados para Integer e Float. A única diferença significativa entre o número inteiro e o número real no PieceofScript é o uso de uma matriz como chaves: os números reais serão arredondados para números inteiros.
7 -42 3.14159

String - entre aspas duplas, é possível escapar com uma barra
"I say \"Hello world!\""

Null - definida por constante
null

booleana sem distinção entre maiúsculas e minúsculas - é o resultado de operações booleanas e operações de comparação, definidas por constantes sem distinção entre maiúsculas e minúsculas
true false

Data - data e hora. "Sob o capô" é um DateTime . As constantes são especificadas entre aspas simples, em um dos formatos .
'now', '2008-08-07 18:11:31', 'last day of next month'

Matriz é uma matriz, o único tipo não escalar. Wrap overArray . Não há literais desse tipo, mas matrizes podem ser o resultado do trabalho de geradores, funções internas (por exemplo, array () - olá PHP 5.3 e abaixo), ou você pode simplesmente acessar as chaves variáveis ​​que serão criadas dinamicamente após a atribuição.

 let $a.1 = 100 let $i = 1 let $a[$i + 1] = 200 let $a.sum = $a.1 + $a.2 print " "; $a.sum //  300 var $b = array(true, 3, $a, "Hi") // [true, 3, {1: 100, 2: 200, "sum":300}, "Hi"] 

Ao acessar uma variável ou elemento de matriz inexistente, o script de teste será interrompido e um erro será gerado. Porém, ao executar instruções assert ou must , se uma variável inexistente for acessada, não haverá erro, mas a verificação será considerada com falha.

Verificando a existência e o tipo de uma variável


Devemos mencionar separadamente a construção de verificar a existência e o tipo da variável @ .

Se @ for especificado no nome da variável em vez de $ , o resultado dessa construção será um dos seguintes:

  • uma string com o nome do tipo da variável ou o tipo do elemento da matriz, se chaves foram usadas;
  • null se a variável não for encontrada em contextos acessíveis ou se o elemento com a chave especificada na matriz não existir.

Esse design pode ser útil ao verificar a estrutura das respostas HTTP.

 var $a.string_field = "Hello World" var $a.number_field = 3.14 var $a.bool_field = true var $a.date_field = '+1 day' var $a.null_field = null var $a.array_field = array(1, "2") assert @a.string_field == "String" assert @a.number_field == "Number" assert @a.bool_field == "Boolean" assert @a.date_field == "Date" assert @a.null_field == "Null" assert @a.array_field == "Array" assert @a.array_field.0 == "Number" assert @a.array_field.1 == "String" assert @a.array_field.2 == null assert @notExistedVar == null 

Ou em construções como:

 assert @comment.optional_field && $comment.optional_field > 20 

As operações booleanas são otimizadas no primeiro operando. Se o primeiro operando for falso , a operação && nem tentará calcular o segundo operando. Da mesma forma com || .

Salvando dados entre execuções


Usei scripts separados, não apenas para teste após a conclusão da tarefa, mas também durante o desenvolvimento. Complementei e mudei o script ao implementar o recurso. Posteriormente, esse script se tornou a base para escrever um caso de teste, mas durante o desenvolvimento, era necessário fazer as mesmas chamadas de API repetidamente. Ao mesmo tempo, cada vez no script era muito tempo para criar novas entidades a partir do zero (por exemplo, registro de usuário), criar lixo no banco de dados e interferir de todas as formas no desenvolvimento. Portanto, decidi adicionar a capacidade de salvar e restaurar os valores das variáveis ​​entre as partidas no armazenamento de valores-chave.

O salvamento é ativado pela opção de linha de comando --storage , que define o nome do arquivo de armazenamento:

 pos.phar run ./start.pos --storage=storage.yaml 

Os dados são salvos no formato YAML, o que facilita a leitura e a edição.

storage \ get (string $ key, $ defaultValue, boolean $ $ saveValue = true) - se a chave $ key não existir ou o arquivo de armazenamento não for especificado, retornará $ defaultValue. Caso contrário, ele retornará o valor armazenado. Se o argumento $ saveValue for verdadeiro e a chave $ key não for encontrada, $ defaultValue será gravado lá.

storage \ set (string $ key, $ value) - salva $ value com a chave $ key e retorna $ value. Se o arquivo de armazenamento não tiver sido definido, ele simplesmente retornará $ value.

storage \ key (string $ regexp = null) Matriz- retorna uma matriz de todas as chaves disponíveis. Se o argumento $ regexp não for nulo, as chaves correspondentes a esta expressão regular serão retornadas. Se o arquivo de armazenamento não tiver sido definido, ele retornará uma matriz vazia.

Saída para stdout


O PieceofScript pode gerar relatórios nos formatos JUnit e HTML. O primeiro é necessário para integração com sistemas de CI / CD, por exemplo, Jenkins. A segunda é ver convenientemente os resultados do teste, por exemplo, ao testar localmente. Os arquivos de relatório podem ser definidos na inicialização:
 pos.phar run ./start.pos --junit=junit_report.xml --html=report.html 
Um exemplo de relatório HTML

Várias informações sobre o trabalho do intérprete são exibidas no stdout. Existem 5 níveis padrão de saída de informações. Tudo o que é exibido no mesmo nível também é exibido nos outros mais "faladores".

Silencioso - o nível mais "silencioso" é definido pela opção de linha de comando -q .
Nesse nível, nada é gerado no stdout, nem mesmo nos erros críticos do interpretador. Mas pelo código de retorno diferente de zero, você pode entender que algo deu errado.

Normal é o nível padrão, sem especificar opções.
Nesse nível, erros são gerados no intérprete. Solicitações incorretas para métodos de API e falha na declaração e devem ser verificadas .

Detalhado - definido por opção-v .
Nesse nível, os resultados da declaração de impressão são exibidos .

Muito detalhado - definido pela opção -vv .
Nesse nível, os avisos do intérprete são exibidos.

Depuração - definido pela opção -vvv .
Nesse nível, todas as linhas de script executadas são exibidas. Todas as solicitações e respostas de métodos de API, os resultados de todas as verificações de declaração e obrigação .

Exemplos


O provérbio "É melhor ver uma vez do que ouvir cem vezes" é verdadeiro e na interpretação "é melhor ver o código uma vez do que ler sua descrição cem vezes". Preparei e coloquei os exemplos no https://github.com/maximw/PosExamples repository .

Virustotal


Virustotal.com - um serviço para verificar arquivos e links maliciosos. Documentação da API . Testes são feitos para a parte pública da API, com exceção dos métodos de comentários, porque Não quero jogar lixo em uma API de "combate" real com dados de teste.

Para acessar a API, você precisa se registrar , obter a chave e adicioná-la ao arquivo Virustotal / globals.pos .

Executando testes:

 pos.phar run ./Virustotal/start.pos 

Che lá para um exe-shnik encontra-se em um repositório?
Para testes, copiei hiddeninput.exe do componente Console do repositório Symfony. Este arquivo pode ser excluído e, para testes, use qualquer outro tamanho de até 32 mb.

Pastery


Analógico de Pasebin. Documentação da API .
Para acessar a API que você precisa registrar , obtenha a chave e adicione-a ao arquivo Pastery / globals.pos .

Executando testes:

 pos.phar run ./Pastery/start.pos 

Vale ressaltar que, com esses testes, um bug foi encontrado no limite do número de visualizações. Já foi consertado pelos desenvolvedores do Pastery.

Rick e morty


Eu acho que essa série animada é conhecida por muitos e amada por muitos. Documentação da API . A API consiste em três seções quase idênticas: Personagem, Local e Episódio. Portanto, os cenários são quase os mesmos, e basta olhar para os casos de teste é apenas uma das seções.

Executando testes:

 pos.phar run ./RickAndMorty/20MinutesTest.pos 

Se você conhece uma API pública que seria interessante testar dessa maneira, escreva em um e-mail pessoal.

Comentários e planos para o futuro, se houver


0) Tenho uma lista de pequenas e grandes melhorias que ainda não preciso, mas que podem ser úteis.

Ver lista
  • Adicione um relatório sobre o trabalho no formato Json com a possibilidade de sobrescrever após várias execuções de scripts
  • body replace remove
  • , YAML
  • HTTP- ,
  • , run . .
  • HTML- stdout, -vvv
  • https
  • application/x-www-form-urlencoded CURLFile . Guzzle 6,
  • « »,
  • API,
  • HTML-, bootstrap- « », .

1) Para a validação de modelos nas respostas da API usando geradores, até o momento existem apenas duas funções - similar () e idêntica () . A validação com eles é muito "desajeitada". Obviamente, já é possível validar as respostas "manualmente" e, em alguns casos, não é possível de outra maneira, mas quero torná-lo mais conveniente e, sempre que possível, evitar a verificação manual da resposta. Existem algumas idéias sobre como tornar possível gerar e validar modelos usando a mesma descrição do modelo, evitando duplicação. Mas até agora essas idéias não foram formadas o suficiente para que você possa implementá-las no código.

2) Eu acho que o andaime para métodos API baseado em descrições em OpenAPI ( Swagger ), RAML , coleções será muito útilCarteiro . Mas vale muito a pena sentar-se se o PieceofScript valer a pena.

3) Seria bom criar plugins para alguns IDEs, com destaque de código e preenchimento automático. O preenchimento automático de nomes de casos de teste, métodos de API, operadores e variáveis ​​seria simplesmente conveniente. Mas ele ainda não "cavou" nessa direção. Noções básicas sobre a criação de realce para Sublime Text e Language Server Protocol . Eu ficaria feliz se já houver pessoas com a mesma opinião, versadas em tais coisas.

4) Não sei qual prioridade colocar a capacidade de criar funções conectadas dinamicamenteimplementado em PHP. Por um lado, tudo é simples lá, basta lidar com o carregamento automático e fazer uma especificação das classes e espaços de nomes usados. Por outro lado, funções complexas com suas dependências inevitavelmente causarão um conflito de namespace entre dependências (no pior caso, versões diferentes). Há também algo em que pensar.

5) Bons sistemas de teste executam testes independentes em paralelo. Agora, isso pode ser feito iniciando o intérprete várias vezes com diferentes arquivos de início, onde diferentes casos de teste estão conectados. Mas acho que precisamos incorporar isso no próprio intérprete com detecção automática do que pode ser iniciado em paralelo.

PS Por um lado, como esse é o meu "ofício", seria lógico colocar um post no hub "Sou PR". Por outro lado, não faço relações públicas, não busco nenhum ganho comercial, apenas uma ferramenta que fiz para mim, decidi "pentear" e publicá-la publicamente.

Source: https://habr.com/ru/post/pt430392/


All Articles