Règles Tests¶
Copy of
sumaris-pod/.agents/rules/unit-testing.md(rule written for AI coding agents, when writing or modifying unit tests, integration tests, or test data fixtures). Do not edit here: runnpm run site:sync-rulesto update it.
Tests Unitaires¶
- Utiliser JUnit 4 (pas JUnit 5) et DBUnit.
- Base de données : Utiliser
DatabaseResource(et nonDataResource) pour gérer la base HSQLDB. - Données : Les données sont chargées à partir de fichiers XML via DBUnit. Ces fichiers se trouvent dans
sumaris-test-shared/src/main/resources. - Génération DB : La base de test est générée une fois via
InitTests, depuissumaris-core:Cela crée les fichiers HSQLDB danscd sumaris-core mvn -Phsqldb,run test-compile exec:javatarget/db/(et marquetarget/db/sumaris.propertiesenreadonly=trueà la fin). Les tests utilisent ensuite une copie de cette base (writeDb()) ou la base directement (readDb(), voir plus bas).-Phsqldbest indispensable ici : le profilhsqldbestactiveByDefault, mais cette activation par défaut est désactivée dès qu'un autre profil (-Prun) est passé explicitement — sans lui, la tâche Ant échoue avecproperty 'jdbc.driver' must be set.test-compileest nécessaire carInitTestsvit danssrc/test/java;exec:javaseul n'exécute aucune phase du cycle de vie et ne trouverait pas la classe.
- Rechargement : La base est regénérée manuellement (pas automatiquement avant chaque test). Si les fixtures XML changent, relancer
InitTests. - Assertions : Préférer
Assert.assertEquals,Assert.assertNotNull, etc. (JUnit 4).
Lancer les tests¶
# Toute la suite d'un module
mvn test -pl sumaris-server
# Une seule classe (Surefire inclut par défaut **/*Test.java et **/*Tests.java, sans besoin de config particulière)
mvn test -pl sumaris-server -Dtest=MyServiceTest -DfailIfNoTests=false
Sur JDK 23+ (JEP 471), ajouter MAVEN_OPTS="--sun-misc-unsafe-memory-access=allow" pour supprimer un warning sun.misc.Unsafe bruyant émis par la Guava embarquée par Maven lui-même ($MAVEN_HOME/lib/guava.jar), sans rapport avec le projet.
Configuration des tests¶
- Chaque module de test doit avoir une
TestConfigurationannotée avec@SpringBootApplicationet@ComponentScan. - Pour les tests de DAO, utiliser
DaoTestConfiguration. - Pour les tests de Service, utiliser
ServiceTestConfiguration. - Garder
TestConfigurationsynchronisée avecApplication.java(excludes du@SpringBootApplication,@EnableCaching, etc.) : un décalage fait échouer silencieusement des fonctionnalités entières dans les tests (ex:@EnableCachingabsent deTestConfiguration(sumaris-server) → aucunCacheManagerjamais disponible dans aucun test du module, sans message d'erreur évident tant qu'aucun test n'en dépendait).
Tester un feature-flag (activé / désactivé)¶
Pour vérifier qu'une propriété comme spring.jms.enabled ou spring.cache.enabled active/désactive réellement une fonctionnalité, écrire deux classes de test :
- une utilisant la config de test par défaut (
sumaris-{module}-test.properties) pour le cas désactivé ; - une avec un fichier de properties dédié, additif, via
@TestPropertySource(locations = "classpath:...-enabled.properties")sur la classe de test (leslocationss'ajoutent à celles héritées deAbstractServiceTest, sans les remplacer), pour le cas activé.
Chaque combinaison de @TestPropertySource distincte fait démarrer un contexte Spring séparé (pas de partage avec les autres tests) — c'est le comportement attendu, pas un bug.
Gestion de la base de données (DatabaseResource)¶
- La
DatabaseResourcepermet de gérer le cycle de vie de la base de données de test (HSQLDB). - Elle propose deux modes d'ouverture :
readDb(): Ouvre la base de données en lecture seule, directement sur../sumaris-core/target/db(pas de copie). Nécessite quetarget/db/sumaris.propertiescontienne déjàreadonly=true— ce que fixeInitTestsen fin d'exécution (voir plus haut) ou une exécution complète de la suiteAllTests. Si ce fichier/propriété est absent, le test échoue à l'initialisation avecCould not find readonly property on db config.writeDb(): Ouvre la base de données en lecture/écriture. La base est copiée depuis../sumaris-core/target/dbdans un répertoire temporaire pour chaque test afin d'isoler les modifications — ne nécessite pas que la base source soit en readonly. PréférerwriteDb()par défaut pour un nouveau test,readDb()uniquement si le besoin de rapidité (pas de copie) est avéré et que l'environnement est déjà initialisé.
Bonnes Pratiques¶
- Indépendance : Chaque test doit être indépendant des autres.
- Nettoyage : Ne pas hésiter à vérifier l'état de la base après une suppression ou une mise à jour.
- Mocks : Utiliser Mockito si nécessaire, mais privilégier les tests d'intégration avec HSQLDB pour les couches Repository/Service si les données sont complexes.
- Couvre les cas limites, erreurs et chemins négatifs; privilégie des tests rapides et déterministes.
Accélération des tests avec HSQLDB server externe¶
Par défaut, les tests utilisent une BDD HSQLDB file-based copiée dans un répertoire temporaire. Pour gagner du temps (pas de copie de DB, pas d'InitTests), on peut pointer sur une BDD HSQLDB server déjà lancée (port 9001) :
# 1. Lancer HSQLDB server (une fois par session)
cd /opt/data/git/sumaris-pod/sumaris-core/src/test/scripts
bash restartServer.sh --force
# 2. Lancer les tests en pointant sur le server
mvn test -pl sumaris-core -Dtest=MyTest -o \
-Dspring.datasource.url=jdbc:hsqldb:hsql://localhost/sumaris
⚠️ En mode server, les tests partagent la même BDD. Utiliser
writeDb()avec précaution (les modifications persistent entre tests). Pour l'isolation, préférer des IDs uniques (timestamp) oureadDb()pour les tests en lecture seule.
Tests GraphQL (sumaris-server)¶
Les tests GraphQL utilisent graphQLTestTemplate via AbstractGraphQLServiceTest. Le pattern :
1. Créer les fichiers .graphql¶
Dans sumaris-server/src/test/resources/graphql/ :
Query (ex: myEntity.graphql) :
query MyEntity($id: Int!) {
myEntity(id: $id) {
__typename
id
label
comments
measurementValues
program { __typename id label }
recorderDepartment { __typename id label }
}
}
Mutation (ex: saveMyEntity.graphql) :
mutation SaveMyEntity($entity: MyEntityVOInput!) {
saveMyEntity(entity: $entity) {
__typename
id
label
comments
measurementValues
}
}
2. Écrire le test¶
public class MyEntityGraphQLTest extends AbstractGraphQLServiceTest {
@ClassRule
public static final DatabaseResource dbResource = DatabaseResource.writeDb();
@Before
public void setUp() throws Exception {
super.setUp();
}
@Test
public void saveMyEntity() throws Exception {
Assume.assumeTrue(authenticate("admin@sumaris.net", "admin"));
// 1. Load via GraphQL query
ObjectNode loadVars = objectMapper.createObjectNode();
loadVars.put("id", 301);
MyEntityVO loaded = getResponse("myEntity", MyEntityVO.class, loadVars);
Assert.assertNotNull(loaded);
// 2. Modify
loaded.setComments("New comment #" + System.currentTimeMillis());
// 3. Save via GraphQL mutation
ObjectNode saveVars = objectMapper.createObjectNode();
JsonNode entityNode = objectMapper.valueToTree(loaded);
removeTypenameFields(entityNode); // GraphQL input types reject __typename
saveVars.set("entity", entityNode);
MyEntityVO saved = getResponse("saveMyEntity", MyEntityVO.class, saveVars);
Assert.assertNotNull(saved);
// 4. Reload and verify
MyEntityVO reloaded = getResponse("myEntity", MyEntityVO.class, loadVars);
Assert.assertEquals(loaded.getComments(), reloaded.getComments());
}
private void removeTypenameFields(JsonNode node) {
if (node == null || !node.isObject()) return;
ObjectNode obj = (ObjectNode) node;
obj.remove("__typename");
obj.fields().forEachRemaining(entry -> {
JsonNode value = entry.getValue();
if (value.isObject()) removeTypenameFields(value);
else if (value.isArray()) value.forEach(this::removeTypenameFields);
});
}
}
Points critiques¶
__typenameremoval : graphql-spqr rejette__typenamedans les inputs. Toujours le supprimer avant d'envoyer un VO comme variable de mutation.- NOT NULL columns : la query doit demander TOUS les champs NOT NULL de l'entité (
rankOrder,matrixId,program.id, etc.) sinon le save échoue avecintegrity constraint violation. DatabaseFixtures: le modulesumaris-servern'a PAS accès ànet.sumaris.core.dao.DatabaseFixtures. Hardcoder les IDs (ex:301pour OBSFISH sample #0) ou créer un fixture local danssumaris-server/src/test/.../DatabaseFixtures.java.getResponse(resourceName, ...): leresourceNamecorrespond au nom du fichier.graphql(sans extension). LequeryNamecorrespond au nom de la query/mutation dans le fichier.- Authentification :
authenticate("admin@sumaris.net", "admin")— toujours wraper dansAssume.assumeTrue()pour skip gracieux si l'auth échoue. - Lancement rapide :
mvn test -pl sumaris-server -Dtest=MyGraphQLTest -o -Dspring.datasource.url=jdbc:hsqldb:hsql://localhost/sumaris