LEZ05

prof. Carlo Bellettini


Estensioni


Estensioni in JUnit5

@ExtendWith

ExecutionConditionCallback ( a livello di classe ) [solo se applicato a classe
  BeforeAllCallback [solo se applicato a classe
    @BeforeAll
      PostProcessTestInstance
        ExecutionConditionCallback ( a livello di test )
          BeforeEachCallback
            @BeforeEach
               BeforeTestExecutionCallback
                 supportParameters  (se c'è almeno un parametro da risolvere)
                 resolveParameters  (se supporta il parametro)
                     TEST METHOD
                 TestExecutionException  (se test solleva eccezione)
               AfterTestExecutionCallback
            @AfterEach
          AfterEachCallback
    @AfterAll
  AfterAllCallback [solo se applicato a classe

ExecutionConditionCallback e BeforeEachCallback non sono intercambiabili: la signature è diversa (l'output di ExecutionConditionCallback determina l'esecuzione del test), inoltre se ExecutionConditionCallback non fa eseguire il test, non viene eseguita nemmeno BeforeEachCallback. Se il test è annotato con @Disabled viene saltata anche ExecutionConditionCallback.

Tutte le volte che non viene eseguita BeforeEachCallback, non viene eseguita nemmeno AfterEachCallback.

TestExecutionException si comporta come un catch in caso di fallimento del test.

JUnit non mi garantisce che le istanze delle estensioni sopravvivano tra un test e l'altro, quindi non devo fare affidamento sul loro stato (meglio usare le variabili statiche o lo store del context).

I context sono gerarchizzati:

├─Jupiter
  ├───Classe di test
      ├── Test 1
      ├── Test 2
      ├── Test parametrico
          ├─ Parametro 1
          ├─ Parametro 2
  ├───Classe di test 2
      ├── Test 1
      ├── Test 2
      ├── Test parametrico
          ├─ Parametro 1
          ├─ Parametro 2

Ogni livello ha il suo context e posso risalire da un context a quello del genitore.


Assignment 4

Scrivere estensione per gestire StandardInput

  • SystemInLogExt
  • @SystemInLog

Usarle nell'esempio Triangle

Riscrivere come estensioni le Rule fatte sopra in JU4


SystemInLogExt

public class SystemInLogExt implements BeforeEachCallback, AfterEachCallback{
    private static InputStream orig;
    private static InputStream inputIS;

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
           orig = System.in;
           inputIS = null;
    }

    public static void provideInput(String input) throws IOException {
        if (inputIS != null && inputIS.available()>0) {
            inputIS = new SequenceInputStream(inputIS,
                                                new ByteArrayInputStream(input.getBytes()));        
        }
        else
            inputIS = new ByteArrayInputStream(input.getBytes());
        System.setIn(inputIS);

    }

    @Override
    public void afterEach(ExtensionContext context) throws Exception {
          System.setIn(orig);
          inputIS = null;
    }
}

Ma potrebbero esserci possibili problemi se in altre classi System.in veniva memorizzato o usato come inizializzazione di uno Scanner.

In generale, non è una buona pratica avere l'estensione degli input che permette di concatenare più input durante il test (avrebbe senso se l'input potesse cambiare in base al risultato del test, ma starei mettendo una condizione dentro a un test).


prima Rule -> Extension

import java.lang.annotation.Annotation;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class RuleMethodDescription implements TestRule{
    @Override public Statement apply(Statement base, Description description) {
        System.out.println("Sto eseguendo "+description.getMethodName());
        System.out.print("Ed è annotato con:");
        for (Annotation annotation : description.getAnnotations()) {
            System.out.print(annotation.toString()+" ");
        }
        System.out.println("");
        return base;
    }
}
@Override public void BeforeEachCallback(ExtensionContext context) {
  System.err.println("Sto eseguendo "+context.getDisplayName());
  System.err.print("Ed è annotato con:");
  for (Annotation annotation : context.getRequiredTestMethod().getAnnotations()){
    System.err.print(annotation.toString()+" ");
  }
  System.err.println("");
}

o in qualche altro metodo della gerarchia


seconda Rule

package it.unimi.di.vec.rulesLib;

import org.junit.AssumptionViolatedException;
import org.junit.runners.model.Statement;

class IgnoreStatement extends Statement {

    private final String msg;
    public IgnoreStatement() { super(); this.msg = ""; }
    public IgnoreStatement(String msg) { super(); this.msg = msg; }
    @Override
    public void evaluate() throws Throwable {
        throw new AssumptionViolatedException(msg);
    }
}

public class RuleSkipOddTests implements TestRule {
    private static HashMap<String,Boolean> isEvenTest = new HashMap<String,Boolean>();
    @Override
    public Statement apply(Statement base, Description description) {
        String className = description.getTestClass().getName();
        boolean even=true;
        if (isEvenTest.containsKey(className))
            even = isEvenTest.get(className);
        isEvenTest.put(className, new Boolean(!even));       
        if (even) return base;
        return new IgnoreStatement("Skipped odd test");
    }
}

possibili problemi con Parametrized...


public class OddTestExecution implements ExecutionCondition, AfterAllCallback, BeforeAllCallback {

  private static Map<String, Boolean> isEvenTest = new HashMap<>();

  @Override
  public void beforeAll(ExtensionContext context) throws Exception {
    isEvenTest.put(context.getRequiredTestClass().getName(), true);
  }

  @Override
  public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
    String className = context.getRequiredTestClass().getName();

    if (context.getTestMethod().isPresent()) {
      Boolean val = isEvenTest.get(className);
      isEvenTest.put(className, !val);

      return val? ConditionEvaluationResult.enabled("Even") :
                  ConditionEvaluationResult.disabled("Odd");
    }
    return ConditionEvaluationResult.enabled("Test");
  }

  @Override
  public void afterAll(ExtensionContext extensionContext) throws Exception {
    isEvenTest.remove(extensionContext.getRequiredTestClass().getName());
  }
}

Composed Annotation

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.junit.jupiter.api.extension.ExtendWith;

@Retention(RUNTIME)
@Target({ TYPE, METHOD, ANNOTATION_TYPE })
@ExtendWith(SystemOutLogExt.class)
public @interface SystemOutLog {}
@Test
@SystemOutLog
void testAssertion(ByteArrayOutputStream out) {
  System.out.print("pipo");
  out.reset();
  System.out.print("pippo");
  assertThat(out).hasStringTo("pippo");
}

Pubblicazione in locale su Maven

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.1'
    }
}

plugins {
    id "java"
    id "maven-publish"
}
apply plugin: 'org.junit.platform.gradle.plugin'

group = 'it.unimi.di.vec'
version = '0.3'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.junit.jupiter:junit-jupiter-api:5.0.1'
    testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.0.1';
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
    repositories {
        mavenLocal()
    }
}

Posso dividere i progetti per riutilizzarli usando Gradle. Se estraggo il codice che voglio riusare e lo metto in un altro progetto, posso dire a Gradle di "pubblicarlo" su `maven-local`. Dopodichè, tutti i progetti sul mio pc potranno includerlo con Gradle.

Assignment 4bis

Fare una estensione che permetta di eseguire il test in base a successo di altro test

  • in JUnit4 potete usare le Rule e fissare ordine di esecuzione dei test
  • in JUnit5 potete usare ExtendWith e visto che non potete fissare ordine, fallite con una eccezione se l'altro test non è ancora stato eseguito

Assume other test

import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;

public class AssumeExt implements AfterEachCallback, ExecutionCondition , BeforeAllCallback {
  private static HashMap<String, Boolean> results = new HashMap<>();
  @Override
  public void beforeAll(ExtensionContext context) throws Exception {
    results = new HashMap<>();
  }
  @Override
  public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
    Optional<AssumeSuccessOf> ann = findAnnotation(context.getTestMethod(), AssumeSuccessOf.class);

    if (!ann.isPresent())
      return ConditionEvaluationResult.enabled("No annotation");
    String key = ann.get().value();
    if (key.isEmpty())
      return ConditionEvaluationResult.enabled("No dependency");
    if (results.containsKey(key)) {
      return results.get(key)==false ? ConditionEvaluationResult.enabled("OK dependency") :
                      ConditionEvaluationResult.disabled("dependency not respected: "+
                                                         context.getDisplayName() + " on "+ key);
    }
    return ConditionEvaluationResult.disabled("Wrong order execution");
  }
  @Override
  public void afterEach(ExtensionContext context) throws Exception {
    results.put(context.getDisplayName(), context.getExecutionException().isPresent());
  }
}

Si poteva fare anche in BeforeEach?


  private static Map<String, Boolean> results = new HashMap<>();

  @Override
  public void beforeEach(ExtensionContext context) throws Exception {
    Optional<MyAssume> ann = findAnnotation(context.getTestMethod(), MyAssume.class);
    if (!ann.isPresent())
      return;
    String key = ann.get().value();
    if (key.isEmpty())
      return;
    if(results.containsKey(key)) {
      assumeFalse(results.get(key));
    } else {
      throw new WrongOrderException();
    }
  }

  @Override
  public void afterEach(ExtensionContext context) throws Exception {
    results.put(context.getDisplayName(),context.getExecutionException().isPresent());
  }
}

Organizzare i test

Perché raggrupparli?

  • Tipi di test: UnitTests, IntegrationTests, SmokeTests, RegressionTests, PerformanceTests ...
  • Velocità di esecuzione: SlowTests, QuickTests
  • Stato di sviluppo del test: UnstableTests, InProgressTests
  • Altro: ComponentXXTests, FeatureXXTests, NightlyBuildTests

Organizzare i test

come raggrupparli?

contenitori fisici

  • package
  • file
  • class
  • nested class

contenitori logici

  • name convention
  • test suite
  • categorie/tag

Un test può stare in un solo contenitore fisico alla volta, mentre può essere messo in più contenitori logici.

I contenitori fisici sono più intuitivi ma anche più rigidi di quelli logici.

Posso far fare il raggruppamento in contenitori logici a Gradle o all'IDE.


gradle filters JU4

test {
    filter {
       //specific test method
          includeTestsMatching "org.gradle.SomeTest.someSpecificFeature"
       //specific test method, use wildcard for packages
          includeTestsMatching "*SomeTest.someSpecificFeature"
       //specific test class
          includeTestsMatching "org.gradle.SomeTest"
       //specific test class, wildcard for packages
          includeTestsMatching "*.SomeTest"
       //all classes in package, recursively
          includeTestsMatching "com.gradle.tooling.*"
       //all integration tests, by naming convention
          includeTestsMatching "*IntegTest"
       //only ui tests from integration tests, by some naming convention
          includeTestsMatching "*IntegTest*ui"
       //specific test class and test method
          includeTest "org.gradle.SomeTest", "someTestMethod"
       }
}

Questo sistema non è ancora supportato in JUnit5


Esempio matching JU4

task small(type: Test) {
    filter {
        includeTestsMatching "*.*SMALL"
    }
}
task medium(type: Test) {
    filter {
        includeTestsMatching "*.*MEDIUM"
    }
}
task large(type: Test) {
    filter {
        includeTestsMatching "*.*LARGE"
    }
}

TestSuite

  • In JUnit 3 erano una gerarchia di classi
  • in JUnit4 sono diventate annotazioni
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
  TestFeatureLogin.class,
  TestFeatureLogout.class,
  TestFeatureNavigate.class,
  TestFeatureUpdate.class
})

public class FeatureTestSuite {
 }

Gerarchie di Categorie

https://github.com/junit-team/junit4/wiki/Categories

public interface FastTest {}
public interface UnitTest extends FastTest {}
public interface SlowTest {}
public interface PerformanceTest extends SlowTest {}
public interface DatabaseTest extends SlowTest {}
  • Le applico (con annotazioni) a classi o a metodi
public class A {
  @Test
  public void a() {
    fail();
  }

  @Category(SlowTest.class)
  @Test
  public void b() {
  }
}

@Category({SlowTest.class, FastTest.class})
public class B {
  @Test
  public void c() {
  }
}

Run delle categorie

  • in JUnit

    @RunWith(Categories.class)
    @SuiteClasses( { A.class, B.class })
    @IncludeCategory(SlowTests.class)
    @ExcludeCategory(FastTests.class)
    public class SlowTestSuite {
    // Will run A.b, but not A.a or B.c
    }
    
  • in gradle

    test {    
         useJUnit {
            includeCategories 'SmallTests'
            excludeCategories 'DBTests'
         }
    }
    

Wildcard Pattern Suite

https://github.com/MichaelTamm/junit-toolbox

  • Permette di specificare le classi figlie della suite di test utilizzando un wildcard pattern.
@RunWith(WildcardPatternSuite.class)
@SuiteClasses({"**/*Test.class", "!gui/**"})
public class AllButGuiTests {}
  • Rimpiazza le Categories perché puoi usare le seguenti annotazioni:
    • @IncludeCategory
    • @ExcludeCategory
@RunWith(WildcardPatternSuite.class)
@SuiteClasses("**/*Test.class")
@IncludeCategory(SlowTests.class)
public class OnlySlowTests {}

JUnit5

@Tag

Rimpiazzano le categorie. Le categorie erano interfacce e potevo contare sull'IDE per identificare errori di battitura, i Tag sono stringhe, se commetto un errore a scrivere il nome di un tag ne definisco un altro. Al momento non sono supportati da IntelliJ

@Nested

Suites

Al momento non supportano i test parametrici


Tag in gradle con JU5

Lista di stringhe

@Tag({"Lento", "Integrazione"})

in build.gradle:

junitPlatform {
    filters {
        engines {
            include 'junit-jupiter'
            // exclude 'junit-vintage'
        }
        tags {
            include 'fast', 'smoke'
            // exclude 'slow', 'ci'
        }
        packages {
            include 'com.sample.included1', 'com.sample.included2'
            // exclude 'com.sample.excluded1', 'com.sample.excluded2'
        }
        includeClassNamePattern '.*Spec'
        includeClassNamePatterns '.*Test', '.*Tests'
    }
}

Using gradle command line

junitPlatform {
    details 'tree'
    filters {
        includeClassNamePattern '.*'

        if (project.hasProperty('itags')) {
            tags.include project.getProperty('itags').split(',')
        }
        if (project.hasProperty('etags')) {
            tags.exclude project.getProperty('etags').split(',')
        }
    }
}

Suite in JUnit5

http://junit.org/junit5/docs/current/user-guide/#running-tests-junit-platform-runner-test-suite

bisogna inserire nuova dipendenza in build.gradle:

testCompile group: 'org.junit.platform', name: 'junit-platform-runner', version: '1.0.0'
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectPackages("example")
public class JUnit4SuiteDemo {
}

possibili problemi icludendo Test Parametrici


annotazioni per suite

http://junit.org/junit5/docs/current/api/org/junit/platform/suite/api/package-summary.html

Include e Exclude sono solo filtri dopo aver applicato la select

  • ExcludeClassNamePatterns
  • ExcludeEngines
  • ExcludePackages
  • ExcludeTags
  • SelectClasses
  • SelectPackages
  • IncludeClassNamePatterns
  • IncludeEngines
  • IncludePackages
  • IncludeTags
  • UseTechnicalNames

in IntelliJ

non c'è ancora supporto diretto per Tag, ma può essere mediato tramite gradle

results matching ""

    No results matching ""