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