Laborator 06 - Unit Testing

Set up

Introducere

De ce testam?

Testam pentru a arata ca programul functioneaza. Acest aspect este logic, dar are cateva probleme:

  • Are tendinta de a se transforma intr-o “profetie”. Cel mai simplu mod de a-ti trece testele si de a te convinge ca programul tau functioneaza este sa alegi un set minim de teste si sa nu incerci foarte mult.
  • Incercarea de a arata ca un program functioneaza prin testare este, intr-un fel, inutila. Testarea programelor poate dezvalui in mod eficient prezenta erorilor, dar niciodata sa nu arate absenta acestora.

Testarea este, in esenta, un proces de esantionare.

Ganditi-va la un sac plin cu pietre, pietre albe indicand o intrare pentru care programul reuseste, pietre negre indicand o intrare pentru care programul esueaza.

Acum sa presupunem ca ati scos pietre din geanta si ca ati vazut doar pietre albe.

Cate trebuie sa extragi inainte de a putea concluziona cu siguranta ca nu exista pietre negre in geanta?

De fapt, singura modalitate de a fi sigur de asta este sa scoti fiecare piatra din sac. Dar daca sacul are atat de multe pietre incat nu avem timp si rabdare sa scoatem mai mult decat un mic procent… Ei bine, pur si simplu nu putem fi siguri ca nu sunt mai multe pietre negre aflate in sac.

Deci „De ce testam?”. Testam pentru a castiga incredere in codul nostru. Daca alegem o multime de teste si le alegem intr-un mod inteligent, putem spune in mod legitim ca am invatat ceva. Poate ca nu putem spune ca nu au mai ramas bug-uri, dar putem spune cel putin ca programul functioneaza de cele mai multe ori.

Moduri de testare

Piramida testelor

Teste unitare

Testele unitare sunt teste automatizate care asigura ca o aplicatie indeplinește cerintele de design si aplicatia ca se comporta conform specificatiilor.
In ceea ce priveste OOP, “unitatea” care urmeaza sa fie supusa testarii este adesea:

  • interfata
  • clasa
  • metoda

Junit 5

  • JUnit - un framework de testare unitară pentru Java.
  • Important in Test-driven development(TDD).

TDD (Test Driven Development) este un sproces de dezvolatre software care se bazeaza pe repetarea a unui ciclu foarte scurt de dezvoltare: Cerintele sunt transformate in cazuri de testare specifice → software-ul este imbunatatit doar pentru a trece DOAR testele noi.
Beneficii TDD:

  • “Driven” de catre obiective clare
  • Cod mai sigur
  • Izoleaza codul incorect
  • Se introduc cu usurinta functionalitati noi
  • Se documenteaza aplicatia.

  • JUnit este conectat ca JAR la momentul compilarii.
  • JUnit este cea mai frecvent inclusa biblioteca externa în proiecte Java.

JUnit 5 este ultima versiune a bibliotecii JUnit. JUnit 5 este compatibil cu Java 8 sau orice versiune dupa aceasta.

Adaugare JUnit in IntelliJ
  1. IntelliJ: File → Project Structure


  2. Project Structure: Libraries


  3. Libraries: + → From Maven…


  4. Tastati in bara de search 'junit jupiter', iar apoi apasati Enter. Selectati: org.junit.jupiter:junit-jupiter:5.8.2


  5. Apasati OK, iar apoi asteptati descarcarea bibliotecii.

  6. In final din fereastra de Project Structure, apasati Apply, ai apoi OK.
Creare teste

Crearea unui test se realizeaza prin adnotarea unei metode cu @Test.

@Test
public void theySeeMeRollinTheyHatin() {
    /* code to test */
}

Sa presupunem ca avem o clasa care contine informatii despre un student:

public class Student {
    private String name;
    private Double gradeIC;
    private boolean lovesIC;
    private LocalDate birthday;
 
    public Student(String name, Double gradeIC, boolean lovesIC, LocalDate birthday) {
        this.name = name;
        this.gradeIC = gradeIC;
        this.lovesIC = lovesIC;
        this.birthday = birthday;
    }
 
    public void sprinkleSomeMagicNerdDust() {
        this.lovesIC = true;
        this.gradeIC = 10.0;
    }
 
    ...
    /* boilerplate code aka getters and setters */
    ...
}

Si dorim sa cream un test pentru metoda, constructor, getters si setters.

Pentru a realiza acest lucru, avem nevoie sa folosim cateva metode din biblioteca de JUnit. Cele mai relevante metode pe care le vom folosi pentru a testa, sunt:

assertEquals(Object expected, Object actual);

assertNotEquals(Object unexpected, Object actual);

assertNotNull(Object actual);

Exista o colectie generoasa de metode prin care se poate face assert: https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html

Astfel, testul nostru o sa arate in modul urmator:

@Test
public void noLongerATestNoobie() {
    Student student = new Student("Chris Rock", 7.0, false, LocalDate.of(1965, 2, 7));
    assertNotNull(student); // True
    assertEquals(student.getGradeIC(), 7.0); // True
    assertEquals(student.getLovesIC(), false); // True
 
    student.sprinkleSomeMagicNerdDust();
 
    assertNotEquals(student.getGradeIC(), 7.0); // True => no longer 7. the previous call changed the grade.
    assertEquals(student.getGradeIC(), 10.0); // True
    assertEquals(student.getLovesIC(), true); // True
}

In cazul in care toate assert-urile o sa fie True, testul a trecut. In cazul in care oricare dintre assert-uri intoarce False, atunci testul o sa pice.

Alte metode uzuale de assert sunt:

  • assertThat
/* e.g. */
assertThat(student, instanceOf(Student.class));
  • assertAll
/* e.g. */
assertAll("last name", () -> assertTrue(student.getName().startsWith("C")),
                       () -> assertTrue(student.getName().endsWith("k")),
                       () -> assertEquals(7.0, student.getGradeIC()));
  • assertThrows
/* e.g. */
NumberFormatException thrown = assertThrows(NumberFormatException.class, () -> { Integer.parseInt("One"); } ,
                                            "NumberFormatException was expected");
 
Assertions.assertEquals("For input string: \"One\"", thrown.getMessage());

In cazul in care nu se face import automat pentru @Test si assert, adaugati:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

In ceea ce priveste numirea metodelor de test, cea mai utilizata conventie este: MethodName_StateUnderTest_ExpectedBehavior. (e.g. isAdult_AgeLessThan18_False). Mai multe conventii gasiti la linkul: https://dzone.com/articles/7-popular-unit-test-naming

In cazul in care doriti sa rulati testele din command line:
java -jar junit-platform-console-standalone-<version>.jar <Options>

Before si After

Sa spunem ca ne dorim sa existe un anumit set up pentru testele noastre, sau ca vrem avem un anumit comportament dupa finalizarea testului.
Am putea adauga aceste functionalitati in corpului metodei de test. Dar daca avem mai multe teste pentru care am vrea sa se intample acelasi lucru inainte sau dupa test?
Am ajunge in situatia in care am avea mult cod duplicat, iar asta ar incalca un pricipiu important de dezvoltare - DRY (Don't repeat yourself).

In acest caz, vom introduce cateva adnotari noi:
@BeforeAll si @AfterAll, respectiv @BeforeEach si @AfterEach.

    @BeforeEach
    void setUp() {
        System.out.println("Test started");
 
        student = new Student("BoJack", 4.0, false, LocalDate.of(1964, 1, 2));
    }
 
    @Test
    void isPassingIC_GradeLessThan5_False() {
        assertNotNull(student);
        asssertFalse("Bro, that Gandalf meme is getting old...", student.isPassing()); // grade is 4 < 5 => student not passing
    }
 
    @Test
    void isPassingIC_GradeGraterThan5_True() {
        assertNotNull(student);
        student.study(); // method body: student.grade = 10;
        asssertTrue("Ops, i did it again!", student.isPassing()); // grade is now 10
    }
 
    @AfterEach
    void tearDown() {
        student.throwAParty();
        System.out.println("So cool, this is!");
    }

Coverage

Code coverage este o metrica care arata cat de mult din cod este testat. Este o metrica foarte utila care ajuta in evaluarea calitatii testelor.

Valorile comune care apar in rapoartele de acoperire includ:

  • Function coverage: cate dintre funcțiile definite au fost apelate.
  • Statement coverage: cate dintre instrucțiunile din program au fost executate.
  • Branches coverage: cate ramuri ale structurilor de control (e.g if-else) au fost executate.
  • Condition coverage: cate dintre subexpresiile booleene au fost testate pentru true si false.
  • Line coverage: cate linii de cod sursa au fost testate.

Pentru a rula coverage pe un test in IntelliJ:
Selectati clasa de test/folder-ul de teste → Click dreapta → Click 'Run “Tests” With Coverage'

Exercitii

  1. Descarcati arhiva de laborator: laborator6.zip.
  2. Importati arhiva ca proiect in IntelliJ.
  3. Parcurgeti continutul din src→main→java→org→catalogic.
  4. Dupa ce ati parcurs codul sursa, intrati in src→test→java→org→catalogic.
  5. In acest package, veti avea 4 suite de teste:
    • (2p) Test1: in aceasta clasa completati testele pentru a va asigura ca factory-ul creeaza instante corecte pentru obiectele voastre.
    • (3p) Test2: in aceasta clasa completati testele pentru a va asigura ca se arunca exceptiile potrivite atunci cand este cazul.
    • (4p) Test3: in aceasta clasa completati testele pentru a va asigura ca funtionalitatea de addGrade functioneaza corespunzator.
    • (1p) Test4: rulati coverage-ul, iar in aceasta clasa completati cu toate testele necesare pentru a avea un coverage de 100%.
icalc/laboratoare/laborator-06.txt · Last modified: 2022/05/02 16:21 by dragos.sandulescu97
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0