Domanda

Il mio motto per Java è "solo perché Java ha blocchi statici, non significa che dovresti usarli". Scherzi a parte, ci sono molti trucchi in Java che rendono il test un incubo.Due dei più odio sono le classi anonime e i blocchi statici.Abbiamo molto codice legacy che fa uso di blocchi statici e questi sono uno dei punti fastidiosi nella nostra spinta nella scrittura di unit test.Il nostro obiettivo è essere in grado di scrivere unit test per classi che dipendono da questa inizializzazione statica con modifiche minime al codice.

Finora il mio suggerimento ai miei colleghi è di spostare il corpo del blocco statico in un metodo statico privato e chiamarlo staticInit.Questo metodo può quindi essere chiamato dall'interno del blocco statico.Per i test unitari un'altra classe che dipende da questa classe potrebbe facilmente deridere staticInit con JMockit per non fare nulla.Vediamolo nell'esempio.

public class ClassWithStaticInit {
  static {
    System.out.println("static initializer.");
  }
}

Verrà cambiato in

public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

In modo che possiamo fare quanto segue in una JUnit.

public class DependentClassTest {
  public static class MockClassWithStaticInit {
    public static void staticInit() {
    }
  }

  @BeforeClass
  public static void setUpBeforeClass() {
    Mockit.redefineMethods(ClassWithStaticInit.class, MockClassWithStaticInit.class);
  }
}

Tuttavia questa soluzione presenta anche i suoi problemi.Non puoi correre DependentClassTest E ClassWithStaticInitTest sulla stessa JVM poiché desideri effettivamente eseguire il blocco statico ClassWithStaticInitTest.

Quale sarebbe il tuo modo di portare a termine questo compito?O qualche soluzione migliore, non basata su JMockit, che ritieni possa funzionare in modo più pulito?

È stato utile?

Soluzione

Quando riscontro questo problema, di solito faccio la stessa cosa che descrivi, tranne per il fatto che rendo protetto il metodo statico in modo da poterlo richiamare manualmente.Oltre a ciò, mi assicuro che il metodo possa essere invocato più volte senza problemi (altrimenti non è migliore dell'inizializzatore statico per quanto riguarda i test).

Funziona abbastanza bene e posso effettivamente verificare che il metodo dell'inizializzatore statico faccia ciò che mi aspetto/voglio che faccia.A volte è semplicemente più semplice avere del codice di inizializzazione statico e non vale la pena costruire un sistema eccessivamente complesso per sostituirlo.

Quando utilizzo questo meccanismo, mi assicuro di documentare che il metodo protetto viene esposto solo a scopo di test, con la speranza che non venga utilizzato da altri sviluppatori.Questa ovviamente potrebbe non essere una soluzione praticabile, ad esempio se l'interfaccia della classe è visibile esternamente (come sottocomponente di qualche tipo per altri team o come framework pubblico).Tuttavia è una soluzione semplice al problema e non richiede la configurazione di una libreria di terze parti (cosa che mi piace).

Altri suggerimenti

PowerMock è un altro framework fittizio che estende EasyMock e Mockito.Con PowerMock puoi farlo facilmente rimuovere comportamenti indesiderati da una classe, ad esempio un inizializzatore statico.Nel tuo esempio aggiungi semplicemente le seguenti annotazioni al tuo test case JUnit:

@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit")

PowerMock non utilizza un agente Java e pertanto non richiede la modifica dei parametri di avvio della JVM.Aggiungi semplicemente il file jar e le annotazioni sopra.

Questo entrerà in JMockit più "avanzato".Si scopre che puoi ridefinire i blocchi di inizializzazione statici in JMockit creando un file public void $clinit() metodo.Quindi, invece di apportare questo cambiamento

public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

potremmo anche andarcene ClassWithStaticInit così com'è e procedi come segue in MockClassWithStaticInit:

public static class MockClassWithStaticInit {
  public void $clinit() {
  }
}

Questo ci consentirà infatti di non apportare alcuna modifica alle classi esistenti.

Occasionalmente trovo inizializzatori statici nelle classi da cui dipende il mio codice.Se non riesco a rifattorizzare il codice, utilizzo PowerMock'S @SuppressStaticInitializationFor annotazione per sopprimere l'inizializzatore statico:

@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("com.example.ClassWithStaticInit")
public class ClassWithStaticInitTest {

    ClassWithStaticInit tested;

    @Before
    public void setUp() {
        tested = new ClassWithStaticInit();
    }

    @Test
    public void testSuppressStaticInitializer() {
        asserNotNull(tested);
    }

    // more tests...
}

Leggi di più su sopprimere comportamenti indesiderati.

Disclaimer:PowerMock è un progetto open source sviluppato da due miei colleghi.

Mi sembra che tu stia trattando un sintomo:progettazione scadente con dipendenze dall'inizializzazione statica.Forse un po' di refactoring è la vera soluzione.Sembra che tu abbia già eseguito un piccolo refactoring con il tuo staticInit() funzione, ma forse quella funzione deve essere chiamata dal costruttore, non da un inizializzatore statico.Se riesci a eliminare il periodo degli inizializzatori statici, starai meglio.Solo tu puoi prendere questa decisione (Non riesco a vedere la tua codebase) ma alcuni refactoring saranno sicuramente di aiuto.

Per quanto riguarda il mocking, utilizzo EasyMock, ma ho riscontrato lo stesso problema.Gli effetti collaterali degli inizializzatori statici nel codice legacy rendono difficili i test.La nostra risposta è stata quella di rifattorizzare l'inizializzatore statico.

Potresti scrivere il codice di prova in Groovy e deridere facilmente il metodo statico utilizzando la metaprogrammazione.

Math.metaClass.'static'.max = { int a, int b -> 
    a + b
}

Math.max 1, 2

Se non puoi usare Groovy, avrai davvero bisogno di refactoring del codice (magari iniettare qualcosa come un inizializzatore).

Cordiali saluti

Suppongo che tu voglia davvero una sorta di fabbrica invece dell'inizializzatore statico.

Un mix di singleton e factory astratta sarebbe probabilmente in grado di offrirti le stesse funzionalità di oggi e con una buona testabilità, ma ciò aggiungerebbe parecchio codice boiler-plate, quindi potrebbe essere meglio provare semplicemente a eseguire il refactoring eliminare completamente le cose statiche o se almeno potessi farla franca con qualche soluzione meno complessa.

Tuttavia, è difficile dire se sia possibile senza vedere il codice.

Non sono molto esperto nei framework Mock, quindi per favore correggimi se sbaglio, ma non potresti avere due diversi oggetti Mock per coprire le situazioni che menzioni?Ad esempio

public static class MockClassWithEmptyStaticInit {
  public static void staticInit() {
  }
}

E

public static class MockClassWithStaticInit {
  public static void staticInit() {
    System.out.println("static initialized.");
  }
}

Quindi puoi usarli nei tuoi diversi casi di test

@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class, 
                         MockClassWithEmptyStaticInit.class);
}

E

@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class, 
                         MockClassWithStaticInit.class);
}

rispettivamente.

Non proprio una risposta, ma mi chiedo solo: non esiste un modo per "invertire" la chiamata Mockit.redefineMethods?
Se non esiste un metodo così esplicito, eseguirlo nuovamente nel modo seguente non dovrebbe risolvere il problema?

Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);

Se esiste un metodo del genere, potresti eseguirlo nella classe' @AfterClass metodo e prova ClassWithStaticInitTest con il blocco inizializzatore statico "originale", come se nulla fosse cambiato, dalla stessa JVM.

Questa però è solo un'intuizione, quindi forse mi sfugge qualcosa.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top