Exercice

Le but de ces exercices est de réaliser le test d'une application Spring et de réaliser des modifications afin de se rendre compte de la pertinence des cas de tests mis en place.

L'application utilise des relevés géologiques faits dans le jura afin de comparer les concentrations en nickel et en cobalt du sol et d'afficher une régression linéaire sur ces deux variables

Les sources de l'application dont disponnibles sur gitlab :

git clone git@gitlab.com:julien-gauchet/tests-unitaires-exercices.git

Vous pouvez également les télécharger ici : Sources tests unitaires


Configuration des tests avec spring

Dans un premier temps, nous allons configurer le projet pour utiliser Spring test et une base H2

  • Créer une classe de config pemettant de récupérer un transactionManager pour les tests
  • Pour structurer les tests, créer deux classes abstraites contenant la config pour les tests
    • La première pour les tests simples (sans appel à la base de données)
    • La seconde initialise une base de données
<dependencies>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
	</dependency>
	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
	</dependency>
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-core</artifactId>
		<version>5.4.21.Final</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.data</groupId>
		<artifactId>spring-data-jpa</artifactId>
		<version>2.3.4.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-dbcp2</artifactId>
		<version>2.8.0</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-test</artifactId>
		<version>5.2.9.RELEASE</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-simple</artifactId>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>
@Profile("test")
@Configuration
@ComponentScan(basePackages = {"fr.julien.graphiques"})
@EnableJpaRepositories(basePackages = {"fr.julien.graphiques.dao.repository"})
public class ConfigTest {

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    /**
     * Interface entre EntityManagerFactory et Hibernate
     */
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        jpaVendorAdapter.setDatabase(Database.H2);
        return jpaVendorAdapter;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(dataSource());
        emf.setPersistenceUnitName("graphiques");
        emf.setPackagesToScan("fr.julien.graphiques.modele.entites");
        emf.setJpaVendorAdapter(jpaVendorAdapter());
        Properties jpaProperties = new Properties();
        jpaProperties.put("hibernate.show_sql", true);
        emf.setJpaProperties(jpaProperties);
        return emf;
    }

    @Bean("transactionManager")
    @Primary
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
}
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = { ConfigTest.class })
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class })
public abstract class AbstractTest {
}
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {ConfigTest.class})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class})
public abstract class AbstractTestBdd {

    @Autowired
    private DataSource dataSource;
    protected Connection connection;

    @PostConstruct
    public void postConstruct() {
        try {
            connection = dataSource.getConnection();
            JdbcDataSource dataSource = new JdbcDataSource();
            dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
            dataSource.setUser("sa");
            dataSource.setPassword("");
            Connection cnx = dataSource.getConnection();
            try (InputStreamReader fr = new FileReader(new File("src/test/resources/init_db2.sql")); BufferedReader br = new BufferedReader(fr)) {
                RunScript.execute(cnx, br);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Création d'un test

Je ne propose pas de correction de cette partie, l'idée sera de construire votre propre jeu de test

Testez le plus efficacement possible les classes de ce projet


Modification du modèle de données

Dorénavant, nous allons utiliser le fichier init_db2.sql pour initialiser la base de données

Pour savoir si vos tests unitaires sont corrects, le but du jeu sera de corriger les tests et le programme et de voir si après cette correction, le programme se lance ou si le jeu de tests était insuffisant pour procéder au lancement

@Entity
@Table(name = "concentrations")
public class Concentration {

    @Id
    private Integer id;
    @ManyToOne
    @JoinColumn(name = "id_releve")
    private Releve releve;
    @ManyToOne
    @JoinColumn(name = "id_minerai")
    private Minerai minerai;
    @Column(name = "valeur")
    private Double valeur;

    public Releve getReleve() {
        return releve;
    }

    public void setReleve(Releve releve) {
        this.releve = releve;
    }

    public Minerai getMinerai() {
        return minerai;
    }

    public void setMinerai(Minerai minerai) {
        this.minerai = minerai;
    }

    public Double getValeur() {
        return valeur;
    }

    public void setValeur(Double valeur) {
        this.valeur = valeur;
    }
}
@Entity
@Table(name = "minerais")
public class Minerai {

    @Id
    private String id;
    @Column(name = "libelle")
    private String libelle;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getLibelle() {
        return libelle;
    }

    public void setLibelle(String libelle) {
        this.libelle = libelle;
    }
}
@Entity
@Table(name = "releves")
public class Releve implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    private Integer id;
    @Column(name = "date_releve")
    private LocalDate date;
    @Column(name = "lieu")
    private String lieu;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public LocalDate getDate() {
        return date;
    }

    public void setDate(LocalDate date) {
        this.date = date;
    }

    public String getLieu() {
        return lieu;
    }

    public void setLieu(String lieu) {
        this.lieu = lieu;
    }
}
public interface ConcentrationsRepository extends JpaRepository<Concentration, Integer> {

    @Query("SELECT c FROM Concentration c WHERE c.minerai.id IN ('ni', 'co')")
    public List<Concentration> findStatistiques();
}
@Component
public class ReleveToNuagePointMapper {

    public NuagePoints releveToNuagePoint(List<Concentration> concentrations) {
        NuagePoints nuage = new NuagePoints(null, null);
        Map<Integer, Point> points = new HashMap<>();
        for (Concentration c : concentrations) {
            Point p = new Point();
            if (points.containsKey(c.getReleve().getId())) {
                p = points.get(c.getReleve().getId());
            }
            else {
                points.put(c.getReleve().getId(), p);
            }
            if (c.getMinerai().getId().equals("ni")) {
                p.setX(c.getValeur());
            }
            else {
                p.setY(c.getValeur());
            }
            points.remove(c.getReleve().getId());
            points.put(c.getReleve().getId(), p);
        }
        for (Point p : points.values()) {
            nuage.add(p);
        }
        return nuage;
    }
}
@Component
public class GenerationRegressionService {

    @Autowired
    private ConcentrationsRepository concentrationsRepository;
    @Autowired
    private ReleveToNuagePointMapper releveToNuagePointMapper;
    @Autowired
    private RegressionLineaireService regressionLineaireService;

    public ResultatRegression genererRegression() throws StatistiqueException {
        List<Concentration> c = concentrationsRepository.findStatistiques();
        NuagePoints nuage = releveToNuagePointMapper.releveToNuagePoint(c);
        ResultatRegression res = regressionLineaireService.calculer(nuage);
        res.setNuage(nuage);
        return res;
    }
}
@ActiveProfiles("test")
public class RegressionLineaireServiceTest extends AbstractTest {

    @Autowired
    private RegressionLineaireService regressionLineaireService;

    @Test
    public void testerMoyenne() {
        try {
            NuagePoints nuage = new NuagePoints(null, null);
            nuage.add(new Point(3, 3));
            nuage.add(new Point(4, 5));
            nuage.add(new Point(5, 5));
            nuage.add(new Point(6, 6));
            nuage.add(new Point(7, 5));
            nuage.add(new Point(8, 8));
            ResultatRegression res= regressionLineaireService.calculer(nuage);
            Assert.assertEquals(1.24d, res.getB0(), 0.1d);
            Assert.assertEquals(0.7d, res.getB1(), 0.1d);
        }
        catch (Exception e) {
            e.printStackTrace();
            Assert.fail();
        }
    }
}
@ActiveProfiles("test")
public class StatistiquesServiceTest extends AbstractTest {

    @Autowired
    private StatistiquesService statistiquesService;

    @Test
    public void testerMoyenne() {
        Assert.assertEquals(5.7d, statistiquesService.moyenne(new Vecteur(3, 4, 5, 6, 7, 9)), 0.1d);
    }

    @Test
    public void testerSomme() {
        Assert.assertEquals(34d, statistiquesService.somme(new Vecteur(3, 4, 5, 6, 7, 9)), 0.1d);
    }

    @Test
    public void testerVariance() {
        Assert.assertEquals(3.89d, statistiquesService.variance(new Vecteur(3, 4, 5, 6, 7, 9)), 0.1d);
    }

    @Test
    public void testerCovariance() {
        try {
            Assert.assertEquals(4.5d, statistiquesService.covariance(new Vecteur(3, 4, 5, 6, 7, 9), new Vecteur(2, 4, 5, 9, 7, 9)), 0.1d);
        }
        catch (StatistiqueException e) {
            e.printStackTrace();
            Assert.fail();
        }
    }
    
    @Test
    public void testerCorrelation() {
        try {
            Assert.assertEquals(0.17d, statistiquesService.correlation(new Vecteur(3, 4, 5, 6, 7, 9), new Vecteur(2, 4, 5, 9, 7, 9)), 0.1d);
        }
        catch (StatistiqueException e) {
            e.printStackTrace();
            Assert.fail();
        }
    }
}
@ActiveProfiles("test")
public class VecteursServiceTest extends AbstractTest {

    @Autowired
    private VecteursService vecteursService;

    @Test
    public void testerTransposition() {
        Vecteur v = new Vecteur(2, 3);
        v.add(2d);
        v.add(3d);
        Vecteur res = vecteursService.transposer(v);
        Assert.assertNotEquals(v.isColonne(), res.isColonne());
    }

    @Test
    public void testerAjout() {
        try {
            Vecteur res = vecteursService.ajouter(new Vecteur(2, 3), new Vecteur(1, 1));
            Assert.assertEquals(Double.valueOf(3), res.get(0));
            Assert.assertEquals(Double.valueOf(4), res.get(1));
        }
        catch (StatistiqueException e) {
            e.printStackTrace();
            Assert.fail();
        }
    }

    @Test
    public void testerMultiplier() {
        try {
            Vecteur res = vecteursService.multiplier(new Vecteur(2, 3), 2);
            Assert.assertEquals(Double.valueOf(4), res.get(0));
            Assert.assertEquals(Double.valueOf(6), res.get(1));
        }
        catch (Exception e) {
            e.printStackTrace();
            Assert.fail();
        }
    }

    @Test
    public void testerMin() {
        try {
            Assert.assertEquals(2d, vecteursService.min(new Vecteur(2, 3)), 0.1d);
        }
        catch (Exception e) {
            e.printStackTrace();
            Assert.fail();
        }
    }

    @Test
    public void testerMax() {
        try {
            Assert.assertEquals(3d, vecteursService.max(new Vecteur(2, 3)), 0.1d);
        }
        catch (Exception e) {
            e.printStackTrace();
            Assert.fail();
        }
    }

    @Test
    public void testerProduit() {
        try {
            Vecteur v2 = new Vecteur(2, 2);
            Vecteur v = new Vecteur(2, 3);
            v.setColonne(!v.isColonne());
            Double res = vecteursService.produit(v, v2);
            Assert.assertEquals(Double.valueOf(10), res);
        }
        catch (StatistiqueException e) {
            e.printStackTrace();
            Assert.fail();
        }
    }
}
@ActiveProfiles("test")
public class GenerationRegressionServiceTest extends AbstractTestBdd {

    @Autowired
    private GenerationRegressionService generationRegressionService;

    @Test
    public void testerGenerationRegression() {
        try {
            ResultatRegression res = generationRegressionService.genererRegression();
            Assert.assertEquals(2.87d, res.getB0(), 0.1d);
        }
        catch (StatistiqueException e) {
            e.printStackTrace();
            Assert.fail();
        }
    }
}
@ActiveProfiles("test")
public class ReleveToNuagePointMapperTest extends AbstractTest {

    @Autowired
    private ReleveToNuagePointMapper releveToNuagePointMapper;

    @Test
    public void testerMapping() {
        List<Concentration> concentrations = new ArrayList<>();
        Releve r = new Releve();
        Minerai ni = new Minerai();
        ni.setId("ni");
        Minerai co = new Minerai();
        co.setId("co");
        r.setId(1);
        Concentration c1 = new Concentration();
        c1.setReleve(r);
        c1.setMinerai(ni);
        c1.setValeur(2d);
        Concentration c2 = new Concentration();
        c2.setReleve(r);
        c2.setMinerai(ni);
        c2.setValeur(3d);
        concentrations.add(c1);
        concentrations.add(c2);
        NuagePoints n = releveToNuagePointMapper.releveToNuagePoint(concentrations);
        Assert.assertEquals(1, n.getX().size());
    }
}
@ActiveProfiles("test")
public class ReleveRepositoryTest extends AbstractTestBdd {

    @Autowired
    private ConcentrationsRepository relevesRepository;

    @Test
    public void testerReleveRepository() {
        try {
            relevesRepository.findStatistiques();
        }
        catch (Exception e) {
            e.printStackTrace();
            Assert.fail();
        }
    }
}

Bilan

Il n'y a pas de solution miracle qui permette de tester entièrement et sans risque de régression les applications, il est cependant possible de réaliser un travail de test intéressant et utile.

Dans notre cas, la couverture de test est vraiment faible (22,7%), et pourtant, ces tests sont tout à fait utiles. Il ne faut pas à tout prix chercher la couverture de test, mais chercher leur utilité.

Couverture de test