TSM - JUnit 5 - o nouă abordare, noi funcționalităţi

Cătălin Tudose - Java and Web Technologies Expert @ Luxoft Training

JUnit este un framework de testare pentru limbajul de programare Java. Versiunea JUnit 5 a fost reproiectată să rezolve anumite probleme particulare ale precedentelor versiuni. De asemenea, oferă o nouă arhitectură, cu posibilitatea de a crea a ierarhie de teste, cu assertions şi assumptions, cu teste dinamice şi parametrizate. Acest articol este o scurtă introducere în JUnit 5, pentru a pune la dispoziţie cititorului, posibilitatea de a înţelege noua arhitectură şi noile funcţionalităţi pe care să le exploreze mai departe.

JUnit, ca framework de testare unitară pentru limbajul de programare Java, este o unealtă foarte importantă pentru abordarea test-driven development (dezvoltare ghidată de teste). Este parte a unei familii de frameworkuri de testare, numite colectiv xUnit.
JUnit este legat la compilare ca și JAR, fiind cel mai frecvent inclusă bibliotecă externă în proiectele Java.

TDD (Test Driven Development) este un proces de dezvoltare software care se bazează pe repetarea unui scurt ciclu: mai întâi, cerinţele sunt transformate în test-case-uri specifice; apoi, software-ul este îmbunătăţit doar să treacă noile teste. Acest lucru este diferit de dezvoltarea care permite software-ului să fie creat fără a dovedi că îndeplineşte cerinţele.

Beneficiile TDD includ:

Programatorul este ghidat de ţinte clare.

Neajunsurile JUnit 4

JUnit 4 apărut în 2006, pune la dispoziţie o arhitectură simplă şi monolitică. Întreaga funcţionalitate este concentrată într-un singur fişier JAR. În ciuda aparentei sale simplităţi, au apărut o serie de probleme, care s-au agravat pe măsura trecerii timpului.

Faptul că API-ul existent nu este flexibil a forţat IDE-urile şi uneltele care utilizau JUnit să fie puternic cuplate cu acesta. Era nevoie să se acceseze implementarea internă a JUnit sau chiar să se utilizeze reflexia pentru a obţine informaţia dorită.

Aşadar, de vreme ce acelaşi fişier JAR era utilizat de toată lumea şi IDE-urile erau puternic cuplate cu acesta, posibilităţile de evoluţie ale JUnit erau serios reduse. Schimbarea unei variabile sau a unei metode private putea să îi afecteze pe cei care o utilizau din exterior. Un nou API proiectat pentru astfel de unelte, şi o nouă arhitectură erau necesare pentru a asigura evoluţia ulterioară.

Noua abordare modulară

O nouă abordare, una modulară, era necesară pentru a permite evoluţia JUnit. Separarea logică solicită:

În consecinţă, arhitectura JUnit 5 rezultată conţine trei module:

JUnit Platform, care serveşte pentru lansarea frameworkului de testare pe maşina virtuală Java. De asemenea, oferă un API pentru lansarea testelor de la consolă, din IDE-uri sau din alte unelte.

JUnit Jupiter este o combinaţie între noul model de programare şi modelul de extensie pentru scrierea testelor şi a extensiilor în JUnit 5. Numele a fost ales de la a cincea planetă a sistemului solar, care este şi cea mai mare.

JUnit Vintage oferă un motor de testare pentru a rula teste JUnit 3 şi JUnit 4 pe noua platformă, asigurând şi necesara compatibilitate înapoi.

Pasul spre JUnit 5

Pentru a putea utiliza JUnit 5 într-un proiect Java, următoarele dependenţe trebuie adăugate la configuraţia Maven:

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.0.1</version>
  </dependency>

  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.0.1</version>
  </dependency>

  <dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-runner</artifactId>
    <version>1.0.1</version>
  </dependency>
</dependencies>

Următoarele diferenţe importante există între JUnit 4 şi JUnit 5:

Arhitectură

Versiuni de Java necesare pentru rulare

Adnotări

Primul test JUnit 5

Spre deosebire de JUnit 4, clasa de test şi metodele de test pot fi package private. Un test simplu arată astfel:

class TestFixturesExample {
  private static 
   HeavyResourceRequiredForAllTests heavyResource;

  private TestSetup testSetup;
  private SystemUnderTest systemUnderTest;

  @BeforeAll
  static void willBeDoneBeforeAllTests() {
    System.out.println("@BeforeAll");
    heavyResource = 
      new HeavyResourceRequiredForAllTests(
      "@BeforeAll", "@AfterAll");

    heavyResource.start(new DummyConfiguration());
  }

 @AfterAll
  static void willBeDoneAfterAllTests() {
    heavyResource.close();
  }
  @BeforeEach
  void willBeDoneBeforeEachTest() {
  testSetup = new TestSetup(
    "@BeforeEach", "@AfterEach");

    testSetup.prepare();
    systemUnderTest = new SystemUnderTest();
  }

  @AfterEach
  void willBeDoneAfterEachTest() {
    testSetup.cleanUp();
  }

  @Test
  void shouldReturnTheTruth() {
  Fact result = systemUnderTest.returnTheTruth();

    assertTrue(result.isTruth());
  }

  @Test
  void shouldReturnTheLie() {
   Fact result = systemUnderTest.returnTheLie();

   assertFalse(result.isTruth());
  }
}

Câteva remarci despre testul de mai sus:

  1. Metoda adnotată cu @BeforeAll va fi executată o singură dată, înaintea rulării testelor.

  2. Metoda adnotată cu @BeforeEach va fi executată de fiecare dată, înaintea execuţiei fiecărui test.

  3. Metodele adnotate cu @Test vor fi executate una câte una, pentru verificarea funcţionalităţii.

  4. Metoda adnotată cu @AfterEach va fi executată de fiecare dată după rularea unui test.

  5. Metoda adnotată cu @AfterAll va fi executată o singură data, după rularea tuturor testelor.

Assertions în JUnit 5

JUnit Jupiter pune la dispoziţie mai multe metode de tip assertion decât JUnit 4. Au fost adăugate câteva care funcţionează foarte bine împreună cu expresiile lambda din Java 8. Toate metodele de acest fel din JUnit Jupiter sunt statice şi provin din clasa org.junit.jupiter.api.Assertions.

Comparaţia între assertions în JUnit 4 şi JUnit 5 arată astfel:

Assertions

Mesajul din assertions este ultimul parametru al metodelor:

static void assertX(..., String message)
static void assertX(..., Supplier  messageSupplier)

Supplierul permite iniţializarea leneşă în cazul mesajelor complexe. O bucată de cod care să utilizeze posibile assertions arată astfel:

class AssertionsExample {
  private final SystemUnderTest systemUnderTest = 
    new SystemUnderTest("Assertions");

  @Test
  void shouldRecognizeWhenTestsStarted() {
    systemUnderTest.examine();
    assertTrue(systemUnderTest.isUnderTest());
  }

  @Test
  void shouldRecognizeIfTestsDidNotStarted() {
    assertFalse(systemUnderTest.isUnderTest());
  }

  @Test
  void shouldReturnNullInCaseOfNoJob() {
    assertNull(systemUnderTest.getCurrentJob());
  }

  @Test
  void shouldReturnJobIfThereIsOneRun() {
    systemUnderTest.addJob(aSomeJob());
    systemUnderTest.run();
    assertNotNull(systemUnderTest.getCurrentJob());
  }

  @Test
  void shouldRecognizeTheSameJob() {
    Job job = aSomeJob();
    systemUnderTest.addJob(job);
    systemUnderTest.run();
    assertSame(job, systemUnderTest.getCurrentJob());
  }

  @Test
  void shouldRecognizeNotTheSameJob() {
    systemUnderTest.addJob(aSomeJob());
    systemUnderTest.run();
    assertNotSame(aSomeJob(),  
      systemUnderTest.getCurrentJob());
  }

  @Test
  void shouldRecognizeEqualJob() {
    systemUnderTest.addJob(aSomeJob());
    systemUnderTest.run();
    assertEquals(aSomeJob(), 
      systemUnderTest.getCurrentJob());
  }

  @Test
  void shouldRecognizeNotEqualJob() {
    systemUnderTest.addJob(aSomeJob());
    systemUnderTest.run();
    assertNotEquals(aDifferentJob(), 
      systemUnderTest.getCurrentJob());
    }

  @Test
  void shouldRecognizeEqualJobs() {
  Object[] expectedJobs = 
    {aSomeJob(), aDifferentJob()};

    systemUnderTest.addJob(aSomeJob());
    systemUnderTest.addJob(aDifferentJob());
    assertArrayEquals(expectedJobs, 
      systemUnderTest.getJobs());
  }

  private Job aDifferentJob() {
    return aJob("different job");
  }

  private Job aSomeJob() {
    return aJob("some job");
  }

  private Job aJob(String name) {
    return new Job(name);
  }
}

Assumptions în JUnit 5

JUnit Jupiter pune la dispoziţie o parte din metodele de tip assumption din JUnit 4. JUnit Jupiter adaugă de asemenea, metode care funcţionează împreună cu expresiile lambda Java 8. Toate metodele de tip assumption din JUnit Jupiter sunt statice şi provin din clasa org.junit.jupiter.api.Assumptions. Parametrul mesaj este pe ultima poziţie.

Metodele de tip assertions sunt executate doar în cazul în care presupunerile de tip assumption sunt îndeplinite.

Metoda arată astfel:

static void assumingThat(
  BooleanSupplier assumptionSupplier, 
  Executable executable)

O comparaţie între assumptions în JUnit 4 şi JUnit 5 se prezintă în această formă:

Assumptions

Utilizarea unei metode de tip assume poate arăta așa:

class AssumeExample {
  private static final String 
    EXPECTED_JAVA_VERSION = "1.8";

  private final TestsEnvironment 
    environment = new TestsEnvironment(
      new JavaSpecification(
        System.getProperty(
        "java.vm.specification.version")),
      new OperationSystem(
        System.getProperty(
        "os.name"), System.getProperty("os.arch"))
  );

  private final SystemUnderTest 
    systemUnderTest = new SystemUnderTest();

  @BeforeEach
  void init() {
    assumeTrue(environment.isWindows());
  }

  @Test
  void shouldRecognizeThatHasNoJobToRun() {
    assumingThat(
      () -> environment.aJavaVersion()
        .equals(EXPECTED_JAVA_VERSION),
        () -> assertFalse(
        systemUnderTest.hasJobToRun()));
  }

  @Test
  void shouldRecognizeThatHasJobToRun() {
   assumeTrue(environment.isAmd64Architecture());
   systemUnderTest.run(new Job());
   assertTrue(systemUnderTest.hasJobToRun());
  }
}

Concluzii

JUnit 5 oferă un API nou şi flexibil pentru scrierea de teste, pentru assertions şi assumptions. Sunt acordate o mulţime de metode statice şi adaptate noilor facilităţi de programare funcţională, introduse de Java 8. De asemenea, noua arhitectură modulară facilitează atât munca dezvoltatorului cât şi interacţiunea cu IDE-urile şi cu alte unelte. Cititorul ar putea avea în acest moment o primă imagine despre ceea ce JUnit 5 pune la dispoziţie şi poate începe scrierea unor prime teste folosind acest framework.