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 :
- Respect du contrat d’interface
- Validation des formats de données
- 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.



