Les outils pour écrire du bon code 🧰

Overview

  • Packager son code : maven (java), buildout(python)
  • Test : Junit4/5 (java), pytest(python)
  • Logging : log4j (java), logger(python)
  • Bonnes pratiques de code

Packager son code 📦

Packager en ligne de commande

  • Source code : votre code (vivant), fortement dépendant de votre environnement de travail
  • Code packagé : code non modifiable (ex compilé) fait pour être exécuté
  • Beaucoup d'IDE font ça pour nous
  • Mais avec le conteneurisation, CI/CD, cloud computing, besoin de savoir faire ça à la main.
  • Besoin juste de faire la différence entre langage nécessitant une compilation (Java, C, Kotlin, C#) et les interprétés (python, js)

Historique

  • Dans les années 70 : compilation des programmes de plus en plus longue et complexe (plusieurs fichiers, commandes, interdépendance)
  • Make pour automatiser la compilation, fichier makefile avec toutes les commandes, facile à partager et réutiliser
  • Java : langage compilé, différentes versions, de dépendances externe etc
  • Maven : remplaçant de Make pour java

C'est quoi Maven

  • ⛽Un outil de gestion des dépendances
  • 🚀Un outil de build + déploiement
  • 📚Un outil de documentation

Pas d'équivalent en python (et c'est bien dommage)

Les objectifs

  • Rendre transparentes les tâches de build (cycle de vie maven)
  • Uniformisation des outils (plugin maven, ligne de commande)
  • Standardisation des pratiques de dev (convention over configuration)
  • Limiter le temps de formation des juniors
  • Limiter l'impact de l'hétérogénéité des configs personnelles
  • Se concentrer sur l'écriture de code

Un monde sans Maven (ou équivalent) 🔥👿

  • Importer à la main les dépendances (pensez pip et le projet 2A)
  • Devoir compiler à la main son application (merci les IDE de faire ça pour nous 💖)
  • Incompatibilités possibles en fonction des versions de java installées
  • Customisation du process de build/déploiement difficile

Cycle de vie java : compilation/exécution

  • Java est un langage qui doit être compilé en bytecode puis interprété par la JVM
  • Besoin de deux instructions
    1. Compilation : javac
    2. Exécution : java

Cycle de vie java

  1. Ecriture de code
  2. Compilation du code :
    
    javac file1.class file2.class
                                    
  3. Lancer le code :
    
    java main_file
                                

Cycle de vie java : dépendances externes

  • Un projet à souvent des dépendances externes 🧳
  • Mais elles sont déjà compilées (.jar) 📦
  • Donc ne peuvent pas l'être à nouveau 🤔
  • Mais sont nécessaires pour l'application 😱
  • Besoin d'ajouter ces dépendances au classpath

Classpath

Classpath est un paramètre passé à une machine virtuelle Java qui définit le chemin d'accès au répertoire où se trouvent les classes et les packages Java afin qu'elle les exécute. Au sein de la machine virtuelle, c'est le chargeur de classe qui parcourt les répertoires spécifiés par le classpath pour trouver où sont installées les classes requises par un programme Java.

Cycle de vie java : jar

  • Pas toujours évident de livrer une application contenant plusieurs centaines de fichiers .java et des dépendances externes
  • Besoin de packager tout ça
  • Deux grands formats : jar, war
  • Basiquement des archives de fichiers avec des metadata
  • Dans le cas d'un jar les metadata sont dans un fichier MANIFEST.MF

Manifest-Version: 1.0
Main-Class: ClassWithMainMethod
Class-Path: useful jars
                        

Cycle de vie java

  1. Ecriture de code
  2. Compilation du code :
    
    javac [-cp external jars] file1, file2...
                                    
  3. Lancer le code :
    
    java [-cp external jars] main_file
                                
  4. Création d'un jar :
    
    jar cmf path.to.MANIFEST jar_name.jar [file1.class, file2.class/*.class]
                                
  5. Lancer un jar :
    
    java jar [path.to.jar]
                                

Hands-on 1.1 ☕: une classe basique

  • Créez une class App avec un main qui print un hello world
  • Compilez cette classe via le terminal
  • Lancez la
  • Faites-en un jar exécutable (internet est votre ami)

Hands-on 1.2 ☕: dépendances externes

  • Récupérez le code sur Moodle
  • Compilez les classes
  • Lancez App.class
  • Faites-en un jar exécutable et lancez le

Meet Maven 🐦

Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM) , Maven can manage a project's build, reporting and documentation from a central piece of information.

Générer un projet maven

  1. Vérifier que vous avez maven d'installé
  2. 
    mvn -B archetype:generate   -DgroupId=com.mycompany.app 
                                -DartifactId=my-app 
                                -DarchetypeArtifactId=maven-archetype-quickstart 
                                -DarchetypeVersion=1.4
                                    
  3. Génère un pom.xml basique et l'arborescence classique maven
    
                                        project
                                        |-- pom.xml
                                        `-- src
                                            |-- main
                                            |   `-- java
                                            |       `-- $package
                                            |           `-- App.java
                                            `-- test
                                                `-- java
                                                    `-- $package
                                                        `-- AppTest.java
                                   

Cycle de vie Maven

  1. validate - validate the project is correct and all necessary information is available
  2. compile - compile the source code of the project
  3. test - test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed
  4. package - take the compiled code and package it in its distributable format, such as a JAR.
  5. verify - run any checks on results of integration tests to ensure quality criteria are met
  6. install - install the package into the local repository, for use as a dependency in other projects locally
  7. deploy - done in the build environment, copies the final package to the remote repository for sharing with other developers and projects.
  • clean : cleans up artifact created by prior builds
  • site : generates site documentation for this project

Cycle de vie Maven

  • Pour build un projet : mvn package
  • Pour déployer : mvn deploy
  • Pour générer un site documentation : mvn site
  • Plus généralement : mvn build phase[:plugin goal]

Le Project Object Model (POM.xml) 🍎

Contient

  • Les metadata du projet : nom, version, contributor
  • Version de java à utiliser
  • Comment il faut le packager : war / jar
  • Les dépendances : bibliothèques externes
  • Comment il faut le builder
Contient toutes les informations pour faire fonctionner votre projet

Exemple de POM


                            
                                4.0.0
                               
                                fr.ensai.app
                                my-app
                                1

                                
                                    1.8
                                    1.8
                                

                                
                                    
                                      junit
                                      junit
                                      4.12
                                      jar
                                      test
                                      true
                                    
                                

                              
                        

Options de build

Hands-on 2 🐦: Maven

  1. Reprenez le code de l'exercice précédent
  2. Créez un pom.xml pour maveniser votre application
  3. Pensez à ajouter la dépendance Apache Commons Collections (go internet)
  4. Compilez votre application avec maven

Wrapper maven

  • Maven permet de gérer la version java
  • Mais qui gère la version de maven ?
  • Wrapper maven : plugin maven qui permet de livrer un script qui encapsule maven
  • Ce script n'a pas besoin d'avoir maven d'installé pour fonctionner
  • Il permet de s'assurer que deux personnes qui lancent le script obtiennent le même résultat

Wrapper maven : les commandes

  1. Créer un fichier .mvnw
    
                                    mvn wrapper:wrapper
                                
  2. Utiliser maven depuis un wrapper maven
    
                                    # Unix
                                    ./mvnw clean install
                                    # Windows
                                    mvnw.cmd clean install
                                

Hands-on 2.1 📦🐦: Wrapper Maven

  1. Reprenez le code de l'exercice précédent
  2. Créez un wrapper maven pour le code
  3. Faites un clean install en utilisant le wrapper

Les tests

tester c'est douter
Source : commitstrip Lien

Définition

“En informatique, un test désigne une procédure de vérification partielle d'un système. Son objectif principal est d'identifier un nombre maximum de comportements problématiques du logiciel. Il permet ainsi, dès lors que les problèmes identifiés seront corrigés, d'en augmenter la qualité.

D'une manière plus générale, le test désigne toutes les activités qui consistent à rechercher des informations quant à la qualité du système afin de permettre la prise de décisions.

Un test ressemble à une expérience scientifique. Il examine une hypothèse exprimée en fonction de trois éléments : les données en entrée, l'objet à tester et les observations attendues. Cet examen est effectué sous conditions contrôlées pour pouvoir tirer des conclusions et, dans l'idéal, être reproduit

En résumé

  • Aucun test n'est parfait !
  • Donne seulement une information partielle de la qualité de votre application
  • Permet de détecter des "erreurs"
  • Et de les corriger si vous êtes consciencieux
  • Il y en a plusieurs types
  • Et ils doivent être reproductibles !

Pourquoi en tant que dev ?

  • Vérifier que notre code fait ce qu'il doit faire
  • Éviter les régressions quand on modifie le code

Quoi tester dans une application ?

  • Les fonctions unitairement
  • Les modules entre eux
  • Les use cases en entier
  • La navigation dans l'ihm
  • La résistance au nombre d'utilisateur
  • La résistance aux intrusions
  • ...

Les tests associés

  • Tests unitaires
  • Tests d'intégration
  • Tests fonctionnels
  • Tests d'IHM
  • Tests de charge
  • Test d'intrusion

Quand tester ?

Quand tester ?

Au début !!!!!

Plus on teste tôt et plus les tests sont efficaces et peu coûteux !

Le Test Driven Development (TDD)

Pratique qui consiste à écrire les tests AVANT de coder

  • On commence par écrire un test (inputs, objet à tester,outputs)
  • Puis on définit les fonctions nécessaires pour lancer le test
  • On lance le test -> échec
  • On code la fonction pour une réussite minimale -> réussite
  • On met à jour la fonctions -> refactor

Le Test Driven Development (TDD)

Cycle global TDD
Par Xarawn — Travail personnel , CC BY-SA 4.0, Lien

Avantages du TDD

  • Grande quantité de tests
  • Rassurant
  • Limite le code inutile

Inconvénients du TDD

  • Beaucoup de temps à faire des tests
  • Il faut maintenir les tests
Avantages >>>> Inconvénients

Les tests unitaires

Les tests unitaires

  • Vérifie qu'une méthode fait bien ce qu'elle doit faire
  • Cas nominaux (doivent fonctionner)
  • Cas d'erreur (doivent lever une erreur)
  • Cas à la marge (ça dépend)
  • Tests à la charge des développeurs

Junit5

  • Le framework de test le plus récent en java
  • Besoin de l'ajouter aux dépendances du projet (dans le pom.xml)
  • Généralement les tests sont dans le dossier test
  • Utilisation d'annotations (pareil en python)

Ajouter Junit5 à votre projet


                            
                        

                            class StandardTests {
                                @BeforeAll
                                static void initAll() {
                                }
                                @BeforeEach
                                void init() {
                                }
                                @Test
                                void succeedingTest() {
                                }
                                @Test
                                void failingTest() {
                                    fail("a failing test");
                                }
                                @Test
                                @Disabled("for demonstration purposes")
                                void skippedTest() {
                                    // not executed
                                }
                                @Test
                                void abortedTest() {
                                    assumeTrue("abc".contains("Z"));
                                    fail("test should have been aborted");
                                }
                                @AfterEach
                                void tearDown() {
                                }
                                @AfterAll
                                static void tearDownAll() {
                                }   
                            }
                        

Anatomie d'un test

Deux structures courantes et équivalentes

  • GIVEN WHEN THEN
  • ARRANGE ACT ASSERT

Anatomie d'un test

  1. Un nom clair qui permet de savoir ce que fait le test
  2. Définition et mise en forme des inputs (GIVEN/ARRANGE)
  3. Réalisation des actions que l'on souhaite tester (WHEN/ACT)
  4. Vérification des assertions (THEN/ASSERT)

                            class StandardTests {
                                @Test
                                void dummyTest() {
                                    // GIVEN
                                    int integer1 = 1;
                                    int integer2 = 3
                                    // WHEN
                                    int sum = integer1+integer2
                                    // THEN
                                    assertEquals(4, sum)
                                }
                            }
                        

                            @Test
                            public void addToppingOk() {
                                // GIVEN
                                Cake cake = new Cake(Ingredient.VANILLA,4);
                                // WHEN
                                cake.addTopping(Ingredient.SPARKLE);
                                // THEN
                                assertEquals(1, cake.getToppings().size());
                                assertTrue(cake.getToppings().contains(Ingredient.SPARKLE));  ;
                            }
                        

C'est quoi un bon test unitaire ?

  • Teste qu'UNE chose
  • Le plus isolé possible
  • Un assert "simple"
  • Commenté
  • Maintenable
Fait partie du code et est de la documentation

La couverture de test

  • Indicateur simple à calculer
  • Combien de lignes sont exécutées via les tests
  • Mais pas très fiable
  • Facile de faire des "faux tests"
  • Préférer faire peu de tests utiles que beaucoup inutiles
  • 80% des problèmes viennent de 20% du code

Hands-on 3 👩‍💻👨‍💻: Tests unitaires

  • Récupérez le code sur moodle
  • Ajoutez la dépendance à Junit5
  • Codez des tests pour toutes les fonctions
  • Lancez vos tests
  • Allez de votre votre couverture de test (target/site/jacoco/index) et améliorez la si besoin

Mutation testing 🦠

  • Les tests permettent d'évaluer la qualité du code
  • Mais qui évalue la qualité des tests ?
  • Il est facile de faire de faux tests

Mutation testing : le principe

  • Modifier légèrement le code de l'application (= faire un mutant)
    • Changer les bornes des conditions > devient >= et inversement
    • Changer incrément en décrément
    • Changer les opérations mathématique
    • ...
  • Lancer les tests qui couvrent le code
  • Si les tests plantent le mutant est tué
  • Sinon le mutant s'échappe
  • Et on souhaite qu'aucun mutant ne s'échappe

Exemple


                            if ( i >= 0 ) {
                                return "foo";
                            } else {
                                return "bar";
                            }                            
                        

mute en


                            if ( i > 0 ) {
                                return "foo";
                            } else {
                                return "bar";
                            }                            
                        

On veux s'assurer qu'au moins un des tests échoue

Pitest : un framework java

  • Compatible Junit4/5
  • Uniquement à ajouter les dépendances dans le pom.xml pour comportement par défaut
  • Doc officielle pour plus d'information
  • En python : mutmut (dernière maj 04/11/21) mutatest(dernière maj 16/05/20)

Dépendance maven


                                                      
                                

Configuration du build


                                                   
                                

Et maintenant ? 🎮

  • Juste à lancer le goal
                                        
                                            mvn test
                                        
                                    
  • Allez lire le rapport dans target>pit-reports
  • Modifiez ses tests

Hands-on 4 🦠: mutation testing

  • Mettez en place Pitest dans votre projet
  • Regardez combien de mutants s'échappent
  • Améliorez vos tests pour tous les éliminer

Une activité à part

  • Test unitaire : pour le dev
  • Les autres : rôle du testeur (responsable qualité)
  • Expertise sur comment mener des tests
  • Indépendant de l'équipe
  • Permet d'éviter les "faux tests"

Résumé des tests

  • Tester dés le début
  • Les tests ne sont jamais parfaits
  • Mais c'est mieux que rien
  • Informe sur la qualité de l'application
  • Cas nominal/erreur/marge
  • Structure GIVEN/WHEN/THEN

Logging 📚

Logging

  • Consiste à émettre et enregistrer des messages suite à des événement
  • Connaître l'état d'un objet, un exception levée, arrivé à un point du code
  • Facile debug, détection d'anomalie, flux de traitement
  • Fonctionne en production, alors que le debug non
  • Importance proportionnelle à la taille de l'application
  • Plusieurs API (Log4j java, logging python)

Logging

  • Permet d'avoir la chronologie des événements (attention en cas de multithreading)
  • Peut être persisté et traité par un système dédié
  • Plusieurs niveau de log
  • Plusieurs sorties possibles
  • Remplace le System.out.println() ou le print()

Log4j (java)

  • Framework populaire java
  • Fiable, rapide, extensible
  • Simple d'utilisation
  • Granularité fine
  • COnfiguration au runtime avec fichier de configuration

Log4j : installation



    
        org.apache.logging.log4j
        log4j-api
        2.17.1
    
    
        org.apache.logging.log4j
        log4j-core
        2.17.1
    

                        

Log4j architecture

  • Loggers : Responsable pour capturer les information de log
  • Appenders : Responsable pour publier les log vers différentes information
  • Layouts : Responsable pour formater les logs

Log4j configuration

  • Configuration dans le code (statique, difficile à faire évoluer, non présenté)
  • Configuration dans un fichier (facile à modifier, surcharger)

Log4j configuration


                            
                            
                                
                                    
                                        
                                    
                                    
                                        
                                            %d %p %c{1.} [%t] %m%n
                                        
                                    
                                
                                
                                    
                                        
                                        
                                    
                                    
                                        
                                    
                                
                            
                        

Logger

  • Responsable de capter les messages
  • On y accède via le code Java
  • Configuration via fichier externe
  • Chaque logger à un nom (convention java, con.foo.bar.ClassName)
  • Héritage possible
  • Priorisation selon le "niveau du message" (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)

Appender

  • Envoie le message dans la bonne destination
  • Un même logger peut avoir plusieurs appenders
  • Chaque appender à un layout
  • ConsoleAppender, FileAppender, RollingFileAppender, SMTPAppender, etc

Layout

  • Pour déterminer l'affichage des logs
  • HTMLLayout, PatternLayout, SimpleLayout, XMLLayout
  • Pattern Layout : %d %p %c{1.} [%t] %m%n
  • Ajoute de couleur avec highlight{pattern}{style}

Les bases de Log4j

  • Qui log les messages ?Les loggers
  • Qui défini le format ? Les layouts
  • Qui décide la priorité des messages ?Son niveau
  • Qui décide où sera envoyé le message ?Les appenders

Logger API


                            import org.apache.logging.log4j.LogManager;
                            import org.apache.logging.log4j.Logger;
                            
                            public class App 
                            {
                                private static Logger logger = LogManager.getRootLogger();
                                public static void main( String[] args )
                                {
                                    logger.trace("msg de trace");
                                    logger.debug("msg de debogage");
                                    logger.info("msg d'info");
                                    logger.warn("msg de warn");
                                    logger.error("msg d'erreur");
                                    logger.fatal("msg fatal");
                                }
                            }
                        

Hands-on : mise en place d'un logger 📚

  1. Récupérez le code de la partie d'avant
  2. Ajoutez différents loggers
    • Un logger qui log tout en console et dans un fichier
    • Un logger qui log les messages WARN et plus dans un fichier

Python logging module

  • Logguer n'est pas spécifique à java
  • Logging : module python de base pour logguer
  • Mêmes principes que log4j, logger, handler, formater, level
  • Configuration par fichier > configuration dans le code

Python logging module

logging.yml

                                    version: 1
                                    formatters:
                                      simple:
                                        format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
                                    handlers:
                                      console:
                                        class: logging.StreamHandler
                                        level: DEBUG
                                        formatter: simple
                                        stream: ext://sys.stdout
                                    loggers:
                                      simpleExample:
                                        level: DEBUG
                                        handlers: [console]
                                        propagate: no
                                    root:
                                      level: DEBUG
                                      handlers: [console]
                                

Python logging module

Fichier python

                                    import logging.config
                                    import yaml
                                    
                                    with open('./logging.yml', 'r') as stream:
                                        config = yaml.load(stream, Loader=yaml.FullLoader)
                                    
                                    logging.config.dictConfig(config)
                                    
                                    # create logger
                                    logger = logging.getLogger('simpleExample')
                                    
                                    # 'application' code
                                    logger.debug('debug message')
                                    logger.info('info message')
                                    logger.warning('warn message')
                                    logger.error('error message')
                                    logger.critical('critical message')