Junit
Comment facilité la conception, l'écriture et la compréhension des tests unitaires
Les tests unitaires





Bonnes pratiques
- Choix à faire lors de l’écriture d’un test:
- Manipulation d’objets complexes => initialiser le minimum de ce dont on a besoin pour satisfaire le test.
- Risque calculé de ce que je veux tester par rapport à l’exhaustivité des tests
- Capitaliser les mocks ‘utilitaires’ (ex : nomenclature).
- Ecrire un TU
- Commencer par le test nominal
- Décrire le test avec gherkin (given…when…then)
- Ecrire de la même façon les autres tests
- Capitaliser les jeux tests d’un service si possible (un jeu de test pour tous les tests d’un même service)
- Faire test nominal en initialisant les données nécessaires. Repartir systématiquement de ces données en les dégradant suivant les besoins pour les autres testspour les tests
- Bug / Régression
- On cherche et complète le given / then OU ajoute un TU.
- Plus de tests à écrire mais tests beaucoup plus simple à comprendre, à écrire et à maintenir
- On n’a plus besoin de tester des méthodes private

Gherkin
Historique
- Ce langage est conçu pour être compréhensible par tous les acteurs du projet, y compris le non-technique. Il permet d'écrire des descriptions de fonctionnalités sous forme de phrases simples et naturelles en français.
- Issu du Behavior-Driven Development (BDD), car il permet d'exprimer la façon dont les utilisateurs interagissent avec le système, ce qui aide à définir les exigences du projet de manière claire et précise.
- US : En tant que … je veux …afin
- TA : Etant donnée …lorsque …alors (given …when …then) Etant donnée que je saisie
- Peut être utilisé dans les tests unitaires afin de coder les tests tout en les décrivant.
- Présent dans Mockito
Principe
- Given : création des inputs
- Initialisation des paramètres de la méthode
- Initialisation des mocks => thenReturn spécifique au TU ou thenAnswer générique suivant comment il est appelé
- When : quelle méthode testons nous ?
- Souvent une seule instruction mais permet d’identifier en un coup d’œil la méthode testée
- Then : Comment vérifier que le test est ok ?
- Verify() on a bien appelé les services mockées
- assertThat() : sur le retour du service ou sur les paramètres à des mocks (Captor)
première itération

deuxième itération
- Inconvénients
- Demande de la rigueur dans la description des tests
- Pas de ligne directrice pour faciliter l'écriture des tests
- Améliorations possibles
- Diriger le développeur dans l'écriture du test Il existe des librairies qui permettent de faciliter l'écriture des tests unitaires en utilisant le gherkin mais je voulais rester dans la simplicité surtout que ce n'est pas pour le moment une volonté générale dans l'équipe. => Limiter les dépendances et l'ajout de librairies J'ai donc décidé de mettre en place le minimum de ce que je voulais normaliser, à savoir écire les tests comme on peut le faire avec les lambdas :
Gherkin.given()
.when()
.then();
Conception d'une librairie Gherkin
@startuml
Title Package Gherkin
' Styles
skinparam linetype ortho
skinparam classAttributeIconSize 0
skinparam shadowing false
skinparam classFontColor #ffffff
skinparam interfaceFontColor #ffffff
skinparam classAttributeFontColor #ffffff
skinparam fontColor #ffffff
skinparam classBackgroundColor #21252d
skinparam classBorderColor #4f5f72
skinparam interfaceBackgroundColor #21252d
skinparam interfaceBorderColor #4f5f72
skinparam ArrowColor #4c566a
' Interfaces
interface GivenStep {
+ and(action: ThrowingRunnable): GivenStep
+ when(action: ThrowingSupplier<R>): WhenStep<R>
+ when(action: ThrowingRunnable): WhenStep<Void>
}
interface WhenStep<R> {
+ then(assertion: ThrowingConsumer<R>): ThenStep<R>
+ then(assertion: ThrowingRunnable): ThenStep<R>
+ thenException(exceptionClass: Class<? extends Throwable>): ExceptionStep<R>
}
interface ThenStep<R> {
+ andThen(assertion: ThrowingConsumer<R>): ThenStep<R>
+ andThen(assertion: ThrowingRunnable): ThenStep<R>
+ andThen(assertion: ThrowingFunction<R, U>): ThenStep<R>
}
interface ExceptionStep<R> {
+ withMessage(message: String): ExceptionStep<R>
}
' Functional Interfaces
interface ThrowingRunnable {
+ execute(): void
}
interface ThrowingSupplier<T> {
+ get(): T
}
interface ThrowingConsumer<T> {
+ accept(t: T): void
}
interface ThrowingFunction<T, U> {
+ apply(t: T): U
}
' Classes
class Scenario {
{static} + given(action: ThrowingRunnable): ScenarioGiven
{static} + GivenStep, WhenStep, ThenStep, ExceptionStep
}
class ScenarioGiven {
- givens: List<Runnable>
+ given(action: ThrowingRunnable): ScenarioGiven
+ and(action: ThrowingRunnable): GivenStep
+ when(action: ThrowingSupplier<R>): WhenStep<R>
+ when(action: ThrowingRunnable): WhenStep<Void>
}
class ScenarioWhen<R> {
- givens: List<Runnable>
- whenThrowingSupplier: ThrowingSupplier<R>
+ then(assertion: ThrowingConsumer<R>): ThenStep<R>
+ then(assertion: ThrowingRunnable): ThenStep<R>
+ thenException(expected: Class<? extends Throwable>): ExceptionStep<R>
}
class ScenarioThen<R> {
- result: R
- assertions: List<Consumer<R>>
+ andThen(assertion: ThrowingConsumer<R>): ThenStep<R>
+ andThen(assertion: ThrowingRunnable): ThenStep<R>
+ andThen(assertion: ThrowingFunction<R, U>): ThenStep<R>
}
class ScenarioException<R> {
+ withMessage(message: String): ExceptionStep<R>
}
class AbstractScenarioTest {
# scenarioTestWatcher: ScenarioTestWatcher
+ beforeEachTest(): void
}
class ScenarioTestWatcher {
- results: List<TestResult>
+ testSuccessful(context: ExtensionContext): void
+ testFailed(context: ExtensionContext, cause: Throwable): void
+ testAborted(context: ExtensionContext, cause: Throwable): void
+ testDisabled(context: ExtensionContext, reason: Optional<String>): void
+ afterTestExecution(context: ExtensionContext): void
}
' Relations
ScenarioGiven ..|> GivenStep
ScenarioWhen ..|> WhenStep
ScenarioThen ..|> ThenStep
ScenarioException ..|> ExceptionStep
ScenarioGiven --> ScenarioWhen : creates >
ScenarioWhen --> ScenarioThen : creates >
ScenarioWhen --> ScenarioException : creates >
Scenario --> ScenarioGiven : uses
Scenario --> ScenarioWhen : uses
Scenario --> ScenarioThen : uses
Scenario --> ScenarioException : uses
AbstractScenarioTest --> ScenarioTestWatcher : has >
@enduml
Utilisation
public class MonTest extends AbstractScenarioTest {
private Service service;
private ServiceMock serviceMock;
private Donnee donnee;
@Test
@ScenarioTest(description = "Test nominal avec toutes les méthodes ")
public void testSimple() {
Scenario.given(this::initServices)
.and(this::maDonnee)
.when(this::executeService)
.then(this::checkResult)
.andThen(this::checkEnregistrement);
}
@Test
@ScenarioTest(description = "Test retournant une exception avec validation du message")
public void testException() {
Scenario.given(this::initServices)
.and(this::maDonneeNull)
.when(this::executeServiceWithException)
.thenException(IllegalArgumentException.class)
.withMessage("L'argument est erroné");
}
private Object executeService(Object parametre){
return service.execute(parametre);
}
private void checkResult(Object result){
assertEquals(result, "resultat attendu");
}
private void checkEnregistrement(){
verify(mockService,times(1)).execute(any());
}
}