Tomáš Piňos
22.3.2011

Proč psát javovské testy v Groovy II



Druhá část blogu o psaní javovských testů v Groovy přinese méně slov a více kódu. 

Témata:



Zkusili jste už psát testy v Groovy? K čemu dalšímu Groovy používáte? Podělte se o své zkušenosti v diskuzi pod článkem.

Deklarace testovacích metod s JUnit

Testovací metody můžeme označit @Test anotací tak, jak to zavedla JUnit 4. Druhou možností je psát testovací třídy jako potomky GroovyTestCase a testovací metody začínat prefixem “test”.

class Test1 {
  @Test void "notification message handling"() {
    Assert.fail() 
  }
}

class Test2 extends GroovyTestCase {
  void test1() {
    fail("notification message not handled")
  }
}

GroovyTestCase dědí z třídy junit.framework.TestCase a přidává pár šikovných drobností navíc (shouldFail).

Implementace rozhranní “mapou”

V Groovy můžeme rozhranní implementovat pomocí closures a map. Pomocí closure:

def accountDao = { new Account(number: “AX001”, balance: 999G) } as AccountDao

Tato closure implementuje všechny metody AccountDao pomocí jednoho kódu (akademický, ale někdy užitečný příklad). Pokud bych chtěl brát v úvahu parametry metod, mohu je v closure deklarovat jako pole – Object[] params. Implementace pomocí mapy:

def accountDao = [ get: { new Account(number: “AX001”, balance: 999G) } ] as AccountDao

Mapa implementuje jen metodu get. Rozhranní takto můžeme implementovat i jen částečně – volání neimplementovaných metod skončí NPE. Je to výborná zkratka pro implementaci různých testovacích stubů (nebo mocků, chcete-li).

Capture

Implementace rozhranní za pomoci map nebo closures může výrazně zjednodušit situace, kde bychom jinak v Javě museli použít např. EasyMock a jeho metodu capture. Jako příklad uvedu zjednodušený test z našeho blogu o testech metod tvořících objekty.

class UserServiceTest {
  @Test public void createUser() {
    def userService = new UserServiceImpl() 
    userService.userDao = { User user -> assert "tomas" == user.username } as UserDao 
    userService.createUser("tomas") 
  }
}

Test ověří, že metoda UserService.createUser správně vytvoří instanci třídy User. Zkuste výsledný kód porovnat s implementací pomocí EasyMocku a capture (převzato z citovaného blogu).

public class UserServiceTest {
  private UserServiceImpl userService;
  private UserDao userDao;
  
  @Before public void setUp() {
    userService = new UserServiceImpl();
    userDao = createMock(UserDao.class);
    userService.setUserDao(userDao); 
  } 
  
  @Test public void createUser() {
    Capture<User> capturedUser = new Capture<User>();
    userDao.save(capture(capturedUser));
    replay(userDao);
    userService.createUser("pavel");
    User user = capturedUser.getValue();
    assertEquals("pavel", user.getUsername());
    verify(userDao);
  }
}

 Java dobrý, Groovy lepší?

Libovolné řetězce jako názvy metod

Pěknou možností je nazývat metody libovolnými řetězci.

@Test void "notification message handling"() { // ... } 

Maven potom zobrazí výsledek běhu testu následujícím způsobem.

Failed tests:
  notification message handling(test.NotificationTest)

shouldFail?

Metody shouldFail třídy GroovyTestCase slouží pro přehledné řešení situací s výjimkami a elegantně obchází obvyklou konstrukci s try – catch blokem.

shouldFail(NullPointerException) {
  new Hashtable()["key"] = null
} 
shouldFail(IllegalArgumentException) { new HashMap(-1) } 

 
A co jsem myslel obvyklou konstrukcí s try – catch blokem?

try {
  Hashtable ht = new Hashtable();
  ht.put("key", null);
  fail();
} catch (NullPointerException e) { // ok }

try {
  new HashMap(-1);
  fail();
} catch (IllegalArgumentException e) { // ok }

 

První příklad testuje vložení null hodnoty pod určitým klíčem do Hashtable – očekáváme, že pokus skončí výjimkou NPE (jinak fail). Podobně v druhém příkladu by vytvoření HashMap velikosti -1 mělo skončit výjimkou IAE.

Testování XML výstupů

Častým předmětem testů je ověření, jestli nějaký XML výstup odpovídá očekávanému tvaru. Groovy nabízí pro parsing XML mimojiné třídu XmlSlurper. Její použití znamená, že nepíšete XPath výrazy, neiterujete přes DOM, ale přistupujete k obsahu XML stále prostředky Groovy jazyka. Příklad z dokumentace:

<records>
  <car name='HSV Maloo' make='Holden' year='2006'>
    <country>Australia</country>
  < record type='speed'>Production Pickup Truck with speed of 271kph</record>
  </car>
  <car name='P50' make='Peel' year='1962'>
    <country>Isle of Man</country>
    <record type='size'>Smallest Street-Legal Car at 99cm wide and 59 kg in weight</record>
  </car>
</records>

Použití třídy XmlSlurper:

def records = new XmlSlurper().parseText(...) 
assert 2 == records.car.size() 
def firstRecord = records.car[0] 
assert 'car' == firstRecord.name() 
assert 'Holden' == firstRecord.@make.text() 
assert 'Australia' == firstRecord.country.text() 

 
Co se čte lépe?
// car[1][@name=’HSV Maloo‘] nebo car[0].@name.text() == “HSV Maloo”?

A další…

Mezi další killer vlastnosti Groovy patří vytváření nových instancí, buildery pro vytváření stromových struktur objektů nebo podpora pro práci s kolekcemi. Pěkně akční český přehled lze nalézt třeba v článku Groovy je žůžo. Groovy má v arzenálu i podporu pro vytváření mock objektů. Standardní přístup mě jako uživatele EasyMocku nepřesvědčil, ale GMock stojí za to. A nezapomínejme na mapy, ty vyřeší mnoho potřeb. I když toho Groovy nabízí hodně a ještě víc, rozhodnout se psát testy v Groovy neznamená opustit skvělou testovací infrastrukturu z Javy. Knihovny jako EasyMock, PowerMock, Hamcrest, … a libovolné jiné můžete stejným způsobem používat dál.

Konfigurace Mavenu

Podporu Groovy začleníme do projektu jednoduše jako závislost na Groovy artefaktu.

<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy</artifactId>
  <version>1.7.5</version>
</dependency>


Pro správný běh testů je ještě třeba nakonfigurovat build plugin pro kompilaci testů v Groovy.

<plugin>
  <groupId>org.codehaus.gmaven</groupId>
  <artifactId>gmaven-plugin</artifactId>
  <version>1.3</version>
  <executions>
    <execution>
      <id>testCompile</id>
      <goals>
        <goal>testCompile</goal>
      </goals>
      <configuration>
        <sources>
          <fileset>
            <directory>${pom.basedir}/src/test/java</directory>
            <includes>
              <include>**/*.groovy</include>
            </includes>¨
          </fileset>
        </sources>
      </configuration>
    </execution>
  </executions>
</plugin>


Příklad popisuje možnost, kdy jsou .groovy zdrojáky ve stejném adresáři jako testy v Javě. Pokud bychom testy umístili do adresáře src/test/groovy, nebylo by třeba v konfiguraci zahrnovat uvedený fileset.

Vaše emailová adresa nebude zveřejněna

Komentáře

Děkujeme za váš komentář
Další
  • specnaz

    "obvyklou konstrukcí s try – catch blokem", tu snad nikdo nepouziva, prece mame: @Test(expected=NullPointerException.class)

  • Luboš Račanský

    to specnaz: Máte pravdu, že existuje konstrukce @Test(expected=NullPointerException.class), kterou samozřejmě používáme. Nicméně try - catch blok bych nezatracoval. Má stále své použití, pokud chcete například testovat message výjimky.