Industrialisation des tests de services web

Tutoriel sur l'intégration de projet SOAPUI dans JUnit pour tester les services web

Richard Carrée

Ce tutoriel s'intéresse à montrer comment exécuter totalement ou partiellement un projet SoapUI à partir de Junit.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Industrialisation des tests de services web

- Auteur : Richard Carrée

- Parution : 12 janvier 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 Richard Carrée. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Généralités sur le lancement de tests à partir du code

- III. Mise en place

- III-A. Ajout des bibliothèques au projet

- III-B. Le fichier de projet SoapUI

- III-C. Méthode simple

- III-D. Méthode intermédiaire

- III-E. Méthode avancée

- IV. Conclusion et remerciements

I - Introduction

Afin d'automatiser les tests de services web, par exemple pour l'intégration continue, il est tout à fait possible de lancer totalement ou partiellement un projet SoapUI à partir de Junit. Le fichier de projet SoapUI étant normalisé et décrivant uniquement les tests, il peut fonctionner aussi bien dans Junit sans qu'une modification du fichier de projet ne soit nécessaire.

Cet article propose de décrire trois méthodes pour exécuter les tests d'un projet SoapUI avec JUnit :

- une méthode simple permettant de lancer facilement tous les tests d'un projet SoapUI, sans mise en échec possible du test JUnit, les résultats étant indiqués dans les logs ;

- une méthode intermédiaire permettant un meilleur contrôle des tests ainsi que leur automatisation pour l'intégration continue ;

- une méthode avancée qui est une optimisation de la méthode intermédiaire permettant d'exécuter un test JUnit par cas de test SoapUI et faciliter ainsi leur identification en cas d'échec.

On suppose que le lecteur connaît déjà SoapUI, ce document s'appuie sur la version 4.5.3.

II - Généralités sur le lancement de tests à partir du code

Les bibliothèques de SoapUI proposent un certain nombre de classes permettant l'intégration de ses tests dans Junit.

Ces classes représentent différents lanceurs de tests, communément appelés « runner », permettant d'exécuter les tests d'un projet SoapUI directement à partir du code. Plus généralement, un « runner » est une instance de classe permettant d'exécuter des tests.

Par exemple JUnit possède son propre « runner » (org.junit.runner.JUnitCore) pour exécuter des tests à partir du code. En voici une implémentation :

1. public class Main {

2. public static void main(String[] args) {

3. JUnitCore runner = new JUnitCore();

4. runner.addListener(new TextListener(System.out));

5. runner.run(MaClasseDeTest.class);

6. }

7. }

Dans l'exemple ci-dessus, le lanceur de test est configuré pour exécuter tous les tests de la classe MaClasseDeTest et afficher leur résultat sur la sortie standard.

III - Mise en place

III-A - Ajout des bibliothèques au projet

Afin de pouvoir exécuter des tests de services web avec SoapUI à partir de JUnit, les bibliothèques nécessaires doivent être ajoutées au projet. En l'occurrence, tous les jars des répertoires bin/ et lib/ situés dans le dossier d'installation de SoapUI sont à ajouter manuellement dans le Java Build Path ou avec Maven en ajoutant les éléments suivants au pom.xml :

1. <repositories>

2. <repository>

3. <id>eviware</id>

4. <url>http://www.soapui.org/repository/maven2</url>

5. </repository>

6. </repositories>

7. ...

8. <dependencies>

9. ...

10. <dependency>

11. <groupId>commons-lang</groupId>

12. <artifactId>commons-lang&lt;/artifactId>

13. <version>2.4</version>

14. <scope>test</scope>

15. </dependency>

16. <dependency>

17. <groupId>commons-beanutils</groupId>

18. <artifactId>commons-beanutils</artifactId>

19. <version>1.7.0</version>

20. <scope>test</scope>

21. </dependency>

22. <dependency>

23. <groupId>commons-collections</groupId>

24. <artifactId>commons-collections</artifactId>

25. <version>3.2.1</version>

26. <scope>test</scope>

27. </dependency>

28. <dependency>

29. <groupId>log4j</groupId>

30. <artifactId>log4j</artifactId>

31. <version>1.2.8</version>

32. <scope>test</scope>

33. </dependency>

34. <dependency>

35. <groupId>commons-cli</groupId>

36. <artifactId>commons-cli</artifactId>

37. <version>1.0</version>

38. <scope>test</scope>

39. </dependency>

40. <dependency>

41. <groupId>saxon</groupId>

42. <artifactId>saxon-dom</artifactId>

43. <version>9.1.0.8j</version>

44. <scope>test</scope>

45. </dependency>

46. <dependency>

47. <groupId>javax.jms</groupId>

48. <artifactId>jms</artifactId>

49. <version>1.1</version>

50. <scope>test</scope>

51. </dependency>

52. <dependency>

53. <groupId>javax.mail</groupId>

54. <artifactId>mail</artifactId>

55. <version>1.4</version>

56. <scope>test</scope>

57. </dependency>

58. <dependency>

59. <groupId>org.mortbay.jetty</groupId>

60. <artifactId>servlet-api</artifactId>

61. <version>2.5-20081211</version>

62. <scope>test</scope>

63. </dependency>

64. <dependency>

65. <groupId>wsdl4j</groupId>

66. <artifactId>wsdl4j</artifactId>

67. <version>1.6.2</version>

68. <scope>test</scope>

69. </dependency>

70. <dependency>

71. <groupId>eviware</groupId>

72. <artifactId>soapui</artifactId>

73. <version>4.5.1</version>

74. <scope>test</scope>

75. </dependency>

76. <dependency>

77. <groupId>eviware</groupId>

78. <artifactId>soapui-xmlbeans</artifactId>

79. <version>4.5.1</version>

80. <scope>test</scope>

81. </dependency>

82. <dependency>

83. <groupId>xmlbeans</groupId>

84. <artifactId>xbean_xpath</artifactId>

85. <version>2.4.0</version>

86. <scope>test</scope>

87. </dependency>

88. <dependency>

89. <groupId>xmlbeans</groupId>

90. <artifactId>xbean</artifactId>

91. <version>fixed-2.4.0</version>

92. <scope>test</scope>

93. </dependency>

94. <dependency>

95. <groupId>org.apache.httpcomponents</groupId>

96. <artifactId>httpclient</artifactId>

97. <version>4.1.1</version>

98. <scope>test</scope>

99. </dependency>

100. <dependency>

101. <groupId>fife</groupId>

102. <artifactId>rsyntaxtextarea</artifactId>

103. <version>2.0.1</version>

104. <scope>test</scope>

105. </dependency>

106. <dependency>

107. <groupId>xerces</groupId>

108. <artifactId>xercesImpl</artifactId>

109. <version>2.9.1</version>

110. <scope>test</scope>

111. </dependency>

112. <dependency>

113. <groupId>bouncycastle</groupId>

114. <artifactId>bcprov-jdk15</artifactId>

115. <version>144</version>

116. <scope>test</scope>

117. </dependency>

118. <dependency>

119. <groupId>xmlunit</groupId>

120. <artifactId>xmlunit</artifactId>

121. <version>1.2</version>

122. <scope>test</scope>

123. </dependency>

124. <dependency>

125. <groupId>amf</groupId>

126. <artifactId>flex-messaging-common</artifactId>

127. <version>1.0</version>

128. <scope>test</scope>

129. </dependency>

130. <dependency>

131. <groupId>amf</groupId>

132. <artifactId>flex-messaging-core</artifactId>

133. <version>1.0</version>

134. <scope>test</scope>

135. </dependency>

136. <dependency>

137. <groupId>eviware</groupId>

138. <artifactId>soap-xmlbeans</artifactId>

139. <version>1.2</version>

140. <scope>test</scope>

141. </dependency>

142. <dependency>

143. <groupId>saxon</groupId>

144. <artifactId>saxon</artifactId>

145. <version>9.1.0.8j</version>

146. <scope>test</scope>

147. </dependency>

148. <dependency>

149. <groupId>jetty</groupId>

150. <artifactId>jetty</artifactId>

151. <version>6.1.26</version>

152. <scope>test</scope>

153. </dependency>

154. <dependency>

155. <groupId>jetty</groupId>

156. <artifactId>jetty-util</artifactId>

157. <version>6.1.26</version>

158. <scope>test</scope>

159. </dependency>

160. </dependencies>

III-B - Le fichier de projet SoapUI

Enregistrons le fichier de projet SoapUI dans notre projet Java. Une bonne pratique consiste à le placer dans un répertoire resources/ correspondant à l'arborescence suivante :

RACINE_DU_PROJET/
src/
test/
resources/

Dans le cas d'un projet Maven, elle serait la suivante :

module/
src/
main/
java/
test/
java/
resources/

Les exemples de cet article utilisent une adresse absolue pour définir le projet SoapUI. Cependant, on peut aussi spécifier une adresse relative, pour cela il est nécessaire de préciser son répertoire de base dans les options du projet SoapUI :

Définition du répertoire de base pour les chemins relatifs

Enfin la méthode System.out.println() des exemples est naturellement à remplacer par celle de votre logger, par exemple log4j.

III-C - Méthode simple

Cette méthode permet d'exécuter facilement toutes les suites de tests d'un projet SoapUI sans mise en échec du test JUnit, les résultats sont alors indiqués dans les logs.

En voici l'implémentation :

1. @Test

2. public void test() throws Exception {

3. SoapUITestCaseRunner testCaseRunner = new SoapUITestCaseRunner();

4. testCaseRunner.setProjectFile("D:/Exemple/ExempleWS/test/resources/Exemple-soapui-project.xml");

5. try {

6. testCaseRunner.run();

7. } catch (Exception e) {

8. e.printStackTrace();

9. }

10. }

Cette méthode n'a pas directement d'utilité dans le cadre d'une automatisation des tests pour l'intégration continue. En effet, le test JUnit n'échoue jamais et l'identification des cas de tests SoapUI échoués ne se fait que dans les logs. Cependant, cette méthode peut être utile pour le développeur souhaitant vérifier rapidement et ponctuellement l'impact des modifications.

III-D - Méthode intermédiaire

Cette méthode propose de mieux contrôler le signalement d'erreurs et la mise en échec du test JUnit, le cas échéant, via sa méthode assertEquals. Elle permet ainsi de détecter automatiquement une régression dans le processus d'intégration continue.

En voici l'implémentation :

1. @Test

2. public void JunitTest1()

3. throws XmlException, IOException, SoapUIException {

4. // Créer une nouvelle instance de WsdlProject en spécifiant le chemin absolu du projet SoapUI

5. WsdlProject project = new WsdlProject(

6. "D:/Exemple/ExempleWS/test/resources/Exemple-soapui-project.xml");

7. // Récupère tous les TestSuites inclus dans le projet SoapUI

8. List<TestSuite> testSuiteList = project.getTestSuiteList();

9. // Itération sur tous les TestSuites du projet

10. for (TestSuite ts : testSuiteList) {

11. System.out.println("******Running " + ts.getName() + "***********");

12. // Récupère tous les TestCases d'une TestSuite

13. List<TestCase> testCaseList = ts.getTestCaseList();

14. // Itération sur tous les TestCases d'un TestSuite particulier

15. for (TestCase testcase : testCaseList) {

16. System.out.println("******Running " + testcase.getName() + "***********");

17. // Exécute le TestCase spécifié

18. TestRunner testCaseRunner = testcase.run(new PropertiesMap(), false);

19. // Vérifie si le TestCase s'est terminé correctement

20. // ou s'il a échoué à cause d'un échec d'assertion

21. assertEquals(TestRunner.Status.FINISHED, testCaseRunner.getStatus());

22. }

23. }

24. }

Cet exemple généraliste permet d'exécuter l'ensemble des suites de tests du projet SoapUI spécifié. Il est également possible d'exécuter seulement une suite de tests particulière ou encore un cas de test particulier. Il suffit alors de remplacer la boucle for de l'exemple souhaitée par la méthode appropriée :

Pour récupérer une suite de tests à partir de son nom (spécifié dans le projet SoapUI) :

1. TestSuite testSuite = project.getTestSuiteByName("NomDeLaTestSuite");

Pour récupérer un cas de test à partir de son nom (spécifié dans le projet SoapUI) :

1. TestCase testCase = testSuite.getTestCaseByName("NomDuTestCase");

Mise en échec de la méthode testTestCaseListeRunner où la totalité des tests SoapUI sont effectués

III-E - Méthode avancée

Dans la méthode intermédiaire, la totalité des tests SoapUI sont effectués dans un seul test JUnit. Cette méthode ne permet donc pas d'identifier automatiquement le cas de test SoapUI de la méthode de service web mis en cause. Cette façon de faire n'est donc pas totalement satisfaisante. Afin de faire correspondre un cas de test SoapUI avec un test JUnit, on va utiliser les tests JUnit paramétrés (JUnit Parameterized).

Les tests paramétrés permettent d'exécuter une méthode de tests avec plusieurs jeux de données, ici différents cas de test SoapUI. Pour cela, on ajoute l'annotation @RunWith(Parameterized.class) devant la classe, puis l'annotation @Parameters devant le jeu de données.

En voici l'implémentation :

1. @RunWith(Parameterized.class)

2. public class JunitTest7 {

4. private String testCaseName;

5. private static String soapuiProjectName = "D:/Exemple/ExempleWS/test/resources/Exemple-soapui-project.xml";

7. public JunitTest7(String testCaseName) {

8. this.testCaseName = testCaseName;

9. }

11. @Parameters(name="{0}")

12. public static Collection<String[] > getTestCases() throws XmlException, IOException, SoapUIException {

13. final ArrayList<String[]>  testCases = new ArrayList<String[]>();

14. WsdlProject soapuiProject = new WsdlProject(soapuiProjectName);

15. WsdlTestSuite wsdlTestSuite = soapuiProject.getTestSuiteByName("TNR2 TestSuite");

16. List<TestCase> testCaseStrings = wsdlTestSuite.getTestCaseList();

18. for (TestCase ts : testCaseStrings) {

19. if (!ts.isDisabled()) {

20. testCases.add(new String[] { ts.getName() });

21. }

22. }

23. return testCases;

24. }

26. @Test

27. public void testSoapUITestCase() throws XmlException, IOException, SoapUIException {

28. System.out.println("Nom du cas de test SoapUI : " + testCaseName);

29. assertTrue(true);

30. assertTrue(runSoapUITestCase(this.testCaseName));

31. }

33. public static boolean runSoapUITestCase(String testCase) throws XmlException, IOException, SoapUIException {

34. TestRunner.Status exitValue = TestRunner.Status.INITIALIZED;

35. WsdlProject soapuiProject = new WsdlProject(soapuiProjectName);

36. WsdlTestSuite testSuite = soapuiProject.getTestSuiteByName("TNR2 TestSuite");

37. if (testSuite == null) {

38. System.err.println("runner soapUI, la suite de test est null : " + testSuite);

39. return false;

40. }

41. WsdlTestCase soapuiTestCase = testSuite.getTestCaseByName(testCase);

42. if (soapuiTestCase == null) {

43. System.err.println("runner soapUI, le cas de test est null : " + testCase);

44. return false;

45. }

46. soapuiTestCase.setDiscardOkResults(true);

47. WsdlTestCaseRunner runner = soapuiTestCase.run(new PropertiesMap(), false);

48. exitValue = runner.getStatus();

50. System.out.println("cas de test soapUI terminé ('" + testSuite + "':'" + testCase + "') : " + exitValue);

51. if (exitValue == TestRunner.Status.FINISHED) {

52. return true;

53. } else {

54. return false;

55. }

56. }

57. }

La méthode de test JUnit a été exécutée autant de fois qu'il y a de cas de test SoapUI

Le test JUnit testSoapUITestCase() est alors exécuté autant de fois qu'il y a de cas de test SoapUI.

Le paramètre name = "{0}" de l'annotation @Parameters permet d'afficher le nom du cas de test SoapUI au lieu d'un simple numéro d'incrément, ce qui permet de l'identifier directement. Ce paramètre est supporté à partir de la version 4.11 de JUnit. Vous trouverez plus d'informations sur le sujet au chapitre « Identify Individual test cases » de la documentation de Junit.

IV - Conclusion et remerciements

Grâce au contenu de cet article, le lecteur est désormais en mesure d'automatiser totalement les tests de services web jusque dans un processus d'intégration continue. Une régression sur un cas de test de service web est alors automatiquement identifiée.

SoapUI a l'avantage d'être un outil suffisamment simple d'utilisation pour être manipulé par un personnel non développeur. Aussi, l'écriture des tests de services web peut être déléguée.

Je tiens à remercier milkoseck pour sa relecture attentive de cet article et Mickaël Baron pour la mise au gabarit.