Introdução à validação de schema JSON utilizando o Postman

Introdução à validação de schema JSON utilizando o Postman

Este artigo tem por objetivo orientar sobre o processo de validação das respostas de uma API utilizando o Postman. Através de tal processo é possível comparar, de forma automatizada, as respostas recebidas com as regras de um schema previamente definido, validando a estrutura do JSON, bem como a tipagem, dentre outras especificações. Isso garante que o retorno esteja totalmente de acordo com o que se espera, padronizando os dados e minimizando a chance de erros ao fazer a integração com o front-end ou em outras utilizações da API.

Considerações iniciais

  • Os passos necessários para a execução dos testes de validação, bem como a preparação do ambiente para tal, serão descritos da maneira mais clara possível para que mesmo um iniciante consiga acompanhar e reproduzir os resultados sem dificuldades, mas é esperado o mínimo de familiaridade com o Postman, pois este texto foi escrito partindo da suposição de que algumas funcionalidades básicas e uma visão geral do software já são conhecidos.

  • O Postman possui duas bibliotecas integradas para a realização das validações: tv4 (Tiny Validator for v4 JSON Schema) e Ajv (Another JSON Schema Validator). Apesar de a documentação oficial do Postman utilizar a tv4 nos exemplos, esta não é mais mantida desde 2017, e seu uso, portanto, não é recomendado.

  • É importante se orientar de modo a sempre buscar testar para as falhas primeiro para ter certeza de que a validação funciona da maneira correta (para cada cenário a ser testado, garante-se que o teste irá falhar propositalmente em um primeiro momento para, então, seguir com a validação). Isso garante a confiabilidade do teste, afinal, se este não falha em nenhum cenário, como se pode ter certeza se o que passa é realmente válido?

  • Pratique um pouco com schemas gerados manualmente, pelo menos no começo. Deixe para utilizar geradores automáticos, como o https://jsonschema.net, quando já possuir um melhor entendimento da estrutura típica de um schema e suas regras, e precisar lidar com schemas longos e complexos. Dessa forma, o arquivo gerado não vai lhe parecer confuso e você será capaz de fazer intervenções quando necessário, garantindo a qualidade dos testes, pois saberá exatamente o que cada coisa faz.

Preparação do ambiente

Antes de mais nada, vamos criar uma coleção, definir as variáveis de ambiente, gerar um exemplo e criar um mock de servidor para realizar os testes iniciais.

Coleção (collection)

Coleções são “pastas” que possibilitam o armazenamento de requisições, facilitando a organização dos endpoints para agilizar utilizações futuras. No menu lateral do Postman clique em Collections e, em seguida, no botão “+” para adicionar uma nova coleção:

Defina um nome, clique sobre a coleção com o botão direito (ou no menu de 3 pontos), e adicione uma nova requisição do tipo GET em Add request. O endpoint que será será utilizado é:

https://postman-echo.com/get?foo1=bar1&foo2=bar2

Variáveis de ambiente (environment variables)

As variáveis de ambiente, apesar de não serem estritamente necessárias, ajudam no controle dos endpoints a serem testados e agilizam requisições futuras por meio da reutilização de valores. Após configurado, a URL acima pode ser substituída por {{url_API}}, por exemplo. Se a coleção já possuir variáveis definidas, pule esta etapa e prossiga com a criação de exemplos.

Para não estender demais o texto e desviar da sua finalidade principal, acesse mais informações sobre a criação e utilização de variáveis dentro do Postman clicando aqui.

Criando exemplos

No contexto do Postman, exemplos são cenários pré-definidos de requisições e respostas que podem ser editados e utilizados nas simulações de servidor, como veremos mais adiante. A maneira mais fácil de se criar um exemplo é a partir de uma requisição para a API que será testada.

Vamos criar um exemplo para uma requisição do tipo GET. Para isso, basta abrir a coleção, escolher a requisição desejada (no caso, a que foi criada anteriormente) e clicar em Send. Feito isso, clique em Save Response e, em seguida, Save as example.

Automaticamente será aberta uma nova aba com o exemplo recém criado. Note que a tela é bem parecida com a de uma requisição comum, e que as informações apresentadas inicialmente são idênticas àquelas da requisição feita anteriormente:

É possível editar parâmetros, headers e o body, assim como em uma requisição normal. Porém, diferentemente do que ocorre nesta, um exemplo permite que a resposta também seja editada, bem como o código de status da mesma. Com isso, é possível criar cenários controlados - perfeito para a execução de testes - a partir do uso de mocks. Para mais detalhes sobre os exemplos, outras formas de criação e utilizações, clique aqui.

Mock de servidor

A ideia por trás do mock é criar um endpoint para um servidor falso que irá simular o comportamento de uma API real, enviando retornos para as requisições feitas, e é aqui que entram os exemplos citados anteriormente: com base no tipo de requisição feita para o mock, o Postman encontrará um exemplo correspondente e responderá com os dados contidos neste. Desta forma, será possível, quando necessário, modificar tais respostas para forçar situações de erros propositais, por exemplo, garantindo que o teste falhe nos cenários em que espera-se que isso ocorra e seu teste seja confiável. Portanto, para que o mock funcione adequadamente, é necessário que ao menos um exemplo do mesmo tipo da requisição que se deseja fazer tenha sido criado na coleção.

Para criar o mock, selecione Collections, clique no menu de 3 pontos da coleção que deseja simular e, em seguida, selecione Mock collection. É preciso estar logado em uma conta do Postman para criar mocks.

Na tela seguinte, escolha um nome para o mock e confira se a coleção a ser simulada está correta (caso exista mais de uma no seu workspace). Você pode selecionar um ambiente para que o mock use as variáveis ali definidas, onde, posteriormente, poderá incluir a URL do mock. Também é possível optar por salvar automaticamente a URL em uma variável, que será criada em um novo ambiente. Aqui seguirei com a segunda opção, mas sinta-se livre para fazer o que achar melhor. Por fim, clique em Create Mock Server.

Tendo criado o mock, já é possível testá-lo. Abra uma nova aba no Postman. No canto superior direito, selecione o ambiente nomeado tal qual o mock criado e monte a URL para a requisição, substituindo a primeira parte da URL original do endpoint, da seguinte forma:

{{url}}/get?foo1=bar1&foo2=bar2

Onde {{url}} é a variável de ambiente correspondente à URL do mock, gerada automaticamente na etapa de criação deste, e /get?foo1=bar1&foo2=bar2 é o caminho do endpoint original e seus parâmetros. Caso não queira fazer uso da variável de ambiente, ao invés de {{url}} basta inserir a URL do mock diretamente.

Ao enviar a requisição, se tudo tiver sido feito corretamente, será apresentado um retorno. Pode-se notar que a resposta recebida é idêntica àquela enviada pelo endpoint original da API, mas isso é porque este retorno é exatamente aquele criado junto ao exemplo, que ainda não foi alterado. Ou seja, quaisquer modificações feitas (e salvas) lá irão se refletir no mock.

Validação do schema

A primeira coisa a ser feita na validação de um schema JSON é estabelecer a estrutura que o compõe. O ideal aqui é ter acesso à documentação provida, por exemplo, através do Swagger, e copiar o schema disponibilizado. Mas, para fins didáticos, aqui será utilizada como base a própria resposta enviada pela API, de onde ele será extraído manualmente.

{
    "args": {
        "foo1": "bar1",
        "foo2": "bar2"
    },
    "headers": {
        "x-forwarded-proto": "https",
        "x-forwarded-port": "443",
        "host": "postman-echo.com",
        "x-amzn-trace-id": "Root=1-63addcc9-5ab135d701ba98d92efac901",
        "user-agent": "PostmanRuntime/7.30.0",
        "accept": "*/*",
        "postman-token": "8bff98e4-237c-4940-bef8-067b3998dd58",
        "accept-encoding": "gzip, deflate, br",
        "cookie": "sails.sid=s%3AmyxFsngmsAqbFM1FofXwBn3M-TlpbIOf.NjXpy0b07Qp08dIDDeOAy9iK7oh5w6Y%2FhyBpbzM9Z1c"
    },
    "url": "<https://postman-echo.com/get?foo1=bar1&foo2=bar2>"
}

Logo de cara é possível perceber que a resposta é composta de um objeto contendo 3 propriedades, sendo duas delas também do tipo objeto e uma do tipo string. Traduzindo para uma estrutura esquemática, inicialmente temos o seguinte:

const schema = {
    "type": "object",
    "properties": {
        "args": {
            "type": "object"
        },
        "headers": {
            "type": "object"
        },
        "url": {
            "type": "string"
        }
    }
}

No primeiro nível, a raiz, temos a definição da tipagem geral do schema: "type": "object". Em seguida, properties enumera as propriedades contidas no objeto principal e, para cada uma, é declarada sua tipagem, assim como foi feito no nível anterior. Essa é a base inicial do schema, ainda bem simples, mas já é possível realizar os primeiros testes.

Dentro da aba do mock, localize a aba Tests, abaixo da barra de endereços, e declare a variável schema, como feito no exemplo acima. Em seguida, declare a função de teste:

pm.test("Validate schema", () => {
    pm.response.to.have.jsonSchema(schema);
});

Aqui vale abrir um pequeno parêntese para destrinchar a função e entender o que está sendo feito.

De acordo com a documentação do Postman:

O objeto pm contém todas as informações pertencentes ao script que está sendo executado e permite o acesso a uma cópia da requisição que está sendo enviada ou da resposta recebida. Também permite obter e definir variáveis de ambiente e globais.

Portanto, o objeto pm é fundamental na construção de testes, sejam de que tipo for. Dentre as propriedades e métodos associados a este objeto, o primeiro a ser utilizado aqui é o método test, que recebe dois argumentos. O primeiro é uma string, que será usada para identificar o teste em questão. É recomendável escolher um nome claro e autoexplicativo, que facilite o entendimento do que se propõe tal teste (neste exemplo foi usado o nome “Validate schema”). O segundo argumento é uma função onde o teste em si será especificado. No caso do nosso exemplo, uma função anônima contendo a seguinte linha: pm.response.to.have.jsonSchema(schema), onde pm.response acessa a resposta recebida do servidor e, em seguida, é feita a asserção to.have.jsonSchema(schema) para garantir que a resposta satisfaça o schema definido anteriormente. Até o momento o schema ainda é bem simples, mas qualquer resposta recebida com tipagem diferente para as propriedades listadas não deverá passar no teste.

Se for enviada uma requisição agora o teste irá passar, mas para ter certeza que funciona corretamente devemos modificar a especificação do schema ou a resposta para forçar uma situação de erro. A fim de evitar confusões mais adiante e você não acabe com um schema - e, consequentemente, um teste - incorreto, o ideal é fazer alterações apenas na resposta, pois é justamente pra isso que criamos o exemplo. Volte na aba do exemplo e altere o tipo de alguma das propriedades. No caso, irei alterar a propriedade url de string para um valor numérico qualquer:

{
    "args": {
        "foo1": "bar1",
        "foo2": "bar2"
    },
    "headers": {
        "x-forwarded-proto": "https",
        "x-forwarded-port": "443",
        "host": "postman-echo.com",
        "x-amzn-trace-id": "Root=1-63addcc9-5ab135d701ba98d92efac901",
        "user-agent": "PostmanRuntime/7.30.0",
        "accept": "*/*",
        "postman-token": "8bff98e4-237c-4940-bef8-067b3998dd58",
        "accept-encoding": "gzip, deflate, br",
        "cookie": "sails.sid=s%3AmyxFsngmsAqbFM1FofXwBn3M-TlpbIOf.NjXpy0b07Qp08dIDDeOAy9iK7oh5w6Y%2FhyBpbzM9Z1c"
    },
    "url": 123
}

Salve a alteração, volte na aba do mock e envie novamente a requisição. Se tudo tiver sido feito corretamente, o teste deverá falhar:

Note que a mensagem de erro nos dá uma pista do que houve: era esperado que data.url fosse do tipo string, conforme definimos no schema. Ótimo! Sinal de que o teste funciona corretamente - por enquanto. Contudo, sua eficiência ainda está limitada pelo nível de especificidade definido no schema. Perceba que se formos até o exemplo e alterarmos o nome de alguma dessas propriedades, adicionarmos propriedades não especificadas, excluirmos da resposta alguma das existentes ou alterarmos o tipo de uma das propriedades aninhadas, o teste passará da mesma forma, pois a especificação, até o momento, é:

A resposta deve vir como um objeto, e sempre que as propriedades args, headers e url estiverem presentes, seus tipos devem ser object, object e string, respectivamente.

Qualquer cenário diferente desse ainda não é coberto pelo teste, permitindo a validação equivocada de vários erros em potencial, então é hora de aumentar o grau de especificidade para termos um teste mais rigoroso. Vale atentar também para erros de digitação ao definir o schema, pois se args for escrito como arg, por exemplo, o Ajv (a biblioteca de testes que usamos) irá entender que se trata de uma outra propriedade (e não há nada ainda que impeça a presença de propriedades não especificadas), ignorando possíveis problemas em args.

Voltando ao schema, a primeira alteração que já pode ser feita é “descrever” melhor as propriedades args e headers, definindo "type" e "properties" como fizemos anteriormente, garantindo que a tipagem de todas propriedades não sejam violadas:

const schema = {
    "type": "object",
    "properties": {
        "args": {
            "type": "object",
            "properties": {
                "foo1": {
                    "type": "string"
                },
                "foo2": {
                    "type": "string"
                }
            }
        },
        "headers": {
            "type": "object",
            "properties": {
                "x-forwarded-proto": {
                    "type": "string"
                },
                "x-forwarded-port": {
                    "type": "string"
                },
                "host": {
                    "type": "string"
                },
                "x-amzn-trace-id": {
                    "type": "string"
                },
                "user-agent": {
                    "type": "string"
                },
                "accept": {
                    "type": "string"
                },
                "postman-token": {
                    "type": "string"
                },
                "accept-encoding": {
                    "type": "string"
                },
                "cookie": {
                    "type": "string"
                }
            }
        },
        "url": {
            "type": "string"
        }
    }
}

Até agora apenas listamos as propriedades e suas tipagens, mas não especificamos se existem propriedades obrigatórias e quais seriam estas. Podemos fazer isso através de required, que se constitui como um array de strings. Para fins de demonstração, required será adicionado apenas para a raiz do schema, mas pode ser incluído para os objetos args e/ou headers também, claro. O importante é entender seu uso:

const schema = {
    "type": "object",
    "properties": {
                "args": {...},
        "headers": {...},
        "url": {
            "type": "string"
        }
    },
    "required": ["args", "headers", "url"]
}

// Detalhes de "args" e "headers" omitidos apenas para abreviar o exemplo

A partir de agora, a ausência de args, headers e/ou url na resposta deve resultar em erro. Como sempre, é importante testar para o erro antes de prosseguir, então irei remover url e reenviar a requisição:

Porém, a presença de propriedades adicionais ainda não é tratada corretamente, de modo que o schema, cumpridos os demais requisitos, continua sendo válido ainda que contenha outras propriedades além das 3 já conhecidas. Neste exemplo específico, não queremos isso. Para lidar com esse problema, utilizaremos additionalProperties:

const schema = {
    "type": "object",
    "properties": {
        "args": {...},
        "headers": {...},
        "url": {
            "type": "string"
        }
    },
    "required": ["args", "headers", "url"],
    "additionalProperties": false
}

// Detalhes de "args" e "headers" omitidos apenas para abreviar o exemplo

Ao enviar uma requisição contendo propriedades adicionais no retorno, a validação deverá falhar:

Aqui cabe uma diferenciação importante entre required e additionalProperties: na primeira estão listadas as propriedades que devem, obrigatoriamente, existir na resposta do servidor, mas não diz nada sobre propriedades extras; já na segunda podemos ou não permitir que a resposta contenha propriedades das quais não temos conhecimento prévio. Sendo assim, um schema contendo as propriedades X, Y e Z que não as liste como obrigatórias, mas onde additionalProperties receba o valor false, não exigirá que X, Y ou Z estejam presentes na resposta, mas invalidará o teste se esta contiver a propriedade N (não prevista) ou no caso de uma das 3 anteriores estar com o nome incorreto (já que seria tratada como desconhecida pelo schema).

Conclusão

Com isso, nosso schema agora é capaz de definir as propriedades, suas tipagens, lista eventuais propriedades obrigatórias e permite ou não a presença de outras, estando pronto para ser usado na validação dos retornos da API verdadeira, bastando copiar o conteúdo da aba Tests do mock. São regras relativamente simples perto do grau de especificidade que é possível atingir, mas já é o suficiente para realizar testes que irão cobrir boa parte dos cenários menos exigentes, garantindo a qualidade dos dados transmitidos. Na maioria dos casos, quando se atua com outras equipes, o schema já estará definido na documentação, poupando parte do trabalho aqui descrito de identificar as regras manualmente. De toda forma, é sempre uma boa utilizar os mocks para executar alguns testes rápidos pra ter certeza de que está tudo ok com as definições. Recomendo uma lida nas regras específicas para propriedades do tipo array, visto que é um tipo bem comum e que não foi abordado neste texto. Para saber mais sobre as regras e o que pode ser definido além do que foi apresentado aqui, a documentação está sempre disponível: https://json-schema.org/. Bons testes!