Présente les différents types de tests automatisés, les objectifs des tests unitaires, les stratégies de mise en œuvre, les bonnes pratiques, les difficultés, ce qu'est un mock, différents outils (Unitils, Mockito, DbUnit, Spring Test) et des exemples de tests (DAO et contrôleurs Spring MVC), sans oublier le test de code legacy.
2. Objectifs
Positionnement des TU par rapport aux autres catégories de tests
Tour d’horizon des outils facilitant l’écriture de tests
Zoom sur Mockito
Exemples concrets
3. Sommaire
Les différents types de tests automatisés
Objectifs des tests unitaires
Stratégies de mise en œuvre des tests unitaires
Bonnes pratiques & Difficultés
Décomposition d’un test
L’intérêt des mocks
Boîte à outils : Unitils, Mockito, DbUnit et Spring Test
Exemples de tests de DAO et de contrôleurs Spring MVC
Tester du code legacy
4. Tests unitaires
Teste une méthode d’une classe en isolation du reste de l’application
Les tests exécutés lors d’un mvn test
Doivent fonctionner sur un poste de dév déconnecté du réseau
Outils : JUnit, Mockito
Tests d’intégration
Teste un ensemble de composants
Exemples :
Web Service SOAP ou REST
Service métier s’appuyant sur un DAO Hibernate interrogeant une base Oracle
Outils : JUnit, SoapUI
Les différents types de tests automatisés (1/2)
5. Tests fonctionnels
Exécute des scénarios fonctionnels en simulant des interactions utilisateurs
Outils : Selenium, HP UFT, CasperJS, Cucumber
Tests de performance
Simule la charge utilisateur sur un environnement iso-prod
Outils : JMeter, Gatling, Dynatrace
Tests de vulnérabilité, de robustesse aux pannes, d’installation, de
déploiement …
Les différents types de tests automatisés (2/2)
6. Vérifier le comportement d’une fonctionnalité au
regard des exigences
Tester ses développements
Fait partie du job d’un développeur
Sérénité vis-à-vis des tests d’intégration
Se prémunir de régression lors de :
Correction d’anomalies
Evolutions fonctionnelles
Montée de socle technique
Refactoring
Objectifs des tests unitaires (1/2)
7. Documenter le code
Contribuent
au design logiciel
à la qualité générale de l’application
Objectifs des tests unitaires (2/2)
8. Tester en priorité :
le code complexe
les corrections de bugs
le code sensible (souvent amené à changer)
Tester les cas limites
Tests en boîte noire / boîte blanche
Stratégies de mise en œuvre des tests unitaires
9. Valider le fonctionnement par des assertions
Automatiser l’exécution des TU
Un TU doit être rapide à exécuter
Essayer d’avoir une méthode de test par scénario de test
L’échec d’un test unitaire doit être compréhensible
Importance du nom de la méthode de test unitaire
Bonnes pratiques
10. Jeux de données
Demande une connaissance fonctionnelle
Peut-être complexe et fastidieux à initier
Tester la couche d’accès aux données
Code existant éloigné des principes inspirés de Clean Code
Difficultés
11. Un test se décompose généralement en 3 étapes
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class CalculatorTest {
@Test
public void evaluateAdditionExpression() {
Calculator calculator = new Calculator();
Expression exp = new ArithmeticExpression("1+2+3");
int sum = calculator.evaluate(exp);
assertEquals(6, sum);
}
}
Décomposition d’un test
3. Then : vérifications du résultat
2. When : appel de la méthode testée
1. Given : initialise l’état du système
12. Besoin : tester individuellement un service métier,
c’est à dire sans ses adhérences
L’intérêt des mocks
Service
Dao
Service
Dao
Mock
Code de production Configuration de test
à programmer
13. ReflectionAssert.assertReflectionEquals de unitils-core
Boîte à outils : assertReflectionEquals de Unitils
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Connor");
assertReflectionEquals(user1, user2);
junit.framework.AssertionFailedError:
Expected: User<id=1, firstname="John", lastname="Doe">
Actual: User<id=1, firstname="John", lastname="Connor">
--- Found following differences ---
lastname: expected: "Doe", actual: "Connor"
14. Boîte à outils : Spring Test
Conteneur léger accessible aux tests unitaires et d’intégration
Support de JUnit 4 et TestNG
Chargement du contexte Spring
Injection de dépendances
Mise en cache du contexte Spring
Extensions de JUnit par
Runner : SpringJUnit4ClassRunner
Annotations : @ContextConfiguration, @Rollback, @Sql, @Repeat, @ActiveProfiles …
Listeners : DependencyInjectionTestExecutionListener
Bouchons prêts à l’emploi : MockHttpSession, MockHttpServletRequest …
Classes utilitaires : JdbcTestUtils, AopTestUtils, ReflectionTestUtils,
TestTransaction …
Spring MVC Test Framework
15. Permet de charger en base des jeux de données
A partir de fichier XML
Favoriser des dataset les plus petits possibles
Suite à un test, permet de vérifier l’état de la base
Comparaison de l’état de la base avec un fichier XML
Ce que DbUnit ne fait pas :
Création de la base et du schéma
Gestion des connexions et des transactions
L’élaboration de jeux de données
Boîte à outils : DbUnit
16. Créer un mock
Boîte à outils : Mockito (1/4)
import static org.mockito.Mockito.*;
List mockedList = mock(List.class);
assertNull(mockedList.get(0)); Les méthodes d’un mock non programmé
ne font rien. Elles retournent null ou false .
Programmer un mock
LinkedList mockedList = mock(LinkedList.class);
when(mockedList.get(0)).thenReturn("first");
assertEquals("first", mockedList.get(0));
assertNull(mockedList.get(1));
Mockito permet de mocker aussi bien
des interfaces que des classes.
Simule un comportement
17. Vérifier les interactions avec le mock
Boîte à outils : Mockito (2/4)
@Test
public void testAdminAuthentication() {
UserDao userDao = mock(UserDao.class);
UserService userService = new UserService(userDao);
User admin = new User("admin");
when(userDao.findOne("admin")).thenReturn(admin);
User loadedUser = userService.loadUserByUsername("admin");
verify(userDao).findOne("admin");
}
Mock le DAO
Programme le DAO
Vérifie l’interaction
A utiliser judicieusement
Lorsque la méthode testée ne renvoie pas de résultat
Pour des problématiques de performance
18. Partial mocking avec spy
Enregistre les interactions
Simule le comportement de méthodes choisies
Boîte à outils : Mockito (3/4)
List<String> list = new ArrayList<String>();
List<String> spyList = Mockito.spy(list);
spyList.add("one");
assertEquals(1, spyList.size());
Mockito.verify(spyList).add("one");
when(spyList.size()).doReturn(100);
assertEquals(100, spyList.size());
19. ArgumentCaptor
Permet de récupérer la valeur des
paramètres d’appel d’un mock
Boîte à outils : Mockito (4/4)
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void inviteToParty(Person friend,
Party party) {
party.addGuest(friend);
}
}
ArgumentCaptor<Person> argument =
ArgumentCaptor.forClass(Person.class);
Party party = mock(Party.class);
Person john = new Person("John");
Person peter = new Person("Peter");
// Peter invites John to the party
peter.inviteToParty(john, party);
verify(party).addGuest(argument.capture());
// verify John was invited
assertEquals("John", argument.getValue().getName());
20. Tester unitairement des DAO nécessite une base de données embarquée
Les puristes les considèrent comme des tests d’intégration
Spring Test
Facilite le chargement de la configuration Spring liée à l’infrastructure
Prend en charge la création du schéma
Support des transactions
Laisse la base inchangée après l’exécution du test (débrayable)
Gestion manuelle des transactions
Possibilité d’exécuter du code en dehors d’une transaction
Tester des DAO (1/3)
21. Tester des DAO (2/3)
Avec DbUnit
accounts-dataset.xml
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<ACCOUNT ID="1" BIC="FR7030002005500000157845Z02" LABEL="Account 1"/>
<ACCOUNT ID="2" BIC="FR70300023455000021Z4234Y45" LABEL="Account 2"/>
</dataset>
public class TestHibernateAccountDao extend AbstractDaoTest<HibernateAccountDao> {
@Test
public void findAccountByIBan() {
DbUnitLoader.loadDataset("accounts-dataset.xml");
String iban = "FR70 3000 2005 5000 0015 7845 Z02";
Account account = dao.findAccountByIBan(iban);
assertNotNull(account);
assertEquals("Account 1", account.getDescription());
}
}
22. Avec Spring Test
Tester des DAO (3/3)
Extrait du fichier spring/dao-config.xml
<beans profile="test">
<jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script location="classpath:create-schema.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource"
jndi-name="jdbc/MyDataSource"/>
</beans>
<bean id="sessionFactory"
class="o.s.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref=" dataSource " />
…
</bean>
<bean id="transactionManager"
class="o.s.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<context:component-scan base-package="com.myapp.dao"/>
@ContextConfiguration(locations = {"classpath:spring/dao-config.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test")
public class ClinicDaoTests {
@Autowired ClinicDao clinicDao;
}
@Test @Transactional
public void shouldInsertOwner() {
Collection<Owner> owners = clinicDao.findOwnerByLastName("Schultz");
int found = owners.size();
Owner owner = new Owner();
owner.setFirstName("Sam");
owner.setLastName("Schultz");
owner.setAddress("4, Evans Street");
owner.setCity("Wollongong");
clinicDao.saveOwner(owner);
assertThat(owner.getId().longValue()).isNotEqualTo(0);
owners = clinicDao.findOwnerByLastName("Schultz");
assertThat(owners.size()).isEqualTo(found + 1);
}
23. Avec Spring Test
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/mvc-core-config.xml"})
@WebAppConfiguration
public class PetControllerTests {
@Autowired PetController petController;
MockMvc mockMvc;
@Before
public void setup() {
mockMvc = MockMvcBuilders.standaloneSetup(petController) .build();
}
@Test
public void testProcessUpdateFormSuccess() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/pets/{petId}/edit", 1,1)
.param("name", "Betty")
.param("type", "hamster")
.param("birthDate", "2015/02/12")
)
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
} }
Tester un contrôleur Spring MVC
24. Tester du code legacy
Code faisant appel à un Singleton avec getIntance()
Création d’un setter de visibilité package permettant de passer un
mock
Méthode phagocyte
Refactoring en sous méthodes qui seront testées individuellement
Méthode private
Changement de la visibilité en portée package
25. Conclusion
Ecrire des tests, cela s’apprend
Tester unitairement, c’est coder
A chaque couche / techno, sa typologie de test
Notas do Editor
Classement personnel issu d’un certain pragmatisme
Test de la configuration Spring s’apparente davantage un test d’intégration mais comme stable et relativement rapide => TU ?
Utilisation d’une base de données en mémoire ?
Tests fonctionnels tests d’acceptance / tests d’IHM
HP Fortify permet de tester la vulnérabilité du code à l’égard de failles de sécurité
Ces 4 types de tests sont complémentaires. Un TU garantie que le contrat de la classe à tester est respectée mais ne garantie pas que l’application respecte les spécifications fonctionnelles.
De part leur nature, les tests d’intégration et les tests fonctionnels sont moins stables : dépendances aux données, à la disponibilité des adhérences, à l’infra …
Objectif premier lorsqu’on ne pratique pas le TDD : trouver des bugs
Le TU fait partie du Done en agile
Les TU permettent au CP de s’assurer à minima que le développeur à tester son code
Se prémunir de régression : penser aux autres développeurs qui maintiendront l’application
Un harnais de tests est toujours sécurisant
Documenter le code : Spring REST Docs utilise les tests pour générer la doc de son API web. Contrairement à la JavaDoc, les TU sont toujours à jour.
Contribue au design de l’application : un code testable est souvent plus lisible / maintenable (pas de singleton ou de méthodes phagocytes)
Design logiciel : couplage faible (IoC), principe de substitution de Liskow (emploi correct de l’héritage), conception orientée service
L’écriture de TU a un coût (souvent estimé à 20% de la charge de dévs) et sa maintenance en a également un,
Eviter de tester du code legacy qui ne change jamais
Des TU sans assertions ne servent à rien (mise à part vérifier qu’aucune exception n’est levée)
Un TU non exécuté (@Ignore ou EclipseTestXXX) n’est pas maintenu
Nom des méthodes de tests : nom à rallonge en camelCase ou avec des _
Messages d’erreur davantage explicite avec AssertJ que JUnit
Qui dit test, dit jeux de données.
Code existant difficilement testable ne respectant pas les principes OCP, LSP, KISS, ISP, DIP, SOLID (cf Clean Code)
Exemple inspiré de la documentation Junit. Mais décomposition valable quelque soit la techno.
Given When Then
fait partie de la méthode agile Behavior-Driven Development
http://martinfowler.com/bliki/GivenWhenThen.html
Bonne pratique : séparer les blocs given/when/then par une ligne vide
Mock = simulacre
Les bouchons ne sont pas des simulacres : http://martinfowler.com/articles/mocksArentStubs.html
Les mockes évitent de devoir coder des stubs et les maintenir (par exemple lors d’ajout de méthode dans une interface)
Adhérences : DAO, autre service métier …
Utilisation des mocks contestées
Le NoMock Movement : http://antoniogoncalves.org/2012/11/27/launching-the-nomock-movement/
Mock ou pas Mock ? : https://www.fierdecoder.fr/2015/11/mock-ou-pas-mock/
Autres outils non présentés : AssertJ,
assertReflectionEquals pratique dans les cas suivant :
Mapping objet / objet
Marshalling / Unmarshalling
Persistance / rechargement en base
Introduction :
Spring encourage le développement en utilisant des POJO. En principe, ceux-ci sont testables sans Spring.
Cependant, il peut être tout de même intéressant de s’appuyer sur Spring pour :
Profiter de l’injection de dépendance dans les tests,
Tester unitairement la configuration Spring
Réutiliser la configuration Spring
Possibilité de ne pas utiliser le module spring test : création du contexte applicatif dans la méthode setUp
Spring 4.2 supporte Junit 4.9 et +
Mise en cache : pour tous les tests utilisant le même fichier de configuration Spring
Demander à JUnit d’utiliser tel runner passe par l’annotation @RunWith (même principe utilisé par Unitils ou Mockito)
Autres annotations :
BeforeTransaction, AfterTransaction
DirtiesContext : invalide le contexte afin que la prochaine méthode de test reconstruise le context Spring
IfProfileValue : exécute un test en fonction de l’évaluation d’une variable système (ex: test spécifique à la plateforme)
Autres bouchons : MockJspWriter, MockServletContext, MockPortletSession
Spring MVC Test Framework : http://docs.spring.io/spring/docs/current/spring-framework-reference/html/integration-testing.html#spring-mvc-test-framework
Les annotations @Sql, @SqlConfig et @SqlGroup ont été introduites dans Spring 4.1
Alternative : DbSetup pour créer des jeux de données en Java
Exemples issus de la documentation officielle : http://mockito.org/
A la place du verify, privilégier plutôt assertEquals(admin, loadedUser);
Performance : vérifier le nombre d’appel d’un WS
Plus d’informations : http://www.baeldung.com/mockito-spy
Exemple tiré du blog https://rwehner.wordpress.com/2010/02/23/mockito-some-useful-programming-examples/
D’autres exemples : http://www.programcreek.com/java-api-examples/org.mockito.ArgumentCaptor
Base de données embarques : H2 ou HSQLDB
DbUnit (XML) ou DbSetup (Java)
Création du schéma : ResourceDatabasePopulator, @Sql, namespace <jdbc:initialize-database/>
Gestion manuelle des transactions : TestTransaction
Exécution du code en dehors des tx : @BeforeTransaction, @AfterTransaction
Nécessite une surcouche technique. Exemple : Spring Test DbUnit https://github.com/springtestdbunit/spring-test-dbunit
Dans cet exemple, la classe abstraite AbstractDaoTest est chargée de :
Instancier le DAO à tester
Initialiser la base de données embarquée
Charger la configuration Hibernate
Créer la table ACCOUNT
La classe AbstractDaoTest utilise ou non Spring Test
La classe DbUnitLoader factorise le code permettant de charger un dataset
Classe de test inspiré de la classe ClinicServiceJpaTests du projet spring-petclinic
La méthode assertThat vient de la librairie AssertJ http://joel-costigliola.github.io/assertj/
@Transactional : inutile de nettoyer la base après le test
Extrait de spring-petclinic : https://github.com/spring-projects/spring-petclinic/blob/master/src/test/java/org/springframework/samples/petclinic/web/PetControllerTests.java