Quando, come e perché utilizzare PowerMock. Vengono analizzati i legami tra design delle applicazioni e strumenti di test. Sono presenti esempi di codice semplice ma verosimile con i rispettivi test.
2. Cos’è PowerMock
PowerMock è un potente framework Java, che permette di scrivere test unitari per del codice che
normalmente non sarebbe testabile.
PowerMock estende alcuni tra i più famosi framework di mocking disponibili per il linguaggio come
EasyMock e Mockito. In questo talk ci occuperemo dell’integrazione con Mockito, ovvero PowerMockito.
Principali funzionalità che PowerMock aggiunge a Mockito:
● Mocking di classi finali, metodi statici e costruttori.
● Verifica delle invocazioni di metodi privati.
● Soppressione di comportamenti indesiderati (side effects) di metodi e classi.
PowerMock implementa le funzionalità sopra elencate, grazie alla combinazione di molteplici tecniche, a
partire dalla semplice introspezione tramite reflection per arrivare ad un classloader personalizzato ed
alla manipolazione del bytecode tramite instrumentazione.
3. Due parole su Mockito
Mockito è il più famoso framework di mocking per Java ed una tra le 10 librerie più utilizzate in assoluto
per il linguaggio.
● E’ un framework intuitivo, flessibile e divertente da utilizzare.
● Si basa su un approccio No expect-run-verify, non ha il concetto di expectation, ma solo le fasi di
stubbing e verification sui collaboratori.
● Si possono creare due tipologie di test doubles, i mock e gli spy per il partial mocking che come
vedremo è molto importante in ottica PowerMock.
@Test
public void test()
{
// Fixture.
final MyInterface mockDependency = Mockito.mock(MyInterface.class);
// Stubbing.
Mockito.when(mockDependency.method(Mockito.eq("testparam"))).thenReturn("testresult");
// Run Test.
final String result = new SystemUnderTest(mockDependency).methodToTest("testparam");
// Assertion.
Assert.assertEquals("Composizione stringa risultato", "prefix-testresult-suffix", result);
// Verification.
Mockito.verify(mockDependency, Mockito.times(1)).method(Mockito.eq("testparam"));
}
4. Ecosistema dei test
TEST DI ACCETTAZIONE
Funziona tutto il sistema?
(Altre definizioni: functional, customer, system tests)
TEST DI INTEGRAZIONE
Funziona il nostro codice rispetto a del
codice che non possiamo modificare?
(framework pubblico / librerie di altri gruppi di lavoro)
TEST UNITARI
I nostri oggetti svolgono correttamente le
loro responsabilità ed è comodo utilizzarli?
Classificazione livelli di test:
Growing Object-Oriented Software, Guided by Tests
PowerMock è uno strumento che si può utilizzare durante la scrittura di test di integrazione e soprattutto
unitari che misurano la qualità interna (codice facile da capire e da modificare) del sistema.
5. Perché questo talk?
Vorrei condividere la mia esperienza rispondendo ad un pò di domande……..
● PowerMock serve solo per testare codice di legacy e/o scritto male?
● Quanto il design delle nostre applicazioni dipende da limiti degli strumenti che utilizziamo per i test?
● Come PowerMock influenza le scelte di design?
● Può PowerMock aiutarci nel processo di refactor e come?
● Può mancare uno strumento come PowerMock nella cassetta degli attrezzi di chi pratica la TDD?
PowerMock è la chiave per eliminare il concetto di design testabile.
6. Design testabile e buon design
DESIGN TESTABILE
Il design testabile misura quanto è facile testare il codice, NON se è possibile testarlo.
Regole per isolare in modo semplice e veloce il codice sotto test dal resto del sistema.
● Istanziare una classe.
● Sostituire un’implementazione.
● Simulare differenti scenari.
● Invocare sul S.U.T (System under test) uno specifico flusso di controllo dal codice di test.
BUON DESIGN Spesso un design testabile corrisponde ad un buon design, ma non sempre.
7. Linee guida del design testabile
● Evitare metodi privati complessi, testare solo metodi pubblici.
● Evitare classi e metodi finali, non si possono creare sottotipi.
● Evitare metodi static se richiesta sostituzione in futuro.
● Utilizzare la keyword new con molta attenzione, è alto il rischio di creare accoppiamento.
● Evitare di gestire la logica che potrebbe essere sostituita nel costruttore.
● Evitare di creare singleton, meglio creare componenti con scope singleton.
● Favorire il riuso del codice per composizione, l’ereditarietà ok per polimorfismo, non per il riuso del
codice.
● Creare wrapper sulle librerie di terze parti, niente mocking su codice che non ci appartiene.
Più il codice è difficile da testare e più sarà difficile da modificare al prossimo cambio di requisiti!
8. Effetti collaterali
CLASSI E METODI FINALI
Critica: Una classe dovrebbe essere pensata per essere estesa.
Effetto: Si rinuncia ad un buon design per i limiti del tool di test.
Mockito nella versione 2 ha introdotto la possibilità di creare mock su classi final.
METODI PRIVATI
Critica: In alcuni contesti serve testare o verificare lo stato interno di un componente.
Effetto: Si cambia la visibilità di alcuni metodi per la fase di test.
L’annotazione @VisibleForTesting di Guava è stata creata proprio per documentare questa
violazione dell’incapsulamento.
9. Effetti collaterali
CLASSI E METODI STATICI
Critica: Alcune responsabilità sono statiche di natura e non siamo maghi per prevedere il futuro.
Effetto: Nel dubbio si rinuncia a creare classi statiche anche quando si dovrebbe.
Tante factory non statiche, codice scomodo da gestire ed aumento footprint.
WRAPPER DI LIBRERIE
Critica: E’ costoso fare classi wrapper ogni volta che si deve utilizzare codice che non ci
appartiene.
Effetto: Tendenza a creare molte classi senza valore aggiunto che fanno aumentare la codebase.
Si reinventa la ruota.
11. public class NewUserService
{
private final UserRepository userRepo;
public NewUserService(UserRepository userRepo)
{
this.userRepo = userRepo;
}
public User createNewUser(String name)
{
// Utilizzo metodo statico.
String newRandomTmpPwd = User.newTempPassword(20);
final User user = new User(name, newRandomTmpPwd);
// Collaborazione con il repository.
this.userRepo.saveOrUpdate(user);
return user;
}
}
Metodi statici e classi finali
12. Metodi statici e classi finali
@RunWith(PowerMockRunner.class)
@PrepareForTest( { UserRepository.class, User.class })
public class NewUserServiceTest
{
@Test
public void test()
{
// Fixture.
UserRepository mockUserRepo = PowerMockito.mock(UserRepository.class);
User newUserExpected = new User("massimo", "newPwdForTest");
// Stubbing.
PowerMockito.mockStatic(User.class);
Mockito.when(User.newTempPassword(Mockito.anyInt())).thenReturn("newPwdForTest");
// Run Test.
NewUserService serviceUnderTest = new NewUserService(mockUserRepo);
User newUserResult = serviceUnderTest.createNewUser("massimo");
// Assertion.
Assert.assertEquals(newUserExpected, newUserResult);
// Verification.
Mockito.verify(mockUserRepo, Mockito.times(1)).saveOrUpdate(Mockito.eq(newUserExpected));
}
}
13. Metodi privati
public class PrivateMethodClass
{
public int bigMethod(String argStr, int argInt) throws IOException
{
return this.privateToTest(argStr, argInt) + 10;
}
private int privateToTest(String argStr, int argInt) throws IOException
{
int newArgInt = this.privateToStub(argStr);
this.execute(newArgInt, argInt);
return newArgInt + 10;
}
private int privateToStub(String string)
{
return string.length() + 1000;
}
private void execute(int arg1, int arg2) throws IOException
{
try( BufferedWriter newBufferedWriter = Files.newBufferedWriter(Paths.get("myFile.txt"));)
{
newBufferedWriter.write(String.valueOf(arg1 + arg2)); // side effects.
}
}
}
14. Metodi privati
@RunWith(PowerMockRunner.class)
@PrepareForTest(PrivateMethodClass.class)
public class PrivateMethodExampleTest
{
@Test
public void testPrivateMethod() throws Exception
{
// Fixture.
PrivateMethodClass classUnderTest = PowerMockito.spy(new PrivateMethodClass());
// - No side effects.
Method method = PowerMockito.method(PrivateMethodClass.class, "execute", int.class, int.class);
PowerMockito.suppress(method);
// Stubbing.
PowerMockito.doReturn(20).when(classUnderTest, "privateToStub", "param");
// Run Test.
int result = Whitebox.invokeMethod(classUnderTest, "privateToTest", "param", 10);
// Assertion.
Assert.assertEquals(30, result);
// Verification.
PowerMockito.verifyPrivate(classUnderTest, Mockito.times(1)).invoke("execute", 20, 10);
}
}
15. Difetti di PowerMock
● Non è uno strumento per principianti.
● Si rischia di creare troppe dipendenze implicite.
● Può mandare in crash la JVM solamente per una annotazione messa nel posto sbagliato.
● Stack di difficile comprensione in debug.
● Va in conflitto con altri tools che a loro volta manipolano il bytecode a runtime, come succede per
alcuni framework di code coverage. E’ in corso una migrazione a ByteBuddy per risolvere il problema.
Hinweis der Redaktion
Massimo Groppelli
PowerMock è molto semplice da utilizzare soprattutto per chi già conosce Mockito, infatti il framework non mira a reinventare la ruota ma si integra molto bene tanto è vero che le API sono praticamente uguali a quelle
dei framework di Mocking che estende.
Dire che alcune linee guida sono validissime e vanno rispettate.