Home Outils Validation d’une réponse REST avec SoapUI

Validation d’une réponse REST avec SoapUI

  Sylvain, Consultant MOA 5 min 27 mai 2019

Même si son nom ne le laisse pas forcément supposer, SoapUI permet d’interroger, manipuler et tester des API REST.

La documentation de l’éditeur et les ressources sur le web ne manquent pas, cet article a pour simple but de servir d’introduction et d’exposer rapidement comment réaliser une suite de tests.

Dans cet exercice nous utiliserons une API disponible gratuitement : https://restcountries.eu/
Sa vocation est de fournir la liste des pays, et nous allons tester plus précisément la méthode qui retourne la liste de tous les pays.

 

Création du projet

Une fois le projet et le service Countries créés, l’appel au service https://restcountries.eu/rest/v2/all retourne bien la liste des pays au format JSON.

 

Validation de la réponse par une série de tests

Définition du plan de test

La validation de la réponse du service s’effectuera en 3 étapes, de la validation « globale » au cas particulier :

  1. Respect du contrat d’interface
  2. Validation des formats de données
  3. Validation fonctionnelle

Pour ce faire dans le projet nous créons une TestSuite.

L’implémentation choisie est la suivante :

  • Le 1er step sera l’appel au service rescountries/all
  • Un script groovy « Init » se chargera de l’appel et stockera la réponse dans une variable définie au niveau projet (cette solution reste acceptable dans la mesure où la réponse du service garde une taille raisonnable, il conviendra d’adopter d’autres solutions en cas de taille trop importante, au risque de surcharger la mémoire utilisée par SoapUI)
  • Script Groovy « JSONSchema validation »
  • Script Groovy « Types & Format validation
  • Script Groovy « Borders validation »

Contrat d’interface

JSON Schema

La validation du contrat d’interface se fera grâce au JSON schema. Ce document décrit la structure et le contenu que doit respecter le document réponse généré.

Normalement l’existence du JSON schema doit précéder celle de la réponse, mais il arrive souvent parfois que les specs soient écrites une fois les devs effectués 😉 : ainsi il existe des services permettant de créer le JSON schema à partir d’un document JSON (ex. : https://jsonschema.net/).

Voici une version du JSON schema du service restcountries/all réalisée à partir de la réponse au service : Télécharger le ZIP du projet (fichier json/Countries_json_schema.json).

La validation dans SoapUI se fera en utilisant la librairie json-schemavalidator disponible sur https://github.com/fge/json-schema-validator.

Pour vous faciliter la tâche, voici l’ensemble des librairies qu’il m’a fallu installer pour faire fonctionner json-schemavalidator (à copier dans « répertoire d’install »/bin/ext, fermer et relancer SoapUI) : Télécharger le ZIP du projet (Dossier jar).

Comme indiqué précédemment, l’implémentation nécessite la création de variables au niveau projet, d’une part pour stocker la réponse, d’autre part pour accéder au fichier JSONSchema.

Le script Init réalise l’appel et enregistre la réponse dans la variable responseAll.

def response = context.expand('${Source all#Response}')
def myTestCase = context.testCase
myTestCase.testSuite.project.setPropertyValue("responseAll",response)

Le script JSONSchema validation lit le fichier contenant le JSONSchema, et valide le contenu de la variable responseAll par rapport au schéma.

import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequestStep;
import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequest;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCaseRunner
import com.eviware.soapui.support.types.StringToObjectMap
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestRunContext

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.fge.jsonschema.core.report.ProcessingReport
import com.github.fge.jsonschema.main.JsonSchema
import com.github.fge.jsonschema.main.JsonSchemaFactory

def myTestCase = context.testCase
// Lecture de la réponse
def response = context.expand(myTestCase.testSuite.project.getPropertyValue("responseAll"))

ObjectMapper mapper = new ObjectMapper()
JsonNode responseJSON = mapper.readTree(response)

// Lecture du JSONSchema depuis le fichier
JsonNode responseSchemaJSON = mapper.readTree(new File(myTestCase.testSuite.project.getPropertyValue("jsonschemaFilePath") + myTestCase.testSuite.project.getPropertyValue("jsonschemaAll")).text);
JsonSchemaFactory factory = JsonSchemaFactory.byDefault()
JsonSchema responseSchema = factory.getJsonSchema(responseSchemaJSON)

def bJSONSchemaTest=false
if (responseSchema.validInstance(responseJSON)) {
  log.info("Response Validated!")
  bJSONSchemaTest=true}
else {
  log.info("Response NOT Validated!")
  // Si erreur alors affichage du rapport de validation
  log.info(responseSchema.validate(responseJSON).toString())
}
// Test réussi si validation de la réponse
assert bJSONSchemaTest==true

Exemple de rapport de validation en cas d’erreur :

--- BEGIN MESSAGES ---
error: instance type (integer) does not match any allowed primitive type (allowed: ["string"])
    level: "error"
    schema: {"loadingURI":"#","pointer":"/items/properties/alpha2Code"}
    instance: {"pointer":"/0/alpha2Code"}
    domain: "validation"
    keyword: "type"
    found: "integer"
    expected: ["string"]
---  END MESSAGES  ---

 

Validation des formats de données

Le script Types & Format validation va parcourir la réponse du service et vérifier le format des données. L’exemple classique réalisé ici porte sur le nombre et la nature de caractères autorisés pour chaque champ.

A noter que ce type de contrôle aurait également pu être effectué en spécifiant plus finement le JSONSchema (au moyen de l’attribut pattern).

import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequestStep;
import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequest;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCaseRunner
import com.eviware.soapui.support.types.StringToObjectMap
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestRunContext

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.fge.jsonschema.core.report.ProcessingReport
import com.github.fge.jsonschema.main.JsonSchema
import com.github.fge.jsonschema.main.JsonSchemaFactory

import groovy.json.JsonSlurper;

def myTestCase = context.testCase
// Lecture de la réponse
def response = context.expand(myTestCase.testSuite.project.getPropertyValue("responseAll"))
def json = new JsonSlurper().parseText(response)

List assertionList = []

// Parcours de la réponse
json.each { country ->
		// Validation des formats de données par regex
		country.alpha3Code ==~ /([A-Z]{3})/ ? log.info("Le format du champ alpha3Code " + country.alpha3Code + " est correct,champ attendu [A-Z]{3}") : assertionList.add("Le format du champ alpha3Code " + country.name + " " + country.alpha3Code + " est incorrect,champ attendu [A-Z]{3}")
		country.topLevelDomain.each { tLD -> 
			if (tLD != "" )
			{ tLD ==~ /(\..{2,3})/ ? log.info("Le format du champ topLevelDomain " + tLD + " est correct,champ attendu (..{2,3})") : assertionList.add("Le format du champ topLevelDomain " + country.name + " " + tLD + " est incorrect,champ attendu (..{2,3})") }
		}
}

// Affichage des messages d'erreur dans le log
assertionList.size() == 0 ? "" : assertionList.each() { e -> log.error(e) }

// Test réussi si aucun message d'erreur
assert assertionList.size() == 0

 

Validation fonctionnelle

Comme exemple de validation fonctionnelle, nous allons nous intéresser à la donnée borders fournie par l’API. Pour chaque pays est indiquée une liste de pays avec lesquels il partage une frontière.

Ex. pour la France

"borders": [
    "AND",
    "BEL",
    "DEU",
    "ITA",
    "LUX",
    "MCO",
    "ESP",
    "CHE"
]

La validation va consister à parcourir l’ensemble des pays, et pour chaque pays limitrophe trouvé s’assurer que la frontière « inverse » est également indiquée.

import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequestStep;
import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequest;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCaseRunner
import com.eviware.soapui.support.types.StringToObjectMap
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestRunContext

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.fge.jsonschema.core.report.ProcessingReport
import com.github.fge.jsonschema.main.JsonSchema
import com.github.fge.jsonschema.main.JsonSchemaFactory

import groovy.json.JsonSlurper;

def groovyUtils = new com.eviware.soapui.support.GroovyUtils(context)

def myTestCase = context.testCase
// Lecture de la réponse
def response = context.expand(myTestCase.testSuite.project.getPropertyValue("responseAll"))

def json = new JsonSlurper().parseText(response)

String sCountryCode = "";
String sBorderCode = "";
String sCountryBCode = "";
Boolean bBorderFound = false;
Boolean bBordersTest = true;

// Parcours de la réponse
json.each { country ->

	sCountryCode = country.alpha3Code;
	// Parcours des pays frontaliers
	country.borders.each { border ->
		bBorderFound = false;
		sBorderCode = border;

		// countryB = pays frontalier (retrouvé par son alpha3Code) pour essayer de retrouver la frontière "inverse"
		countryB = json.find { it.alpha3Code == sBorderCode }

		if (countryB != null) {
			sCountryBCode = countryB.name;
			// Frontière trouvée si le code du premier pays est retrouvé dans la liste
			bBorderFound = (sCountryCode == countryB.borders.find { it == sCountryCode })
		}
		if (!bBorderFound){
			log.info country.name + " " + country.alpha3Code + "-" + border + " " + sCountryBCode + " / " + border + "-" + country.alpha3Code + " missing !"
		}
		
		bBordersTest = bBordersTest && bBorderFound;
	}
}
// Validation si toutes les frontières ont été retrouvées
assert bBordersTest==true;

 

Exécution du plan de test

Le TestCase « Countries all » est configuré pour poursuivre son exécution en cas d’échec d’un de ses composants (option « Abort on Error » désactivée).

Voici le résultat de son exécution

Step 2 OK, une réponse a été reçue.

Step 3 OK, la réponse respecte le contrat d’interface.

Step 4 OK, les tests des différents formats de données sont concluants.

Le test échoue car le Step 5 fonctionnel n’a pas été validé.

Les onglets « SoapUI log » et « Error log » confirment que la condition de validation n’a pas été assurée, et l’onglet « script log » qui affiche les logs fonctionnels définis précédemment permet d’en découvrir les causes.

Le Tchad est signalé comme possédant une frontière avec le Soudan, alors que le Soudan n’indique pas le Tchad parmi ses pays frontaliers, et ainsi de suite.

Il ne reste plus qu’à signaler aux développeurs les incohérences sur ces différences de frontières.

Lire les articles similaires

Laisser un commentaire

Social Share Buttons and Icons powered by Ultimatelysocial