Présentation

L'objectif de cet exercice est de créer un programme permettant de transformer un fichier MarkDown en un fichier html

Nous allons mettre en place des tests et utiliser le développement guidé par les tests. A chaque étape, nous allons :

  • Créer la scructure des services
  • Développer les jeux de tests
  • Développer les services pour faire fonctionner les tests

Accès aux fichiers

L'objectif de ce premier traitement est de créer un service qui permet de lire et d'écrire des fichiers texte

Créer un service AccesFichiers ayant les caractéristiques suivantes :

  • Une méthode List<String> lireFichier(File f) permettant de lire un fichier texte et de retourner une liste de ses lignes
  • Une méthode ecrireFichier(File f, List<String> contenu) permettant d'écrire un fichier grâce à la liste des ses lignes
  • Ajouter des logs dans les lectures et écritures afin de tracer les différents opératione effectuées

Pensez à utiliser les méthodes de la classe Files : readAllLines et write

Etape 1 : Créer la structure du service

public class AccesFichiers {
	private static final AccesFichiers instance = new AccesFichiers();

	public List<String> lireFichier(File f) throws IOException {
		return null;
	}

	public void ecrireFichier(File f, List<String> contenu) throws IOException {
	}

	public static AccesFichiers getInstance() {
		return instance;
	}
}

Etape 2 : Créer un test et le faire échouer

public class LectureFichierTest {
	@BeforeAll
	public static void init() {
		Configurator.initialize(new DefaultConfiguration());
		Configurator.setRootLevel(Level.INFO);
	}
	
	@AfterAll
	public static void end() {
		File f = new File("src/test/resources/tmp/fichier1.txt");
		if (f.exists()) {
			f.delete();
		}
	}

	@Test
	public void testerLecture() {
		try {
			List<String> l = AccesFichiers.getInstance().lireFichier(new File("src/test/resources/donnees/services/fichiers/LectureFichierTest/exemple.md"));
			Assertions.assertNotNull(l);
			Assertions.assertEquals(11, l.size());
		}
		catch (IOException e) {
			e.printStackTrace();
			Assertions.fail();
		}
	}
	
	@Test
	public void testerEcriture() {
		try {
			List<String> lignes = Stream.of("ligne1", "ligne2").collect(Collectors.toList());
			File f = new File("src/test/resources/tmp/fichier1.txt");
			AccesFichiers.getInstance().ecrireFichier(f, lignes);
			List<String> lignesLues = Files.readAllLines(f.toPath());
			Assertions.assertEquals(2, lignesLues.size());
		}
		catch (IOException e) {
			e.printStackTrace();
			Assertions.fail();
		}
	}
}

Etape 3 : Ecrire le code du service AccesFichiers

public class AccesFichiers {
	private static final Logger LOG = LogManager.getLogger(AccesFichiers.class);
	private static final AccesFichiers instance = new AccesFichiers();
	
	private AccesFichiers() {
		super();
	}

	public List<String> lireFichier(File f) throws IOException {
		LOG.info("Lecture du fichier " + f.toString());
		return Files.readAllLines(f.toPath());
	}

	public void ecrireFichier(File f, List<String> contenu) throws IOException {
		LOG.info("Ecriture du fichier " + f.toString());
		Files.write(f.toPath(), contenu);
	}

	public static AccesFichiers getInstance() {
		return instance;
	}
}

Etape 4 : Vérifier que le test passe

C'est bon

Etape 5 : Refactorer éventuellement

Vérifier en particulier que les log soient bien affichées


Titres et paragraphes

L'objectif de ce premier traitement est de créer à partir d'un fichier donné *.md un fichier .html dans lequel les titres et les paragraphes sont correctement remplacés

  • Chaque saut de ligne double du fichier en entrée correspond à un changement de paragraphe
  • Le caractère # en début de ligne correspond à un titre de niveau 1
  • Les caractère ## en début de ligne correspondent à un titre de niveau 2

Jeu de test :

Fichier en entrée :

# Titre de premier niveau

premier paragraphe

# Titre de premier niveau

## Titre de second niveau

Paragraphe 1

Paragraphe 2

Fichier attendu :

<h1>Titre de premier niveau</h1>
<p>premier paragraphe</p>
<h1>Titre de premier niveau</h1>
<h2>Titre de second niveau</h2>
<p>Paragraphe 1</p>
<p>Paragraphe 2</p>

Pour concevoir ce programme, nous allons utiliser un filtre par opération. Un filtre prend en entrée un liste et retourne une liste, nous pourrons ainsi les chaîner dans nos traitements

Etape 1 : Créer la structure des services

public interface Filtre {
	List<String> traiter(List<String> entree);
}
public class FiltreTitre implements Filtre {
	private static final FiltreTitre instance = new FiltreTitre();

	private FiltreTitre() {
		super();
	}

	@Override
	public List<String> traiter(List<String> entree) {
		return null;
    }

	public static FiltreTitre getInstance() {
		return instance;
	}
}

public class FiltreParagraphe implements Filtre {
	private static final FiltreParagraphe instance = new FiltreParagraphe();

	private FiltreParagraphe() {
		super();
	}

	@Override
	public List<String> traiter(List<String> entree) {
		return null;
	}

	public static FiltreParagraphe getInstance() {
		return instance;
	}
}

public class TraductionFichiers {
    private static final TraductionFichiers instance = new TraductionFichiers();

	private TraductionFichiers() {
		super();
	}

	public List<String> traduireFichier(File entree, File sortie) {
	}

	public static TraductionFichiers getInstance() {
		return instance;
	}
}

Etape 2.1 : Créer le test du filtre sur les titres et le faire échouer

public class FiltreTitreTest {
	@BeforeAll
	public static void init() {
		Configurator.initialize(new DefaultConfiguration());
		Configurator.setRootLevel(Level.INFO);
	}

	@Test
	public void testerFiltreTitre() {
		List<String> lignes = Stream.of("# Titre", "## Titre 2", "Paragraphe 1", "", "Paragraphe 2").collect(Collectors.toList());
		List<String> res = FiltreTitre.getInstance().traiter(lignes);
		Assertions.assertEquals(5, res.size());
		Assertions.assertEquals("<h1>Titre</h1>", res.get(0));
		Assertions.assertEquals("<h2>Titre 2</h2>", res.get(1));
	}
}

Etape 3.1 : Ecrire le code permettant de faire passer le test

public class FiltreTitre implements Filtre {
	private static final FiltreTitre instance = new FiltreTitre();

	private FiltreTitre() {
		super();
	}

	@Override
	public List<String> traiter(List<String> entree) {
		List<String> res = new ArrayList<>();
		for (String s : entree) {
			if (s.startsWith("# ")) {
				res.add("<h1>" + s.substring(2) + "</h1>");
			}
			else if (s.startsWith("## ")) {
				res.add("<h2>" + s.substring(3) + "</h2>");
			}
			else {
				res.add(s);
			}
		}
		return res;
	}

	public static FiltreTitre getInstance() {
		return instance;
	}
}

Etape 2.2 : Créer le test du filtre sur les paragraphes et le faire échouer

public class FiltreParapgrapheTest {
	@BeforeAll
	public static void init() {
		Configurator.initialize(new DefaultConfiguration());
		Configurator.setRootLevel(Level.INFO);
	}

	@Test
	public void testerFiltreParagraphe() {
		List<String> lignes = Stream.of("# Titre", "Paragraphe 1", "", "Paragraphe 2").collect(Collectors.toList());
		List<String> res = FiltreParagraphe.getInstance().traiter(lignes);
		Assertions.assertEquals(3, res.size());
		Assertions.assertEquals("<p>Paragraphe 1</p>", res.get(1));
	}
}

Etape 3.2 : Ecrire le code permettant de faire passer le test

public class FiltreParagraphe implements Filtre {
	private static final FiltreParagraphe instance = new FiltreParagraphe();

	private FiltreParagraphe() {
		super();
	}

	@Override
	public List<String> traiter(List<String> entree) {
		List<String> res = new ArrayList<>();
		for (int i = 0; i < entree.size(); i++) {
			String s = entree.get(i);
			if ((s == null || s.isEmpty())) {
				if (!res.get(res.size() - 1).startsWith("<")) {
					res.set(res.size()-1, "<p>" + res.get(res.size()-1) + "</p>");
				}
			}
			else {
				res.add(s);
			}
		}
		if(!res.get(res.size()-1).startsWith("<")) {
			res.set(res.size()-1, "<p>" + res.get(res.size()-1) + "</p>");
		}
		return res;
	}

	public static FiltreParagraphe getInstance() {
		return instance;
	}
}

Etape 2.3 : Créer le test du traitement et le faire échouer

public class TraductionFichiersTest {
	private static File sortie = new File("src/test/resources/tmp/sortie.html");

	@BeforeAll
	public static void init() {
		Configurator.initialize(new DefaultConfiguration());
		Configurator.setRootLevel(Level.INFO);
	}

	@AfterAll
	public static void end() {
		if (sortie.exists()) {
			sortie.delete();
		}
	}

	@Test
	public void testerTraitement() {
		File entree = new File("src/test/resources/donnees/services/traitements/TraductionFichiersTest/exemple.md");
		try {
			TraductionFichiers.getInstance().traduireFichier(entree, sortie);
			Assertions.assertTrue(sortie.exists());
			List<String> lignes = Files.readAllLines(sortie.toPath());
			Assertions.assertNotNull(lignes);
			Assertions.assertEquals(6, lignes.size());
			List<String> attendu = Stream.of(
				"<h1>Titre de premier niveau</h1>", 
				"<p>premier paragraphe</p>", 
				"<h1>Titre de premier niveau</h1>", 
				"<h2>Titre de second niveau</h2>", 
				"<p>Paragraphe 1</p>", 
				"<p>Paragraphe 2</p>").collect(Collectors.toList());
			Assertions.assertEquals(attendu, lignes);
		}
		catch (IOException e) {
			e.printStackTrace();
			Assertions.fail();
		}
	}
}

Le fichier exemple.md contient le texte fourni dans la description du travail à faire

Etape 3.3 : Ecrire le code permettant de faire passer le test

public class TraductionFichiers {
	private static final TraductionFichiers instance = new TraductionFichiers();

	private TraductionFichiers() {
		super();
	}

	public List<String> traduireFichier(File entree, File sortie) throws IOException {
		List<String> lignes = AccesFichiers.getInstance().lireFichier(entree);
		lignes = FiltreTitre.getInstance().traiter(lignes);
		lignes = FiltreParagraphe.getInstance().traiter(lignes);
		AccesFichiers.getInstance().ecrireFichier(sortie, lignes);
		return lignes;
	}

	public static TraductionFichiers getInstance() {
		return instance;
	}
}

Création des outils

A cet instant, nous comprennons que nous allons avoir besoin d'outils permettant de gérer les fichiers, leur écriture, leur cycle de vie et qu'il est préférable de centraliser les développements associés à ces opérations. Nous avons alors deux solutions :

  • Créer des services propres aux tests dans src/test/java : pour les petits services et les projets n'ayant pas de gros besoins de services de test (sans mock massifs par exemple). Dans ce cas, ces services ne font pas l'objet de tests unitaires
  • Créer un projet à part : services-test et l'utiliser dans les autres projet en l'incluant avec le scope test. Dans ce cas, le projet service-test a son propre jeu de test unitaire (serions-nous entrés dans une boucle infinie ?)