Mockito – ja testataan ihan mitä vain isolaatiossa

Olen joskus aiemmin kirjaillut juttuja siitä miten esim. EJB komponentteja voidaan yksikkötestata – ja käytellyt välineenä EasyMockia. Testaus isolaatiossa, eristyksissä muusta on yksi tavoiteltava asia koska näin tehdyt testit voidaan ajaa salamannopeasti ja jatkuvasti, saaden palautetta muutoksista koko ajan.

Mockito-JUnit

Sanoinko testit? Köhköh, tarkoitin tiestysti speksit. Isolaatiotestit mahdollistavat TDD ja BDD disipliinien soveltamisen haltuessaan, tai itseni suosima Spec-While-Writing tapa joka on oikeastaan ihan samaa kuin em mutta rennommin rantein. Fanaattiset isolaatiotestithän eivät nimenomaan ole testeinä kovinkaan hyviä usein, mutta spekseinä ne ovat aivan verrattomia, ja monimutkaiselle logiikalle lähes ilmaiseksi sivuvaikutuksena syntyvät testitkään eivät ole ihan huono asia.

Sanoinko ilmaiseksi? Hups. Kyllähän testien kirjoittamiseen aikaa kuluu, etenkin jos niitä säveltää etupainotteisesti ennen koodipyrähdyksiä. Etenkin isolaation saavuttaminen on aika ajoin hankalaa. Viime aikoina olen löytänyt uutta apua tälle saralle: Kuten otsikko jo spoilasi, Mockito framework on varsin kiva, etenkin Spring Bootin kanssa. Vastaavaan tapaan Javascript-puolella käyttelen nykyään Jasmine mockeja, mutta tämä ei ole artikkeli niistä vaan Mockitosta. Tässä siis vähän maistiaisia siitä miltä maistuu yksikkötestaus mockitolla ja mitä hyöytyä siitä saa.

Mockito testi voi olla tavallinen JUnit tai TestNG testi, eli aloitellaan tähän tapaan:

public class UserResourceTest {

  @Test
  public void changePasswordShouldSucceedWithValidParameters() 
    throws Exception {
      fail("Not implemented yet");
  }

}

Testatessa on tietysti mukavaa olla se mitä testataan. Perinteisesti sen voi vaikka instansioida nimellä sut – system under test, Mockitolla homma hoituu näin:

@InjectMocks
private UserResource sut;

Jotta tuo tekee jotain on tietysti syytä ajaa tämä Mockiton kera. Lisätäänpä siis testiluokan alkuun:

@RunWith(MockitoJUnitRunner.class)

Nyt tapahtuu jo jotain. Tässä testattavassa luokassa on kuitenkin paljon riippuvuuksia, jäsenmuuttujissa on yhtä ja toista jota Spring injektoi ajon aikana, mutta jotka aiheuttavat nullpointer exceptioneitä ellei niitä ole alustettu. Olen joskus injektoinut näitä riippuvuuksia reflectionin avulla, joskus setXXX-metodeita tekemällä. Mockitolla homma sujuu kokonaisuudessaan näin:

@RunWith(MockitoJUnitRunner.class)
public class UserResourceTest {

    @Mock
    private Utils utils;

    @Mock
    private UserDetailsService userDetailsServiceMock;

    @InjectMocks
    private UserResource sut;

Eli, kun alan kirjoittelemaan omaa testiäni, kaikki kolme edellämainittua on instansioitu Mockiton toimesta, mock objekteille on mock toteutukset, jotka esim. palauttavat null/eivät tee mitään. Ja mikä parasta, mock objektit on injektoitu sut objektin sisään, privaatteihin jäsenmuuttuja-kenttiin. Ensin yritetään samalla tyypillä, sitten samalla nimellä.

Mockiton oma dokumentaatio InjectMocks-metodista sanoo näin:

Mockito will try to inject mocks only either by constructor injection, setter injection, or property injection in order and as described below. If any of the following strategy fail, then Mockito won’t report failure; i.e. you will have to provide dependencies yourself.

Eli tarkkana tämän kanssa.

Mitä muuta hienoa? No Mockitossa on verrattain helppoa opettaa mockit palauttamaan sopivia arvoja, esim:

UserEntity currentUser = new UserEntity();
UserAuthentication userAuthentication = new UserAuthentication(currentUser);
when(utils.getAuthentication()).thenReturn(userAuthentication);

Ja sitten vain paukuttamaan omaa suttia, joka taas kutsuu tuota utils.getAuthentication() metodia. Jos haluat tarkistaa, kutsuiko, sen voi tarkistaa verify-kutsulla:

verify(utils).getAuthentication();

Eli, Mockito vaikuttaisi hyvältä. Pääidea on tuoda testauksen hintaa alas, ja näin saada siitä yhtä luontevaa kuin hengittäminen. Tai luontevampaa.

 

Mainokset

Testauksesta ja Tomcatistä

No niin, tämä viikko sujuu sitten kiireen merkeissä joten ei ole paljoa uutta kirjoiteltavaa.. Mutta muutama asia on silti mielessä.

 

Olen miettinyt paljon yksikkötestauksen käyttöä ohjelmistokehityksessä Test Driven tapaan. Äärimmäisen harva sovelluskehitystiimi voi kerskua sillä että tekee TDD tapaan 100% kattavuudella eli yksikkötestaa tietokantakerroksen, liiketoimintakerroksen ja web käyttöliittymäkerroksen eristetyin testein joita on nopea ajaa, ja jotka kehitetään Test First periaattein, siten että testit toimivat suunnitteluvälineenä. Ei ole mitään estettä miksei tätä voisi teknisesti tehdä, mutta se on vain työlästä kun se tehdään oikein. Sensijaan integraatiotestit jotka testaavat moduulia ympäristössään ja ovat hitaampia ajaa, ovat myös halvempia tehdä ajankäytöllisesti, ja niitä voi rakentaa samoilla yksikkötestausvälineillä (JUnit ja Mockito ovat kovia tässä).

Oli miten oli, TDD ajatuksessa on kuitenkin jotain filosofisesti hyvin puhuttavaa. Testattava koodi on avointa ja tasarakenteista koodia, ja miten voisi paremmin tehdä funktion kuin ensin miettimäll ämistä tietää että se toimii? Ja mikä parasta, nimeämällä testit fiksusti testikoodi on melkeinpä luettavaa speksiä. Mockitoa koodatessakin käytännössä kirjoitetaan speksejä englanniksi – mutta näitä speksejä voi nappia painamalla tarkistaa milloin vain haluaa.

Ja tästä päästiinkin viikon linkkivinkkiin, eli http://www.dzone.com/links/r/10_steps_to_more_readable_tests.html

Toinen huomio: Tomcat 7 on saavuttanut enemmän tai vähemmän vakaan tason ja päässyt ulos betasta. Tomcat taitaa olla edelleenkin suosituimpia alustoja joissa java web sovelluksia ajetaan (huomioiden että JBOSS ajaa myös Tomcatiä), ja kun uusin versio on jälleen uusi Java EE 6 alusta, se herättää heti mielenkiinnon. Yksi muutos on Servlet 3.0 tuki – hyvästit XML:lle jos niin haluat. Tervetuloa asynkroniset kutsut. Lopulta Tomcat 7:ssä on myös korjattu aiempia muistivuoto-oireita, eli se tarjoaa kiinnostavan alustan testata myös vanhoille sovelluksille. EJB Lite ei valitettavasti onnistu vieläkään – mutta sitä varten on JBOSS 6…

 

EJB 3 Yksikkötestauksesta 3/3

Artikkelisarjan kahdessa ensimmäisessä osassa käsiteltiin EJB 3 ja JUnit yhteiskäyttöä. Todettiin että on pääosin kolme strategiaa millä EJB komponentteja voi testata:

1) Testataan niitä EJB säiliössä. Testikoodi toimii tavallaan clientin roolissa ja kutsuu ejb fasadin palveluita. Säiliö toimittaa tietokantaresurssit, transaktiot ja muut. Tämä ei ole kovin aitoa yksikkötestausta tai varsinkaan TDD:tä mutta voi olla hyödyllistä testausta silti.

2) Testataan EJB 3 komponentteja ilman säiliötä, tavallisina java-luokkina. Tämä on helppoa triviaaleilla EJB komponenteilla mutta koska niiden logiikka usein kaipaa taustalleen resursseja kuten JNDI, Tietokantapalvelimet, Connection Poolit, transaktiopalvelimet, sanomapalvelimet, JCA connectorit, jne, niitä voi olla vaikeaa testata eristyksessä. Ratkaisuna on pystyttää testaukseen dedikoitu muistitietokanta tms resurssi, joka antaa mahdollisuuden testata businesslogiikan ja pysyvyysratkaisun yhteistoimintaa. Tämä vaatii sen, että kytkös kantaan tai sanomapalvelimeen on helppo vaihtaa, esim. Dependency Injection patternilla. Tämäkään ei ole aivan puhdasta yksikkötestausta mutta voi olla höydyllistä testausta ja on ilman muuta tehtävissä.

3) Puhtaan Test Driven lähestymistavan puitteissa haluamme keskittyä testaamaan EJB:n varsinaista business logiikkaa, ei kannan toimintaa. Lisäksi testien pitäisi olla vakioituja olosuhteiltaan, ja salamannopeita suorittaa (jotta niitä voidaan ajaa toistuvasti ja jatkuvasti, kenen hyvänsä työasemassa). Tämä on mahdollista jos voimme korvata taustaresurssit sopivalla Mock frameworkillä. Tässä mallissa on varjopuolena että Mock objektit pitää ensin ’kouluttaa’ palauttamaan oikeat, odotetut arvot. Toisaalta tämä koulutusvaihe samalla speksaa mitä odotamme sovelluksen tekevän, eli palvelee dokumentoinnin tehtävää.

Tässä artikkelissa esittelen suositun Mock frameworkin Mockiton käyttöä. Testauksen kohteena on jo sarjan 2. osassa esitelty BankFacade SessionBean, joka näyttää siis tältä:

@Stateless
public class BankFacadeBean implements BankFacadeLocal {

@PersistenceContext
private EntityManager em;

public EntityManager getEm() {
return em;
}

public void setEm(EntityManager em) {
this.em = em;
}

public void updateAccount(Account a) {
em.merge(a);
}

public void createAccount(Account a) {
em.persist(a);
}

public Account getAccount(Long id) {
return em.find(Account.class, id);
}

}

Yksinkertainen Bean, jolla tulee ajon aikana olemaan EntityManager injektoituna jäsenmuuttujaan. Meidän testimme kannalta sellaista ei kuitenkaan ole – joten mockaamme sellaisen. Tässä on Mockito frameworkiä käyttävän JUnit yksikkötestin luokkamääritys ja setUp-metodi:

… snip …

import static org.mockito.Mockito.*;

public class BankFacadeBeanMockTest extends TestCase {

private static Logger logger = Logger.getLogger(BankFacadeBeanMockTest.class.getName());
private EntityManager em;

public static final Account ACCOUNT = new Account();
public static final double BALANCE=10000.0;

public BankFacadeBeanMockTest(String testName) {
super(testName);
}
private BankFacadeBean instance;

@Override
protected void setUp() throws Exception {
super.setUp();

ACCOUNT.setBalance(BALANCE); // initialize account
ACCOUNT.setId(Long.valueOf(1));

// Set up a MOCK EntityManager here
em = mock(EntityManager.class);

// teach mock how to do its things

// make sure ACCOUNT is in EntityManager context
when(em.contains(ACCOUNT)).thenReturn(Boolean.TRUE);

// make sure we can find account
when(em.find(Account.class, Long.valueOf(1))).thenReturn(ACCOUNT);

instance = new BankFacadeBean();
instance.setEm(em);
}

.. snip …

Kävikö selväksi? Importoimme staattisen Mockito objektin kaikki metodit. Loimme testiä varten vakioidun tilin jossa tilinumero ja saldo pysyy samana (refaktoroinnille olisi kyllä jo käyttöä..). mock()-metodilla loimme EntityManager rajapinnasta mock objektin. Mock objektin kolmea metodia tullaan testeissä kutsumaan, joten varmistamme että ne palauttavat odotetut arvot:

1) contains() palauttaa true oikealle tilille – ja väittää näin objektin löytyvän kannasta

2) find() palauttaa luomamme tilin – aivan kuin se olisi haettu kannasta.

Lopuksi injektoimme mockatun EntityManager objektin pankki-fasadille, ja alamme testailemaan sitä. Tässä testirutiini:

public void testPersistence() {
System.out.println(”updateAccount”);

try {
// find existing account from db

// update account data

//a.setId(Long.valueOf(1000)); // don’t set id, since it’s autoid

instance.createAccount(ACCOUNT);
Long id = ACCOUNT.getId();
System.out.println(”Created with account id ”+ id);

// em should know Account a now
assertTrue(em.contains(ACCOUNT));
// Verify that our account exists and has proper balance
Account b = instance.getAccount(id);
assertEquals(BALANCE,b.getBalance());
} catch (Exception ex) {
ex.printStackTrace();
fail(”Exception during testPersistence”);
}
}

Mockito Unit Test

Mockito yksikkötestaus EJB 3.0 komponenteille on helppoa ja salamannopeaa

Testi ei siis vaadi EJB säiliötä, tai tietokantaa, tai muutakaan resurssia, testaamme Java-luokkaa pelkkänä POJO:na. Tällaista testiä voit ajaa salamannopeasti uudelleen, ja uudelleen, ja uudelleen aina muutoksia tehtäessä. Se ei mittaa tietokannan tai muiden taustajärjestelmien toimintaa, mutta varmentaa ja dokumentoi mitä odotamme niiden osien tekevän. Huomaa että tämä tekee refaktoroinnista varmempaa ja auttaa myös sovelluksen suunnittelussa.

Jos tämä herätti kiinnostuksen, kannattaa seurata myös dirty-mockito projektia (http://code.google.com/p/dirty-mockito/wiki/JpaSupport), tässä maistiainen siitä:

public class WidgetDaoTest extends ActiveTest<WidgetDao> {

@Mock
private EntityManager em;

@Mock
private Query query;

private WidgetDao widgetDao;

@Test
public void testFindByName() {
Widget widget = new Widget();

when(em.createNamedQuery(”WidgetDao.findByName”)).thenReturn(query);
when(query.getResultList()).thenReturn(Collections.singletonList(widget));

assertSame(widget, widgetDao.findByName(”Bob”));

verify(query).setParameter(”name”, ”Bob”);
}

}

Jos olet utelias, Maven pom.xml tiedosto tälle projektille näyttää tältä:

<project xmlns=”http://maven.apache.org/POM/4.0.0” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance
xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd”>
<modelVersion>4.0.0</modelVersion>
<groupId>fi.tieturi</groupId>
<artifactId>EJB3UnitTestDemo</artifactId>
<packaging>ejb</packaging>
<version>1.0-SNAPSHOT</version>
<name>EJB3MockUnitTestDemo JEE5 EJB (ejb)</name>
<url>http://maven.apache.org</url&gt;
<dependencies>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>

<!– NOTE: This was needed as fix for Absent Code error that you get –>
<!– Put this dependency BEFORE javaee apis so you have persistence implementation available –>
<dependency>
<groupId>net.java.dev.glassfish</groupId>
<artifactId>glassfish-persistence-api</artifactId>
<version>b32g</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javaee</groupId>
<artifactId>javaee-api</artifactId>
<version>5</version>
<scope>provided</scope>
</dependency>

<!– for Mockito mock framework –>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.2</version>
<scope>test</scope>
</dependency>

</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.1</version>
<configuration>
<ejbVersion>3.0</ejbVersion>
</configuration>
</plugin>

</plugins>
<finalName>EJB3UnitTestDemo</finalName>
</build>
</project>