From c330379555b733e3ca03c16bae98e7e3d2c4d003 Mon Sep 17 00:00:00 2001
From: Thomas Clavier <tom@tcweb.org>
Date: Thu, 8 Feb 2024 22:05:03 +0100
Subject: [PATCH] avant le cours

---
 03-tests.md                                   | 969 ++++++++++++++++++
 README.md                                     |   5 +-
 sample-code/pom.xml                           |  60 ++
 .../java/fr/univlille/iut/info/r402/Cell.java |   4 +
 .../univlille/iut/info/r402/CellContent.java  |   5 +
 .../fr/univlille/iut/info/r402/Ocean.java     |  14 +
 sample-code/src/test/java/OceanTest.java      |  16 +
 7 files changed, 1071 insertions(+), 2 deletions(-)
 create mode 100644 03-tests.md
 create mode 100644 sample-code/pom.xml
 create mode 100644 sample-code/src/main/java/fr/univlille/iut/info/r402/Cell.java
 create mode 100644 sample-code/src/main/java/fr/univlille/iut/info/r402/CellContent.java
 create mode 100644 sample-code/src/main/java/fr/univlille/iut/info/r402/Ocean.java
 create mode 100644 sample-code/src/test/java/OceanTest.java

diff --git a/03-tests.md b/03-tests.md
new file mode 100644
index 0000000..ce31b55
--- /dev/null
+++ b/03-tests.md
@@ -0,0 +1,969 @@
+---
+author: Thomas Clavier
+title: Il est beau mon test
+---
+
+# Programme parfait ?
+
+C'est quoi un programme parfait ?
+
+# Selon XP
+
+* DRY (Don't Repeat Yourself) 
+* Simple
+* Du code lisible
+* Tous les tests passent
+
+# DRY
+
+Une information dupliquée c'est le meilleur moyen pour avoir une désynchronisation plus tard.
+
+## Information dupliquée ?
+Pour vous, dans quels cas a-t-on une information dupliquée ?
+
+. . .
+
+* Copier / Coller
+* Fichier de configuration / Code
+
+## Resource vs Java
+
+```java
+FileReader readerUtilisateur = new FileReader( getClass()
+    .getClassLoader()
+    .getResource("utilisateurs.json").getFile() );
+List<Utilisateur> utilisateurs = JsonbBuilder.create()
+    .fromJson(readerUtilisateur);
+UtilisateurDao utilisateurDao = BDDFactory.buildDao(UtilisateurDao.class);
+if (! tableExist("utilisateurs") {
+    utilisateurDao.createTable();
+    for ( Utilisateur utilisateur: utilisateurs) {
+        utilisateurDao.insert(utilisateur);
+    }
+}
+```
+```json
+[
+    {"id": "1", "role": "admin", "nomPrenom": "Juliette", "motDePasse": "1234"},
+    {"id": "2", "role": "presta", "nomPrenom": "Toto", "motDePasse": "1234"},
+    {"id": "3", "role": "client", "nomPrenom": "Richard", "motDePasse": "1234"}
+]
+```
+
+## Resource vs Java
+
+```java
+UtilisateurDao utilisateurDao = BDDFactory.buildDao(UtilisateurDao.class);
+if (! tableExist("utilisateurs") {
+    utilisateurDao.createTable();
+    utilisateurDao.insert(new Utilisateur(ADMIN, "Juliette", "1234"));
+    utilisateurDao.insert(new Utilisateur(PRESTA, "Toto", "1234"));
+    utilisateurDao.insert(new Utilisateur(CLIENT, "Richard", "1234"));
+}
+```
+
+## Complexité accidentelle
+
+C'est la complexité introduite dans des programmes informatiques non en raison de la complexité du problème, mais de manière accidentelle en raison des choix de développement.
+
+* Fichier de configuration vs code : oui si vos utilisateurs ne savent/peuvent pas éditer le code
+* Fichier de données vs code : oui si c'est plus facile à mettre à jour (masse, données venant d'un autre système, etc.)
+
+# Simple
+
+* KISS (Keep It Simple Stupide)
+* YAGNI (You Aren't Gona Need It)
+
+## KISS <small>Simplicité technique</small>
+
+```java
+public static double average(List<Double> numbers) {
+    Double sum = 0.0;
+    for (Double number : numbers) {
+        sum += number;
+    }
+    if (numbers.size() > 0)
+        return sum / numbers.size();
+    return 0.0;
+}
+```
+
+```java
+public static double average(List<Double> numbers) {
+    return numbers.stream()
+        .mapToDouble(Double::doubleValue)
+        .average()
+        .orElse(0.0);
+}
+```
+
+## SOLID
+
+* *S*ingle responsability 
+* *O*pen/closed
+* *L*iskov substitution
+* *I*nterface segregation
+* *D*ependency inversion
+
+## Loi de Demeter
+
+```java
+public class Ocean {
+    private final Cell[][] internalRepresentation;
+
+    public Cell[][] getInternalRepresentation() {
+        return internalRepresentation;
+    }
+}
+```
+
+```java
+Ocean pacific = new Ocean(3,3);
+pacific.getInternalRepresentation()[0][0] = new Cell(Fish);
+```
+. . .
+
+vs
+
+```java
+public class Ocean {
+    public Ocean putCellContentAt(CellContent content, int line, int col) {
+        internalRepresentation[line, col] = new Cell(content);
+        return this;
+    }
+}
+```
+
+```java
+Ocean pacific = Ocean.of(3,3).putCellContentAt(Fish,0,0);
+```
+
+
+
+## YAGNI <small>Simplicité fonctionnelle</small>
+
+Et si un jour il faut configurer une semaine avec plus que 7 jours ?
+
+```json
+[
+    {"id": "1", "name": "lundi"},
+    {"id": "2", "name": "mardi"},
+    {"id": "3", "name": "mercredi"},
+    {"id": "4", "name": "jeudi"},
+    {"id": "5", "name": "vendredi"},
+    {"id": "6", "name": "samedi"},
+    {"id": "7", "name": "dimanche"},
+]
+```
+
+. . . 
+
+C'est arrivé 1 fois depuis la création du calendrier grégorien en 525 ! Même si la semaine de 7 jours existe depuis Babylone en -1595.
+
+# Lisible
+
+C'est quoi un code lisible ?
+
+## Lisible ?
+
+```java
+public void init() {
+    // Paramétrage
+    Properties properties = new Properties();
+    properties.setProperty("foreign_keys", "true");
+
+    // Connection à la base de données
+    Jdbi jdbi = Jdbi.create(basePath + "data.db", properties)
+            .installPlugin(new SQLitePlugin())
+            .installPlugin(new SqlObjectPlugin())
+            .registerArgument(new UUIDArgumentFactory());
+
+    // Initialisation avec un admin
+    if (!tableExist("users")) {
+        UserDao userDao = jdbi.onDemand(UserDao.class);
+        userDao.createUsersTable();
+        userDao.insert(new User("Admin", "AdminPassword"));
+    }
+}
+```
+
+## No comment
+
+```java
+public void init() {
+    Jdbi jdbi = getDatabaseConnection();
+    initializeUsersTableWithDefaultAdmin(jdbi);
+}
+```
+
+## No comment 
+
+```java
+public void init() {
+    Properties properties = new Properties();
+    properties.setProperty("foreign_keys", "true");
+    Jdbi jdbi = Jdbi.create(basePath + "data.db", properties)
+            .installPlugin(new SQLitePlugin())
+            .installPlugin(new SqlObjectPlugin())
+            .registerArgument(new UUIDArgumentFactory());
+    if (!tableExist("users")) {
+        UserDao userDao = jdbi.onDemand(UserDao.class);
+        userDao.createUsersTable();
+        userDao.insert(new User("Admin", "AdminPassword"));
+    }
+}
+```
+## Mémoire de travail
+
+## Mémoire de travail
+
+![](../includes/2pts.png)
+
+
+## Mémoire de travail
+
+![](../includes/6pts.png)
+
+## Mémoire de travail
+
+![](../includes/10pts.png)
+
+## Mémoire de travail
+
+![](../includes/20pts.png){height=400px}
+
+. . .
+
+* 2 informations : 4 groupes de 5
+* et 1 opération :  4 × 5 = 20
+
+## Mémoire de travail
+
+* 7 +/- 2 éléments (instructions et/ou informations)
+* L'entrainement permet de s'approprier des motifs
+
+cf. travaux de Nelson Cowan, Alan Baddeley, Graham Hitch et neurosciences
+
+. . .
+
+```python
+print([x for x in ["apple", "banana", "cherry", "kiwi", "mango"] if "a" in x])
+```
+
+## Indirection Implicite
+
+```java
+@Before
+public void setEnvUp() { utilisateurdao.createTable(); }
+
+@After
+public void tearEnvDown() throws Exception { utilisateurdao.dropTable(); }
+
+...
+
+@Test
+public void testCreateUtilisateur() {
+    Utilisateur utilisateur = sampleUtilisateur();
+    Response response = target("/utilisateurs").request()
+        .post(Entity.json(utilisateur));
+    Utilisateur entityFromDB = utilisateurdao.load(utilisateur.getName())
+    assertEquals(entityFromDB, utilisateur);
+}
+```
+
+## Indirection Implicite
+
+```java
+@Test
+public void testCreateUtilisateur() {
+    resetTableUtilisateur();
+    Utilisateur utilisateur = sampleUtilisateur();
+    Response response = target("/utilisateurs").request()
+        .post(Entity.json(utilisateur));
+    Utilisateur entityFromDB = utilisateurdao.load(utilisateur.getName())
+    assertEquals(entityFromDB, utilisateur);
+}
+```
+
+## Lisibilité du code
+
+* Moins de 8 lignes.
+* Le moins d'opération mentale possible pour le lecteur.
+* Entrainements.
+
+# Tests verts
+
+## Du code testable
+
+* Comment faire du code testable ? \
+    <i class="fa fa-arrow-right" aria-hidden="true"></i> cf. [Tout est testable](../02-mock/)
+* Comment écrire de bons tests ? \
+    <i class="fa fa-arrow-right" aria-hidden="true"></i> ***Une partie de la réponse aujourd'hui.***
+
+
+## Testable ?
+
+```java
+public long TicksElapsedFrom(int year) {
+    LocalDateTime now = LocalDateTime.now();
+    LocalDateTime from = LocalDateTime.of(year, 1, 1, 0, 0);
+    return Duration.between(from, now).getSeconds();
+}
+```
+. . .
+
+**#NonRepeatable**
+
+. . .
+
+```java
+public long TicksElapsedFrom(int year, LocalDateTime now) {
+    LocalDateTime from = LocalDateTime.of(year, 1, 1, 0, 0);
+    return Duration.between(from, now).getSeconds();
+}
+```
+## Testable ?
+
+```java
+public class User {
+    private String name;
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDisplayName(String prefix) {
+        name = prefix + " " + name;
+        return name;
+    }
+}
+```
+
+. . .
+
+**#SideEffect**
+
+## Testable ?
+
+```java
+public void triche() {
+    if (this.tricheActivee) {
+        for (Bateau bateau : this.bateaux) {
+            System.out.println(bateau);
+        }
+    }
+}
+```
+
+. . .
+
+**#InputOutput**
+
+. . .
+
+```java
+public String triche() {
+    String output = "";
+    if (this.tricheActivee) {
+        for (Bateau bateau : this.bateaux) {
+            output += bateau + "\n";
+        }
+    }
+    return output;
+}
+```
+
+
+## Testable ?
+
+```java
+public String statement() {
+    double totalAmount = 0;
+    String result = "Rental Record for " + getName() + "\n";
+    for (Rental each : this.rentals) {
+        double thisAmount = each.determineAmount();
+        result += "\t" + each.getMovie().getTitle() + "\t" + thisAmount + "\n";
+        totalAmount += thisAmount;
+    }
+    result += "Amount owed is " + totalAmount + "\n";
+    return result;
+}
+```
+
+. . .
+
+**#NoSingleResponsability**
+
+## Testable = Code propres
+
+* Fonction pure : 
+    * Sa valeur de retour est la même pour les mêmes arguments
+    * Son évaluation n'a pas d'effets de bord
+* Limiter les mutations
+* Des fonctions ou objets dédiés à l'I/O 
+* Une seul responsabilité (cf. SOLID)
+
+## Bon ou mauvais test ?
+
+```java
+@Test
+void testViewIsGettingUpdated() {
+    model.addSeller(new MarketSeller("John Doe", 500));
+    assertTrue(view.getMarketSellers().isEmpty());
+    model.attach(view);
+    model.addSeller(new MarketSeller("John Dodo", 500));
+    assertFalse(view.getMarketSellers().isEmpty());
+    assertEquals(view.getMarketSellers().size(), 2);
+    assertEquals(view.getMarketSellers().get(0).getName(), "John Doe");
+    assertEquals(view.getMarketSellers().get(1).getName(), "John Dodo");
+}
+```
+
+## Qualité d'un test
+
+* Il est rapide (quelques secondes max)
+* Je peux le jouer 2 fois de suite il est encore vraie, même à plusieurs jours d'intervalles. 
+* Il est explicite, les destinataires de ce test peuvent le lire et le comprendre.
+* Il ne test qu'une règle métier.
+* Il est indépendant des autres tests : 
+    * je peux jouer les tests dans le désordre
+    * je peux jouer tous les tests en même temps.
+* Je peux restructurer mon code sans avoir à le changer
+
+## Bon ou mauvais test ?
+
+```java
+@Test
+public void stock_exchanges_test(){
+    Bourse bourse = new Bourse(800.0);
+    assertEquals(800.0, bourse.getAmount());
+}
+```
+
+. . .
+
+**#NonExplicit**
+
+## Bon ou mauvais test ?
+
+```java
+@Test
+void should_compute_interval_between_2_dates_in_good_order() {
+    LocalDate startDate = LocalDate.of(2015, 3, 2);
+    LocalDate endDate = LocalDate.of(2015, 3, 3);
+    Long days = IntervalDomain.between(startDate, endDate);
+    assertEquals(1, days);
+}
+```
+
+. . .
+
+**#Good**
+
+## Bon ou mauvais test ?
+
+```java
+@Test
+void should_raise_an_error_when_endDate_is_before_startDate() {
+    assertThrows(InvalidParameterException.class, () -> {
+            LocalDate startDate = LocalDate.of(2025, 3, 2);
+            LocalDate endDate = LocalDate.of(2020, 4, 3);
+            IntervalDomain.between(startDate, endDate);
+        });
+}
+```
+
+. . .
+
+**#Good**
+
+## Bon ou mauvais test ?
+
+```java
+@Test   
+public void testSurpopulate(){
+    grille = new Grid();
+    grille.setAlive(1,1);
+    grille.setAlive(1,2);
+    grille.setAlive(0,1);
+    grille.setAlive(0,0);
+    grille.setAlive(2,2);
+    grille.playTurn();
+    assertFalse(grille.getCell(1,1).isAlive());
+}
+```
+
+. . .
+
+**#NonExplicit #Franglais**
+
+## Bon ou mauvais test ?
+
+```java
+@Test
+void un_depot_doit_incrementer_le_solde() {
+    CompteEnBanque compteDeBob = CompteEnBanque.creer("Bob");
+    compteDeBob.depot(12.2);
+    Assertions.assertEquals(12.2, compteDeBob.solde());
+}
+```
+
+. . .
+
+**#WarningRefactoring #WarningImmutability**
+
+. . .
+
+```java
+@Test
+void un_depot_doit_incrementer_le_solde() {
+    GivenAnAccount()
+        .with(euro(12.5))
+        .whenIDeposit(euro(10))
+        .thenTheSoldeShouldBe(euro(22.5));
+}
+```
+
+## Bon ou mauvais tests ?
+
+```java
+@Test
+public void marketPlaceTest() {
+    MarketPlace marketPlace = new MarketPlace();
+    Member alice = new Member("Alice", 1000);
+    Member bob = new Member("Bob", 1500);
+    marketPlace.addMember(alice);
+    assertEquals(marketPlace.toString(), "Member [name=Alice, money=1000]\n" );
+    marketPlace.addMember(bob);
+    assertEquals(marketPlace.toString(), 
+            "Member [name=Alice, money=1000]\n" +
+            "Member [name=Bob, money=1500]\n" );
+    marketPlace.remove(alice);
+    assertEquals(marketPlace.toString(),"Member [name=Bob, money=1500]\n" );
+}
+```
+. . .
+
+**#NonExplicit #Multiples #TropLong #PascalCase**
+
+## Bon ou mauvais tests ?
+
+```java
+@Test
+public void ajouterUnProduitTest() {
+    myBourse.ajouterProduit(p1);
+    assertTrue(myBourse.produitDisponible(p1));
+    assertFalse(myBourse.produitDisponible(p2));
+}
+
+@Test
+public void ajouterProduitsTest() {
+    myBourse.ajouterProduits(p2, p3);
+    assertTrue(myBourse.produitDisponible(p1));
+    assertTrue(myBourse.produitDisponible(p2));
+    assertTrue(myBourse.produitDisponible(p3));
+}
+```
+. . .
+
+**#WarningBeforeEach #TestsDependencies #NonExplicit**
+
+## Bon ou mauvais tests ?
+
+```java
+@Test
+void should_instance_member() {
+    Member alice = new Member("Alice",1000);
+    assertTrue(alice != null);
+}
+```
+. . .
+
+**#NonExplicit #ToujoursVrai**
+
+## Bon ou mauvais tests ?
+
+```java
+@Test
+void testConstructor(){
+    assertEquals(10, this.jdv.getTerrain().length);
+    assertEquals(10, this.jdv.getTerrain()[0].length);
+}
+```
+. . .
+
+**#InternalLeak #NotDemeter**
+
+## Bon ou mauvais tests ?
+
+```typescript
+describe("Game", () => {
+    it("should add food when no stock", () => {
+        const game = new Game(100, 100)
+        game.players = []
+        game.foods = []
+        game.addFood(new Food())
+        expect(game.foods.length).toEqual(1)
+    })
+})
+```
+. . .
+
+**#WarningRefactoring #WarningImmutability #NoSingleResponsability**
+
+## Bon ou mauvais test ?
+
+```typescript
+describe("Game", () => {
+    it("should add food when no stock", () => {
+        expect(addFood([]).length).toEqual(1)
+    })
+    it("should add food when no stock", () => {
+        expect(FoodStock.of([]).add().length).toEqual(1)
+    })
+})
+```
+. . .
+
+**#Good**
+
+
+## Bon ou mauvais test ?
+
+```java
+public BookingResponse book() {
+    int	place = new Random().nextInt(0,seats.length);
+    return new BookingResponse(BookingResponseStatus.OK,seats[place].getNumber());
+}
+```
+
+. . .
+
+**#NonRepeatable #NoSingleResponsability**
+
+## Bon ou mauvais test ?
+
+```java
+@Test
+public void shouldReturnTrue() {
+    SecretSanta s = new SecretSanta();
+    assertTrue(s.addSanta("toto"));
+}
+```
+
+. . .
+
+**#NonExplicit #PascalCase #MissingException**
+
+## Bon ou mauvais test ?
+
+:::: {.columns}
+::: {.column width="50%"}
+```java
+public class TirageTest {
+    Tirage tirage;
+    String p1;
+    String p2;
+
+    @BeforeEach
+    public void initialize()
+    {
+        tirage = new Tirage();
+        p1 = "Théo";
+        p2 = "Nathan";
+    }
+
+    @Test
+    public void test_add()
+    {
+        assertTrue(tirage.add(p1));
+        assertTrue(tirage.add(p2));
+```
+:::
+::: {.column width="50%"}
+```java
+        assertFalse(tirage.add(p1));
+    }
+
+    @Test
+    public void test_affectation()
+    {
+        tirage.add(p1);
+        tirage.add(p2);
+        tirage.affectation();
+        assertTrue(
+          tirage.affectation().size() 
+          ==
+          tirage.size());
+    }
+}
+```
+:::
+::::
+
+. . . 
+
+**#AssertEquals #TestsDependencies**
+
+## Bon ou mauvais test ?
+
+```java
+public class PersonneTest {
+        Personne p1;
+        Personne p2;
+        Personne p3;
+
+        @BeforeEach
+        public void setUp() {
+            p1 = new Personne("Robert" );
+            p2 = new Personne("Jean");
+            p3 = new Personne("Pierre");
+        }
+
+        @Test
+        public void test_set_gifter(){
+            p1.setGifter(p2);
+            assertTrue(p1.getGifter().equals(p2));
+        }
+}
+```
+
+. . .
+
+**#TooComplicated**
+
+## Bon ou mauvais test ?
+
+```java
+@Test
+public void testBookProperty() {
+    Train t = new Train();
+    BookingResponse expected = t.getSeats()[4].getBr();
+    assertEquals(expected, t.book(SeatProperty.Duo));
+}
+```
+
+. . .
+
+**#NonExplicite**
+
+## Bon ou mauvais test ?
+
+```java
+void should_return_a_seat_with_seat_property(){
+    Train train = new Train();
+    assertEquals(26, train.book(Duo).getSeat());
+    assertEquals(17, train.book(Square).getSeat());
+}
+```
+
+. . .
+
+**#TooTechnical**
+
+## Bon ou mauvais test ?
+
+```java
+Train t = new Train();
+
+@Test
+void bookshould_return_place() {
+    BookingResponse response = new BookingResponse(BookingResponseStatus.OK, 17);
+    assertEquals(response,t.book());
+}
+
+@Test
+void retourne_une_place_en_respectant_propriete() {
+    BookingResponse response = new BookingResponse(BookingResponseStatus.OK, 26);
+    assertEquals(response, t.book(SeatProperty.Duo));
+}
+
+@Test
+void retourne_place_au_plus_proche() {
+    assertEquals(Seat.of(17, Square, Window), t.book(14));
+}
+```
+
+. . .
+
+**#NotExplicit #TestsDependencies**
+
+## Bon ou mauvais test ?
+
+```gherkin
+Scenario: On retourne une place respectant la propriété en paramètre
+  Given un train
+  When on réserve une place avec une propriété SeatProperty.DUO
+  Then retourne une place avec la propriété SeatProperty.DUO
+```
+
+. . .
+
+**#WarningRefactoring #TechnicalLeak**
+
+
+# Anatomie d'un test
+
+## Les étapes d'un test
+
+* *Given* : un ensemble d'éléments observables permettant de décrire avec précision l'état de départ.
+* *When* : **une** action
+* *Then* : L'ensemble des éléments observables du système après l'action permettant de décrire avec précision l'état d'arrivée.
+
+## 3 étapes
+
+![](../includes/given-when-then.png)
+
+## Given
+
+:::: {.columns}
+::: {.column width="50%"}
+Étant donné un échiquier avec :
+
+* le roi blanc en A5
+* le roi noir en A7
+* un pion blanc en B7
+* une tour blanche en C7
+* une tour noir en A8
+
+Et c'est au blanc de jouer.
+:::
+::: {.column width="50%"}
+![](../includes/given.png)
+:::
+::::
+
+## When
+
+Quand le joueur blanc déplace son pion de B7 à B8 
+et demande un cavalier noir.
+
+## Then
+
+:::: {.columns}
+::: {.column width="50%"}
+Alors Les blancs gagnent ! 
+
+Échec et mat !
+
+:::
+::: {.column width="50%"}
+![](../includes/then.png)
+:::
+::::
+
+## Junit
+
+```java
+@Test
+void should_promote_a_pawn_in_an_other_color () {
+    Chessboard chessboard = new Chessboard()
+        .with(King.of(White), "A5")
+        .with(King.of(Black), "A7")
+        .with(Pawn.of(White), "B7")
+        .with(Tower.of(White), "C7")
+        .with(Tower.of(Black), "A8")
+    chessboard.move("B7", "B8").request(Knight.of(Black))
+    assertEquals(chessboard.get("B8"), Knight.of(Black))
+}
+```
+
+. . .
+
+```java
+@Test
+void should_promote_a_pawn_in_an_other_color () {
+    Chessboard chessboard = new Chessboard().with(Pawn.of(White), "B7")
+    chessboard.move("B7", "B8").request(Knight.of(Black))
+    assertEquals(chessboard.get("B8"), Knight.of(Black))
+}
+```
+
+## DSL
+
+```java
+@Test
+void should_promote_a_pawn_in_an_other_color () {
+    GivenAChessboard().with(Pawn.of(White), "B7")
+        .whenMoveFrom("B7").to("B8").andRequestA(Black(Knight()))
+        .thenAt("B8").shouldFoundA(Black(Knight))
+}
+```
+
+## Gherkin
+
+```gherkin
+Scenario: Promote a pawn in an other color
+   Given a chessboard with a "white pawn" in "B7"
+   When "white" player move from "B7" to "B8"
+    And request a "black knight"
+   Then in "B8" we can see a "black knight"
+```
+
+# Types vs Tests
+
+## Tester le métier
+```python
+class Wallet:
+    def value(self, currency: Currency, rate: RateProvider) -> Amount:
+        ...
+```
+```java
+public class Wallet {
+    public Amount value(Currency currency, RateProvider rate) {
+        ...
+```
+```typescript
+export class Wallet {
+    public value(currency: Currency, rate: RateProvider): Amount {
+        ...
+```
+
+## Tester aussi les paramètres
+```python
+class Wallet:
+    def value(self, currency, rate):
+        ...
+```
+```python
+def test_should_throw_bad_parameter_when_not_currency():
+    ...
+
+def test_should_throw_bad_parameter_when_not_rateProvider():
+    ...
+```
+
+# Qualités d'un test
+
+* Il est rapide (quelques secondes max)
+* Je peux le jouer 2 fois de suite il est encore vraie, même à plusieurs jours d'intervalles. 
+* Il est explicite, les destinataires de ce test peuvent le lire et le comprendre.
+* Il ne test qu'une règle.
+* Il est indépendant des autres tests : 
+    * je peux jouer les tests dans le désordre
+    * je peux jouer tous les tests en même temps.
+* Je peux restructurer mon code sans avoir à le changer
+
+## Qualité d'un exemple
+
+Pour les tests qui doivent être lut par les biz, les exemples doivent être :
+
+* ***précis*** : pour éviter les ambigüités, un bon exemple doit clairement définir l'état du système avant et après l'action. Le test doit être facile à reproduire.
+* ***réaliste*** : utiliser de vraies exemples issue de la vraie vie qui permétrons aux lecteurs de mieux comprendre le contexte. `Ocean toto = ...` vs `Ocean atlantic = ...`
+* ***simple*** à comprendre : évitez la tentation d'explorer toutes la combinatoire. Évitez d'utiliser des tableaux avec des dizaines de lignes et colonnes. Concentrez vous sur des cas précis et représentatifs.
+
+## Non fonctionnel
+
+On appel besoins non fonctionnels, les besoins implicites des utilisateurs évidents et les attentes des utilisateurs non évidents comme : 
+
+* Le service doit répondre en moins de 200ms
+* Le programme doit être paramétrable par un unique fichier JSON
+
+Les tests doivent aussi couvrir ces attentes.
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 33d033c..b59a32a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # R4.02 - Qualité de développement
 
-[Version en ligne](https://univlille.gitlab.io/iut-info/r4.02/cours/)
+[Version en ligne](http://univlille.gitlab.io/iut-info/r4.02/cours/)
 
 ## Fiche ressource
 
@@ -31,7 +31,8 @@ Tests, tests de non régression, tests d'intégration
 ## Déroulé du cours
 
 * [Retour sur le TDD](./01-tdd/)
-* [Tester le non testable](./02-mock/)
+* [Tout est testable](./02-mock/)
+* [Il est beau mon test](./03-tests/)
 
 ## TPs
 
diff --git a/sample-code/pom.xml b/sample-code/pom.xml
new file mode 100644
index 0000000..35f6bd3
--- /dev/null
+++ b/sample-code/pom.xml
@@ -0,0 +1,60 @@
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+                             http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <properties>
+        <junit.version>5.7.0</junit.version>
+    </properties>
+
+    <groupId>fr.univlille.iut.info.r402</groupId>
+    <artifactId>sample-code</artifactId>
+    <version>1.0</version>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>17</source>
+                    <target>17</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.0.0-M5</version>
+            </plugin>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>0.8.4</version>
+                <executions>
+                    <execution>
+                        <id>prepare-agent</id>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <!-- junit 5 -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
+
diff --git a/sample-code/src/main/java/fr/univlille/iut/info/r402/Cell.java b/sample-code/src/main/java/fr/univlille/iut/info/r402/Cell.java
new file mode 100644
index 0000000..67e7b84
--- /dev/null
+++ b/sample-code/src/main/java/fr/univlille/iut/info/r402/Cell.java
@@ -0,0 +1,4 @@
+package fr.univlille.iut.info.r402;
+
+public record Cell(CellContent cellContent) {
+}
diff --git a/sample-code/src/main/java/fr/univlille/iut/info/r402/CellContent.java b/sample-code/src/main/java/fr/univlille/iut/info/r402/CellContent.java
new file mode 100644
index 0000000..4f91077
--- /dev/null
+++ b/sample-code/src/main/java/fr/univlille/iut/info/r402/CellContent.java
@@ -0,0 +1,5 @@
+package fr.univlille.iut.info.r402;
+
+public enum CellContent {
+    Water, Fish, Shark;
+}
diff --git a/sample-code/src/main/java/fr/univlille/iut/info/r402/Ocean.java b/sample-code/src/main/java/fr/univlille/iut/info/r402/Ocean.java
new file mode 100644
index 0000000..5d53f2a
--- /dev/null
+++ b/sample-code/src/main/java/fr/univlille/iut/info/r402/Ocean.java
@@ -0,0 +1,14 @@
+package fr.univlille.iut.info.r402;
+
+public class Ocean {
+    private final Cell[][] internalRepresentation;
+
+    public Ocean(int height, int width) {
+        this.internalRepresentation = new Cell[height][width];
+    }
+
+    public Cell[][] getInternalRepresentation() {
+        return internalRepresentation;
+    }
+
+}
diff --git a/sample-code/src/test/java/OceanTest.java b/sample-code/src/test/java/OceanTest.java
new file mode 100644
index 0000000..509dd98
--- /dev/null
+++ b/sample-code/src/test/java/OceanTest.java
@@ -0,0 +1,16 @@
+import fr.univlille.iut.info.r402.Cell;
+import fr.univlille.iut.info.r402.CellContent;
+import fr.univlille.iut.info.r402.Ocean;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static fr.univlille.iut.info.r402.CellContent.Fish;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class OceanTest {
+    @Test
+    void fish_should_move() {
+        new Ocean(3,3).getInternalRepresentation()[0][0] = new Cell(Fish);
+        assertEquals(Fish, new Ocean(3,3).getInternalRepresentation()[0][0].cellContent());
+    }
+}
-- 
GitLab