Pokazywanie postów oznaczonych etykietą JUnit. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą JUnit. Pokaż wszystkie posty

poniedziałek, 1 września 2014

Implementacja Singleton’a w Javie

Ostatnio, podczas z jednej z rozmów rekrutacyjnych, dostałem pytanie, "A jak by Pan zaimplementował singleton w Javie?". Pytanie dość standardowe, mógł by je zadać każdy. Odpowiadam więc: „Klasa, z prywatnym konstruktorem, z polem statycznym o typie zadeklarowanej klasy, metoda getInstnce(), itd.”. Rekruter na to: "No dobra, a jakiś inny pomysł?".

Na szybko nie przychodziło mi jednak nic do głowy... Wtedy padło pytanie, które zmotywowało mnie do przygotowania tego wpisu: "A czytał Pan Effective Java, Joshua Bloch?". A no nie czytałem i nie sądzę, abym ją przeczytał.

Dlaczego?

Przede wszystkim uważam że ta książka jest trochę przestarzałą. Drugie wydanie pochodzi z 2008 roku i informacje w niej zawarte są trochę nieaktualne. To nie książka o wzorcach projektowych Bandy Czworga, która nie traci na swojej aktualności, a jedynie pozycja o tym jak coś tam dobrze zrobić w Javie. I tak przykładowo rozdział: "Item 51 Beware the performance of string concatenation", traktujące o tym, aby lepiej używać StringBuilder’a niż + do sklejania tekstów, jest już dawno nieaktualne! Chciałem kiedyś pisać posta o tym, ale na stackoverflow i jeszcze gdzieś tam już dawno o tym było. Nie wiem tylko od kiedy dokładnie istnieje ten mechanizm zamiany + na StringBuilder’a w Javie.

O innych dobrych praktykach, można się dowiedzieć zawsze z innych źródeł, niekoniecznie czytając wspomnianą książkę.

Dobra wróćmy do tematu wpisu. W innym rozdziale Effective Java (Item 3: Enforce the singleton property with a private constructor or an enum type), jest zalecenie, aby Singletona implementować za pomocą Enuma. Z tym rozwiązaniem spotkałem się po raz pierwszy podczas review kodu kogoś, kto czytał tę książkę. Użycie wówczas enuma w roli singletonu było dla mnie zupełnie niezrozumiałe! Musiałem dopytać autora o co chodzi.

Dlaczego nie lubię tego rozwiązania? Spójrzmy na przykładowy kawałek kodu (inspirowany Joshuą Blochem, co bym za dużo nie musiał wymyślać, jak tu mądrze użyć singletona). Kod jest i tak kiepski (obliczanie czasu), ale chodzi mi o zaprezentowanie działania omawianego wzorca.
public enum Elvis {
    INSTANCE;

    private final int ELVIS_BIRTHDAY_YEAR = 1935;

    public int howOldIsElvisNow() {
        return new GregorianCalendar().get(Calendar.YEAR) - ELVIS_BIRTHDAY_YEAR;
    }
}

public class ElvisProfitService {

    public double ELVIS_SALARY_YEAR = 70_000;

    public double calculateElvisProfit() {
        return Elvis.INSTANCE.howOldIsElvisNow() * ELVIS_SALARY_YEAR;
    }
}

No i weź tu panie przetestuj taki kod! Możemy jeszcze Elvisa statycznie zaimportować, to linijka 6 skróci się do jeszcze mniej czytelnej formy.

return INSTANCE.howOldIsElvisNow() * ELVIS_SALARY_YEAR;

Ktoś ma jakieś pomysły jak taki kod przetestować? Da się oczywiście, z wykorzystaniem PowerMock’a [link do rozwiązania na końcu ^1], ale chyba nie o to chodzi aby pisać nietestowany kod?

Dlaczego wolę starszą wersję tegoż rozwiązania:

public class Elvis {

    private static final Elvis INSTANCE = new Elvis();

    private final int ELVIS_BIRTHDAY_YEAR = 1935;

    private Elvis() {
    }

    public static Elvis getInstance() {
        return INSTANCE;
    }

    public int howOldIsElvisNow() {
        return new GregorianCalendar().get(Calendar.YEAR) - ELVIS_BIRTHDAY_YEAR;
    }
}

Dochodzi prywatny konstruktor, statyczna metoda getInstance() i inicjalizacja pola klasy wraz z deklaracją.

W tym przypadku kod korzystający z tego singletonu, mógłby być następujący:

public class ElvisProfitService {

    private final double ELVIS_SALARY_YEAR = 70_000;
    private Elvis elvis = Elvis.getInstance();

    public double calculateElvisProfit() {
        return elvis.howOldIsElvisNow() * ELVIS_SALARY_YEAR;
    }

    // For tests
    void setElvis(Elvis elvis) {
        this.elvis = elvis;
    }
}

W linii 4 wywołałem getInstance(), aby w przykładowym kodzie produkcyjnym było wszystko cacy. Dzięki temu, zależność ta jest definiowana jako pole w kasie i mamy setter do tego, więc możemy sobie bardzo ładnie przetestować tą funkcjonalność, bez hackowania z PowerMockiem:

public class ElvisProfitServiceTest {

    @Test
    public void shouldCalculateElvisProfit() {
        // given
        ElvisProfitService service = new ElvisProfitService();
        Elvis elvis = mock(Elvis.class);
        when(elvis.howOldIsElvisNow()).thenReturn(1);
        service.setElvis(elvis);

        // when
        double elvisProfit = service.calculateElvisProfit();

        // then
        assertEquals(70_000, elvisProfit, 0.1);
    }
}

W sekcji given mamy bardzo ładnie zdefiniowane zachowanie, za pomocą Mockito, jakie ma przyjmować masz singleton na potrzeby tego testu.

A Ty jak definiujesz (o ile to robisz) swoje singletony? Które rozwiązanie uważasz za lepsze?



[1] Co do rozwiązania zagadki, jak przetestować Singletona jako Enum’a, to tutaj jest odpowiednia rewizja na github’ie: SingletonInJava e714fb a kod poniżej:

@RunWith(PowerMockRunner.class)
@MockPolicy(ElvisMockPolicy.class)
public class ElvisProfitServiceTest {

    @Test
    public void shouldCalculateElvisProfit() {
        // given
        ElvisProfitService service = new ElvisProfitService();

        // when
        double elvisProfit = service.calculateElvisProfit();

        // then
        assertEquals(70_000, elvisProfit, 0.1);
    }
}

public class ElvisMockPolicy implements PowerMockPolicy {

    @Override
    public void applyClassLoadingPolicy(MockPolicyClassLoadingSettings settings) {
        settings.addFullyQualifiedNamesOfClassesToLoadByMockClassloader("com.blogspot.mstachniuk.singletoninjava.Elvis");
    }

    @Override
    public void applyInterceptionPolicy(MockPolicyInterceptionSettings settings) {
        Method method = Whitebox.getMethod(Elvis.class, "howOldIsElvisNow");
        settings.proxyMethod(method, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return 1.0;
            }
        });
    }
}

niedziela, 17 lutego 2013

Porównanie tradycyjnych asercji, Hamcrest'a i FEST'a

Po tygodniowym szaleństwie w Alpach i niewielkim odmrożeniu, czas wrócić do pracy aby odpocząć ;) Temat może nie będzie zbytnio odkrywczy, ale akurat prezentowałem kolegom w projekcie jak działa FEST Asserts 2.X, więc niewielkim nakładem pracy można coś na blogu o tym skrobnąć. Jak ktoś już zna i używa, to nie musi dalej czytać. A kto nie zna jeszcze FEST’a to niech się przyjrzy przykładom przygotowanym przeze mnie i upiększy swoje nowe testy.

O wyższości FEST’a nad Hamcrest'em słyszałem po raz pierwszy na prezentacji Bartosza Bańkowskiego i Szczepana Fabera pt. Pokochaj swoje testy na Wroc JUG. I to już było wiadome w 2009 roku. W międzyczasie Hamcrest został wciągnięty do JUnit'a i Mockito, ale to z innych względów. A tym czasem FEST Asserts został trochę odświeżony i podbito jego numerek wersji. Dalej jest bardzo dobry i pozwala lepiej definiować fachowe asercje.

Czas na przykład. Jest on całkiem życiowy, aczkolwiek domena problemu została zmieniona i trochę uproszczona dla przejrzystości przykładu. Ja po zobaczeniu podobnego kodu u siebie w projekcie, podczas review, gdzie pewien proces zwracał sporo skomplikowanych wyników, stwierdziłem, że muszę natychmiast zastosować FEST’a. Trochę nawijki z architektem i dostałem zielone światło. Test w pierwszej, typowej JUnit’owej formie poniżej:

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem solarSystem = new SolarSystem();

 // when
 Set<Planet> innerPlanets = solarSystem.getInnerPlanets();

 // then
 assertEquals(4, innerPlanets.size());
 for (Planet innerPlanet : innerPlanets) {
  if (innerPlanet.getName().equals("Mercury")) {
   List<Gases> mercuryGases = asList(Gases.OXYGEN, Gases.SODIUM, Gases.HYDROGEN);
   assertPlanet(innerPlanet, RotationDirection.LEFT, 4_879_400, 87.96935, 3.701, mercuryGases);
  } else if (innerPlanet.getName().equals("Venus")) {
   List<Gases> venusGases = asList(Gases.CARBON_DIOXIDE, Gases.NITROGEN);
   assertPlanet(innerPlanet, RotationDirection.RIGHT, 12_103_700, 224.700_96, 8.87, venusGases);
  } else if (innerPlanet.getName().equals("Earth")) {
   List<Gases> earthGases = asList(Gases.NITROGEN, Gases.OXYGEN);
   assertPlanet(innerPlanet, RotationDirection.LEFT, 12_756_273, 365.256_363_004, 9.806_65, earthGases);
  } else if (innerPlanet.getName().equals("Mars")) {
   List<Gases> marsGases = asList(Gases.CARBON_DIOXIDE, Gases.NITROGEN);
   assertPlanet(innerPlanet, RotationDirection.LEFT, 6_804_900, 686.960_1, 3.69, marsGases);
  } else {
   throw new AssertionError("Undefined Planet Name: " + innerPlanet.getName()
     + " in result\n" + innerPlanet.toString());
  }
 }
}

private void assertPlanet(Planet planet, RotationDirection direction, long diameterInMeter,
        double yearInEarthDays, double acceleration, List<Gases> atmosphereGases) {
 assertEquals(direction, planet.getRotationDirection());
 assertEquals(diameterInMeter, planet.getDiameter());
 assertEquals(yearInEarthDays, planet.getSiderealYear().inEarthDays(), 0.01);
 assertEquals(acceleration, planet.getAcceleration(), 0.01);
 for (Gases gas : atmosphereGases) {
  assertTrue("Planet " + planet.getName() + " doesn't contains " + gas,
    planet.getAtmosphereGases().contains(gas));
 }
}  

Testujemy metodę generującą zbiór planet wewnętrznych, czyli 4ch pierwszych z naszego układu słonecznego. Pierwsza asercja w linii 10 sprawdza, czy metoda zwróciła odpowiednią ilość planet. Co się stanie, gdy w wyniku będzie nieprawidłowa ilość elementów? Będziemy o tym wiedzieć, ale co dokładnie jest, a czego nie ma, nie będzie wyszczególnione w komunikacie błędów zepsutego testu. Można do pracy wtedy za prząść debugera, ale ja jego nienawidzę.

Kolejny problem to konieczność odszukania właściwego obiektu. Nasza testowana metoda zwraca zbiór planet, przez co nie możemy być pewni, co do kolejności obiektów w wyniku. Dzięki temu dostaliśmy brzydki blok if-else. W takim każdym if’ie definiowana jest najpierw lista gazów, jakie ma zawierać dana planeta. Można by było to co prawda zrobić inline w wywołaniu assertPlanet(), ale wtedy w ogóle łańcuszek argumentów byłby nieczytelny.

Drugim elementem każdego if’a jest właśnie wywołanie assertPlanet() z jakimiś argumentami. Co one oznaczają? Trzeba skoczyć do ciała metody i sprawdzić. Jaka jest ich kolejność? Przy takiej ich liczbie na pewno jej nie zapamiętamy na dłużej niż 5 minut. Można ewentualnie zrezygnować z metody assertPlanet() i ją zinline’nować, ale wtedy mamy sporo duplikacji w kodzie, niskiej czytelności i słabej reużywalności.

Czas na wersję tego samego kodu z Hamcrestem.

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem solarSystem = new SolarSystem();

 // when
 Set<Planet> innerPlanets = solarSystem.getInnerPlanets();

 // then
 assertThat(innerPlanets, hasItem(withName("Mercury")));
 Planet mercury = findPlanetByName(innerPlanets, "Mercury");
 assertThat(mercury, is(rotation(RotationDirection.LEFT)));
 assertThat(mercury, is(diameterInMeter(4_879_400)));
 assertThat(mercury, is(yearLongInEarthDays(87.96935)));
 assertThat(mercury, is(acceleration(3.701)));
 assertThat(mercury, hasGas(Gases.OXYGEN));
 assertThat(mercury, hasGas(Gases.SODIUM));
 assertThat(mercury, hasGas(Gases.HYDROGEN));

 assertThat(innerPlanets, hasItem(withName("Venus")));
 Planet venus = findPlanetByName(innerPlanets, "Venus");
 assertThat(venus, is(rotation(RotationDirection.RIGHT)));
 assertThat(venus, is(diameterInMeter(12_103_700)));
 assertThat(venus, is(yearLongInEarthDays(224.700_96)));
 assertThat(venus, is(acceleration(8.87)));
 assertThat(venus, hasOnlyGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN));

 assertThat(innerPlanets, hasItem(withName("Earth")));
 Planet earth = findPlanetByName(innerPlanets, "Earth");
 assertThat(earth, is(rotation(RotationDirection.LEFT)));
 assertThat(earth, is(diameterInMeter(12_756_273)));
 assertThat(earth, is(yearLongInEarthDays(365.256_363_004)));
 assertThat(earth, is(acceleration(9.806_65)));
 assertThat(earth, hasGases(Gases.NITROGEN, Gases.OXYGEN));
 assertThat(earth, hasNotGases(Gases.SODIUM));

 assertThat(innerPlanets, hasItem(withName("Mars")));
 Planet mars = findPlanetByName(innerPlanets, "Mars");
 assertThat(mars, is(rotation(RotationDirection.LEFT)));
 assertThat(mars, is(diameterInMeter(6_804_900)));
 assertThat(mars, is(yearLongInEarthDays(686.960_1)));
 assertThat(mars, is(acceleration(3.69)));
 assertThat(mars, hasGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN));
 assertThat(mars, hasNotGases(Gases.OXYGEN));

 assertThat(innerPlanets, hasSize(4));
}


Kod wygląd już bardziej czytelnie, ale musiałem do tego dopisać 8 klas Matcher’ów. Właściwie na każdą asercję potrzeba oddzielnego Matcher’a. Mimo wszystko nie pozbyliśmy się problemu wyszukiwania potrzebnego obiektu, co widać w linii 11. Może jest jakieś lepsze obejście na to, ale ja nie znalazłem. Na koniec testu, warto jeszcze sprawdzić, czy testowany zbiór posiada żądaną ilość elementów. Jednak tutaj i tak ewentualny komunikat błędu będzie mało czytelny.

Dobra czas na gwiazdę wieczoru, czyli na test napisany z wykorzystaniem FEST’a.

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem service = new SolarSystem();

 // when
 Set<Planet> innerPlanets = service.getInnerPlanets();

 // then
 assertThat(innerPlanets)
   .containsPlanetWithName3("Mercury")
   .withRotation(RotationDirection.LEFT)
   .withDiameterInMeter(4_879_400)
   .withYearInEarthDays(87.96935)
   .withAcceleration(3.701)
   .withGas(Gases.OXYGEN)
   .withGas(Gases.SODIUM)
   .withGas(Gases.HYDROGEN);

 assertThat(innerPlanets)
   .containsPlanetWithName3("Venus")
   .withRotation(RotationDirection.RIGHT)
   .withDiameterInMeter(12_103_700)
   .withYearInEarthDays(224.700_96)
   .withAcceleration(8.87)
   .containsOnlyGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN);

 assertThat(innerPlanets)
   .containsPlanetWithName3("Earth")
   .withRotation(RotationDirection.LEFT)
   .withDiameterInMeter(12_756_273)
   .withYearInEarthDays(365.256_363_004)
   .withAcceleration(9.806_65)
   .containsGases(Gases.NITROGEN, Gases.OXYGEN)
   .doesNotContainGases(Gases.SODIUM);

 assertThat(innerPlanets)
   .containsPlanetWithName3("Mars")
   .withRotation(RotationDirection.LEFT)
   .withDiameterInMeter(6_804_900)
   .withYearInEarthDays(686.960_1)
   .withAcceleration(3.69)
   .containsGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN)
   .doesNotContainGases(Gases.OXYGEN);

 assertThat(innerPlanets)
   .containsOnlyPlanets("Mercury", "Venus", "Earth", "Mars");
} 

Początek testu jest taki jak w poprzednich przypadkach, ale od sekcji  //then  robi się ciekawie. Widać wyraźnie 4 logicznie oddzielone od siebie bloki, po jednym na sprawdzenie każdej z planet. Można też łatwo zauważyć łańcuch wywołań, który powinien nam być przynajmniej ze wzorca Bulder znany, a który można ogólniej zdefiniować, jako fluent interface. Hamcrest stara się również iść w tym kierunku, ale jak dla mnie te zapis jest bardziej zwięzły i przejrzysty. Mamy po prostu nazwę właściwości, którą weryfikujemy, konkretną wartość i zazwyczaj jakiś prefix w nazwie metody. Ja preferuję dla kolekcji prefix contains, a dla konkretnych właściwości with lub has. Ale jest to już kwestia umowna, co komu lepiej leży.

Jak więc tworzyć takie asercje?

public class PlanetSetAssert extends AbstractAssert<PlanetSetAssert, Set<Planet>> {

    private PlanetSetAssert(Set<Planet> actual) {
        super(actual, PlanetSetAssert.class);
    }

    public static PlanetSetAssert assertThat(Set<Planet> actual) {
        Assertions.assertThat(actual)
                .isNotNull();
        return new PlanetSetAssert(actual);
    }

    public PlanetSetAssert containsPlanetWithName(String expectedPlanetName) {
        for (Planet planet : actual) {
            if(planet.getName().equals(expectedPlanetName)) {
                return this;
            }
        }
        throw new AssertionError("Actual Set doesn't contains Planet with name: " + expectedPlanetName);
    } 

Aby stworzyć własną, fachową asercję, należy rozszerzyć klasę AbstractAssert. Ta przyjmuje za dwa typy generyczne kolejno samą siebie i typ który będziemy obsługiwać, w tym przypadku Set<Planet>. Później definiujemy konstruktor, który jako argument bierze badany obiekt. Konstruktora nie będziemy wywoływać poza obrębem tej klasy, więc można go uczynić prywatnym. Korzystać natomiast będziemy ze statycznej metody fabrykującej assertThat(), która to jako jedyna ze zdefiniowanego wcześniej konstruktora korzysta. Ja preferuję w tym miejscu od razu sprawdzić, czy przekazany obiekt nie jest null’em. Oficjalny tutoral (swoją drogą bardzo dobry, z licznymi komentarzami) preferuje wykonywać to sprawdzenie w każdej metodzie, ale jak dla mnie to jest niepotrzebna redundancja.

Na koniec jest przykładowa implementacja metody, sprawdzającej, czy w danym zbiorze znajduje się planeta o podanej nazwie. W tym przypadku metoda zwraca this, co pozwala wywoływać łańcuchowo dalsze asercje na zbiorze planet. Minusem takiego podejścia jest konieczność definiowania własnego komunikatu błędu, w przypadku niepowodzenia – linia 19. W przypadku gdy zależy nam na sprawdzeniu, czy dany zbiór zawiera szukany element, można tą samą funkcjonalność zaimplementować troszkę inaczej.

public PlanetSetAssert containsPlanetWithName2(String expectedPlanetName) {
 Planet expectedPlanet = new Planet(expectedPlanetName);

 Assertions.assertThat(actual)
   .usingElementComparator(new PlanetNameComparator())
   .contains(expectedPlanet);
 return this;
}

private class PlanetNameComparator implements Comparator<Planet> {
 @Override
 public int compare(Planet p1, Planet p2) {
  return p1.getName().compareTo(p2.getName());
 }
} 


W tym przypadku, za pomocą metody usingElementComparator(), podmieniamy sposób porównywania elementów w kolekcji. Bardzo użyteczny mechanizm. Nadpisujemy tylko typowego porównywacza (komparatora) i gotowe. I co fajniejsze, to dostajemy bardzo przejrzysty komunikat błędu:

java.lang.AssertionError: expecting:
<[Venus, Earth, Mars]>
 to contain:
<[Mercury]>
 but could not find:
<[Mercury]>
 according to 'PlanetNameComparator' comparator
 at com.blogspot.mstachniuk.hamcrestvsfest.fest.PlanetSetAssert.containsPlanetWithName2(PlanetSetAssert.java:34)
  at com.blogspot.mstachniuk.hamcrestvsfest.fest.SolarSystemFestTest.shouldGenerateInnerPlanets(SolarSystemFestTest.java:24)

Właśnie przejrzyste komunikaty błędów (zwłaszcza wśród kolekcji) to siła tego narzędzia.

Teraz czas na moją ulubioną implementację tego typu metody, która to pozwala nam budować jeszcze bardziej płynące interfejsy:

public PlanetAssert containsPlanetWithName3(String expectedPlanetName) {
 Planet expectedPlanet = new Planet(expectedPlanetName);

 PlanetNameComparator comparator = new PlanetNameComparator();
 Assertions.assertThat(actual)
   .usingElementComparator(comparator)
   .contains(expectedPlanet);

 for (Planet planet : actual) {
  if(comparator.compare(planet, expectedPlanet) == 0) {
   return PlanetAssert.assertThat(planet);
  }
 }
 return null;
} 

Początek metody jest analogiczny jak w poprzednim przykładzie. Natomiast zamiast zwracać obiekt this, wyszukuję żądany element i zwracam już asercję, dla tej znalezionej planety. Dzięki temu mogę już sprawdzić inne jej właściwości. Właściwie, mógłbym w pętli wywołać zwykłą metodę equals() do wyszukania elementu, ale skoro mam już napisanego komparatora…

Wrcając jeszcze to całego kodu z listingu 3, to warto na końcu metody testowej jeszcze sprawdzić, czy w zwracanym wyniku, są tylko te planety, których się spodziewamy (jeśli tak mamy w wymaganiach). Przykładowa implementacja, jak to można zrobić:

public PlanetSetAssert containsOnlyPlanets(String... planets) {
 List<Planet> expectedPlanets = new ArrayList<Planet>();
 for (String planet : planets) {
  Planet p = new Planet(planet);
  expectedPlanets.add(p);
 }
 Assertions.assertThat(actual)
   .usingElementComparator(new PlanetNameComparator())
   .containsOnly(expectedPlanets.toArray(new Planet[expectedPlanets.size()]));
 return this;
}

Co ogólnie jest jeszcze fajnego w takich asercjach? To to, że sprawdzamy dokładnie to co chcemy sprawdzić, zamiast sprawdzać zawsze wszystko, jak było to w pierwszej wersji testu. Również zdefiniowałem sobie różne metody odnośnie sprawdzania występujących na danej planecie gazów atmosferycznych: withGas(Gases.OXYGEN), containsGases(Gases.NITROGEN, Gases.OXYGEN), containsOnlyGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN), doesNotContainGases(Gases.SODIUM). Do wyboru do koloru / potrzeby. Po więcej szczegółów odsyłam do wiki projektu.

Z ciekawostek, warto jeszcze zaprezentować generator FEST asercji. Można sobie to samemu przetestować, ściągając mój projekt z github’a i odpalając assertion-generator\planet.bat. Skrypt automatycznie kopiuje wygenerowaną klasę do podkatalogu fest2 gdzieś w hierarchii testowej, gdzie jednocześnie znajduje się test niej korzystający. Wystarczy zmienić nazwę pakietu i doimportować potrzebne klasy, aby test z powrotem działał.

Tak wygląda kod, który korzysta z przykładowo wygenerowanych asercji:

@Test
public void shouldGenerateInnerPlanets() {
 // given
 SolarSystem service = new SolarSystem();

 // when
 Set<Planet> innerPlanets = service.getInnerPlanets();

 // then
 Planet mercury = findPlanetByName(innerPlanets, "Mercury");
 assertThat(mercury)
   .hasRotationDirection(RotationDirection.LEFT)
   .hasDiameter(4_879_400)
   .hasSiderealYear(new SiderealYear(87.96935))
   .hasAcceleration(3.701)
   .hasAtmosphereGases(Gases.OXYGEN, Gases.SODIUM, Gases.HYDROGEN);

 Planet venus = findPlanetByName(innerPlanets, "Venus");
 assertThat(venus)
   .hasRotationDirection(RotationDirection.RIGHT)
   .hasDiameter(12_103_700)
   .hasSiderealYear(new SiderealYear(224.700_96))
   .hasAcceleration(8.87)
   .hasAtmosphereGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN);

 Planet earth = findPlanetByName(innerPlanets, "Earth");
 assertThat(earth)
   .hasRotationDirection(RotationDirection.LEFT)
   .hasDiameter(12_756_273)
   .hasSiderealYear(new SiderealYear(365.256_363_004))
   .hasAcceleration(9.806_65)
   .hasAtmosphereGases(Gases.NITROGEN, Gases.OXYGEN);

 Planet mars = findPlanetByName(innerPlanets, "Mars");
 assertThat(mars)
   .hasRotationDirection(RotationDirection.LEFT)
   .hasDiameter(6_804_900)
   .hasSiderealYear(new SiderealYear(686.960_1))
   .hasAcceleration(3.69)
   .hasAtmosphereGases(Gases.CARBON_DIOXIDE, Gases.NITROGEN);
}


Niestety musimy sami sobie dopisać metodę szukającą odpowiednią planetę, albo asercję dla Set<Planet>. W wygenerowanym kodzie jest jednoznaczna konwencja, że wszystkie metody sprawdzające nasze własności, dostają prefix has. Można sobie oczywiście to i sporo innych rzeczy zmienić w szablonach generowanego kodu.

A jak komuś mało takie generowanie kodu, to może sobie ten kod generować podczas builda maven’owego, dzięki Maven plugin for fest-assertion-generator. Można to również w moim przykładowym projekcie przetestować, usuwając pliki *Assert.java z katalogu: com\blogspot\mstachniuk\hamcrestvsfest\. Wówczas po odpaleniu mvn install, lub mvn test lub mvn generate-test-sources zostaną wygenerowane na nowo pliki z asercjami. Wówczas test SolarSystemFest3Test, który z nich korzysta, będzie nadal działał.

Ostatnio pojawił się jeszcze plugin do Eclipse’a: FEST eclipse plugin generujący prosto z tegoż środowiska nasze asercje. Zapowiada się ciekawie, więc zachęcam do skorzystania.

Pomyślnych asercji!

piątek, 30 marca 2012

Nowa ksiazka o testowaniu jednostkowym


Dnia 28 marca 2012 pojawiła sie oficjalnie książka: Practical Unit Testing with TestNG and Mockito napisana przez Tomka Kaczanowskiego. Wspominałem w jednym z poprzednich wpisów (na temat testowania wyjatków), że ta książka nadchodzi. Już oficjalnie jest dostępna do kupienia na practicalunittesting.com do czego zachecam.

Dlaczego o tym pisze?

Powodów jest kilka.

Po pierwsze jest to książka polskiego autora (co prawda po angielsku, ale spolszczenie jest w przygotowaniu), a tych należy wspierać. Łatwiej (podobno) jest napisać książkę techniczną po angielsku i później ją przetłumaczyć na nasze, niż pisać po polsku i później się męczyć.

Po drugie jest to kompletny, uporządkowany i aktualny zbiór technik z przykładami, jak pisać dobre testy. Książka wyjaśnia wszelkie wątpliwości związane z nazewnictwem, konwencjami i dobrymi praktykami, jak i daje wiele cennych wskazówek.

Dla kogo jest ta książka?

Książka jest bardzo dobra dla osób, które chcą właśnie zacząć pisać testy jednostkowe do swojego kodu i nie wiedzą jak wystartować. Książka wprowadza krok po korku do lepszego świata kodu pokrytego testami. Również dla startych wyjadaczy znajdzie się sporo cennych informacji. Dalsze rozdziały książki omawiają zaawansowane zagadnienia ze świata testowania. Tomek wskazuje również dobre praktyki jak pisać testy, a także prezentuje narzędzia powiązane z nimi(jak catch-exception czy testy mutacyjne).

Dlaczego więc się wypowiadam o tej książce, skoro dopiero co się ukazała?

Dobra, dość tej krypto reklamy, czas na konkrety ;)

Powodem dla którego piszę na blogu o tej książce, jest fakt, że brałem udział w jej powstawaniu. Mianowicie robiłem korektę (review) książki, za co dostałem podziękowania uwiecznione na jednej ze stron w książce:
In various different ways a number of people have helped me with writing this book – some by giving feedback, others by comforting me in times of doubt.
Marcin Stachniuk was the first person to offer to review the book in the early stages of its being written, and at the same time the most persistent of my reviewers. He read every part of the book and gallantly overcame the obstacles I frequently put in his way: frequent releases, constant juggling of the contents, minor piecemeal adjustments, etc.
Nadszedł teraz czas podzielenia się moimi wrażeniami ze wspólnej pracy z autorem.

Wszystko zaczęło się na konferencji GeeCON 2011 w Krakowie, Podczas (za)ostatniego dnia konferencji (tzw. Community Day) Tomek miał swoją prezentację pt.: Who watches the watchmen? - on quality of tests. Na prezentacji trochę się wynudziłem, gdyż otarłem się już o większość omawianych tam zagadnień. Pod koniec jednak prelegent pochwalił się, że właśnie pisze książkę o testach. Wówczas jeden z uczestników (nazwisko do wiadomości redakcji) zaproponował, że chętnie by zrobił review jego książki. Bardzo zaciekawiła mnie ta inicjatywa, i mimo iż wszyscy już wychodzili z sali, zostałem chwilę dłużej, aby przysłuchać się rozmowie na temat recenzowania książki.

Tomek zaproponował aby chętni się do niego zgłosili mail’owo, aby ustalić szczegóły współpracy.  I tak to się zaczęło. Stwierdziliśmy, że najlepszą formą wymiany informacji będzie założona na Google prywatna grupa dyskusyjna, poprzez którą Tomek będzie podsyłać nowe wersje książki. Tym kanałem można również podyskutować i wyjaśnić ewentualne kwestie sporne. Rozwiązanie to sprawdziło się.

Preferowanym sposobem zgłaszania uwag, było odsyłanie aktualnego pdf’a ze wstawionymi komentarzami w teksie, w formie żółtych karteczek. Funkcjonalność taką daje nam Foxit Reader. Dzięki temu Tomek widział dokładnie, którego miejsca dotyczy uwaga. Było to również wygodne dla mnie, gdyż fragmenty czytałem na ekranie komputera. Niektórzy zgłaszali swoje uwagi w postaci wiadomości na grupie, ale dla mnie żółte karteczki i tak były lepsze, gdyż dokładniej wskazywały miejsce, gdzie jest coś nie tak.

Na podstawie e-maili naliczyłem jakieś 20 „release’ów” książki, tzn. fragmentów, które Tomek podsyłał do korekty. Początkowo przychodziły pojedyncze rozdziały do czytania, ale lepszym wyjściem, jak się później okazało, było podsyłanie całości. Dzięki temu nie było rozdziałów wyrwanych z kontekstu i było widać ogólnego zarys całości książki, w którą stronę ona dąży.

Koleje wersje pojawiały się z różną częstotliwością. Czasem było kilka w tygodniu, a czasem była cisza przez dłuższy czas. Ale to już była kwestia Tomka jak sobie organizuje pracę nad książką. Ze swojej strony mogę powiedzieć, że często nowe wersje się pojawiały, jak akurat mocno byłem zajęty innymi sprawami i niemiałem możliwości aby usiąść od razu do lektury. Kilka razy dogoniła mnie kolejna wersja i wtedy już trzeba było się zmobilizować i przeczytać to co tam Tomek naskrobał :)

Co do zgłaszanych uwag, to Tomek zazwyczaj je akceptował. Jak nie widziałem poprawy w następnej wersji, to początkowo się czepiałem, że moje sugestie są nieuwzględniane, ale w późniejszym czasie doszedłem do wniosku, że nie powinienem się tym przejmować. W końcu to książka Tomka i Jego koncepcja, a nie moja. Jedynie sprawy edytorsko/estetyczne (zła czcionka, rozjeżdżające się tabelki) były dla mnie bardzo irytujące, ale to chyba przez spędzenie dawniej sporej ilości czasu z LaTeX’em. Tomek postanowił zostawić to sobie na sam koniec, przez co ciągle widziałem te niedoskonałości. To była chyba jedyna pierdółka, która mi troszkę przeszkadzała w naszej współpracy.

Moje zaangażowanie zostało wynagrodzone cytowanymi wyżej podziękowaniami w książce (Dzięki Tomek!). Właściwie to nie nawet nie ustaliliśmy szczegółów współpracy, tzn. co ewentualnie będę z tego miał. Do samego końca nie wiedziałem nawet, czy pojawią się te podziękowania w książce (po cichu na nie liczyłem). W końcu w jednej z ostatnich wersji książki zobaczyłem, że są :) Świadczy to o tym, że moje zaangażowanie było kompletnie non-profit.

Podsumowując współpracę cieszę się, że wziąłem udział w tym przedsięwzięciu. Było to bardzo ciekawe doświadczenie i mogłem przez to poznać proces powstawania książki. Robienie review sprawia wielka frajdę, czego zwieńczeniem jest oczywiście ukazanie się książki „na produkcji”. Szczerze polecam każdemu zaangażowanie się w tego typu akcję.


Btw. Co do relacji z 33 degree (kilka osób już o nią pytało), to musi ona jeszcze poczekać kilka dni, z powodu natłoku innych zajęć (w tym korekty ostatecznej wersji książki Tomka), jak i obecnych sporych zmian życiowych.

sobota, 4 lutego 2012

Testowanie wyjątków

Testowanie wyjątków w testach jednostkowych od zawsze trochę mnie irytowało. Niby sprawa banalna, ale jak do tego dołączymy słynny szablon // given // when // then to nie bardzo wiadomo gdzie powinniśmy umieścić słówko // then, aby test był dalej przejrzysty.

Problem nie jest nowy. Zastanawiano się nad dobrym rozwiązaniem już podczas prezentacji Bartosza Bańkowskiego i Szczepana Fabera pt. Pokochaj swoje testy [czas - 18.04] na Wroc JUG, albo pewnie jeszcze wcześniej. Idąc za radą lepszych, zawsze korzystałem z try / catch’a, aby wiedzieć gdzie dokładnie spodziewam się wyjątku.

@Test
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    try {
        // when
        someClass.doSomething();
        fail("This method should throw SomeException");
    } catch(SomeException e) {
        // then
        assertThat(e.getMessage()).isEqualTo("Some message");
    }

}


Blok instrukcji try / catch wymusza na nas pewien sposób formatowania kodu i przez to nie do końca widać gdzie jest // when i // then. Można oczywiście próbować umieszczać je w trochę innym miejscu, np. // when przed try, a // then przed fail().

@Test
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    try {
        someClass.doSomething();
        // then
        fail("This method should throw SomeException");
    } catch(SomeException e) {
        assertThat(e.getMessage()).isEqualTo("Some message");
    }

}

Jednak rozwiązanie dalej jest nie do końca czytelne. Gdyby jeszcze nic nie umieszczać w bloku catch, to już w ogóle, trzeba się chwilę zastanowić, co my tu tak na prawdę chcemy przetestować. Całe szczęście narzędzia do statycznej analizy kodu dbają o to, aby nie zostawiać pustych bloków catch.

Alternatywnym rozwiązaniem dla testowania rzucanych wyjątków, jest stosowanie andotacji @Test z parametrem expected w JUnit’cie  :

@Test(expected = SomeException.class)
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    someClass.doSomething();

    // then
    fail("This method should throw SomeException");
}

Test wygląda już lepiej, ale ma swoje wady. Jak mi jakiś test nie przechodzi, to pierwsze co robię, to czytam sekcję // then testu. W tym przypadku widzę wywołanie fail() co sugeruje mi, że test zawsze powinien nie przechodzić. Dopiero po chwili zauważam, że test został zadeklarowany jako @Test(expected = SomeException.class), czyli spodziewam się wyjątku typu SomeException. Jest tutaj jednak pewne niebezpieczeństwo. Jeżeli faza // given testu, czyli przygotowania środowiska testowego, będzie trochę dłuższa, to może się zdarzyć, że tam gdzieś poleci wyjątek. Test będzie dalej przechodził, a tak naprawdę nie będzie testowane to co chcieliśmy. Wspominał o tym już Szczepan Faber w cytowanym fragmencie video. Dodatkowo nie można, jeśli byśmy chcieli sprawdzić np. treść komunikatu wyjątku. Z tych względów nie stosowałem tej konstrukcji.

Sprawa wygląda trochę  lepiej w przypadku TestNG. Tutaj mamy analogiczna adnotację, mianowicie zamiast expected używamy expectedExceptions.

@Test(expectedExceptions = SomeException.class,
        expectedExceptionsMessageRegExp = "Some Message.*")
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    someClass.doSomething();

    // then
    fail("This method should throw SomeException");
}

Tu jeszcze dochodzi fajna zabawka w postaci expectedExceptionsMessageRegExp, czyli możemy za pomocą wyrażeń regularnych sprawdzić, czy wyjątek posiada spodziewaną wiadomość. Dalej jednak istnieje ryzyko wyrzucenia tego wyjątku w sekcji // given.

Podobną zabawkę daje nam JUnit, ale w trochę innym wydaniu. Mianowicie od wersji 4.7 można zastosować ExpectedException:

public class SomeClassTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void shouldThrowSomeException() throws Exception {
        // given
        thrown.expect(SomeException.class);
        thrown.expectMessage("Some Message");
        SomeClass someClass = new SomeClass();

        // when
        someClass.doSomething();

        // then
        fail("This method should throw SomeException");
    }
}

Tutaj w klasie testowej musimy zdefiniować regułę (linie 3 i 4), która początkowo mówi, że nie spodziewamy się wyjątków. Natomiast już w naszych metodach testowych, redefiniujemy to zachowanie i mówimy, czego się spodziewamy w danym teście (linie 9 i 10). Możemy dzięki temu sprawdzić komunikat rzucanego wyjątku. Tutaj jednak podajemy fragment wiadomości, którą ma zawierać nasz wyjątek. Istnieje również przeciążona wersja tej metody, która jako argument przyjmuje Matcher’a Hamcrest’owego.

Do niedawna były to jedyne rozwiązania, jakie były dostępne w temacie testowania wyjątku. Jakiś czas temu jednak pojawiła się ciekawa biblioteka: catch-exception. Kod napisany za jej pomocą może wyglądać tak:

@Test
public void shouldThrowSomeException() throws Exception {
    // given
    SomeClass someClass = new SomeClass();

    // when
    caughtException(someClass).doSomething();

    // then
    assertThat(caughtException())
            .isInstanceOf(SomeException.class)
            .as("Some Message");

}


Czyli mamy metodę CatchException.catchException(), gdzie jako argument przekazujemy obiekt naszej klasy. Następnie wywołujemy metodę, którą chcemy przetestować. Na koniec w sekcji // then sprawdzamy czy otrzymaliśmy wyjątek, którego się spodziewaliśmy. Bezargumentowa wersja caughtException() zwraca wyjątek rzucony przez ostatnią klasę, którą przekazaliśmy do metody caughtException(). W naszym wypadku jest to ostatni wyjątek wygenerowany przez someClass.

I to rozwiązanie mi się podoba. W sekcji // when informuję, że będę łapał wyjątki, a w sekcji // then sprawdzam czy poleciał ten wyjątek, którego oczekiwałem. I do tego nie bruździ to przy formatowaniu kodu i użyciu // given // when // then. I mamy czytelny kod :)

Zafascynowany tą biblioteką postanowiłem zajrzeć do środka (kod jest dostępny na googlecode.com), aby zobaczyć jak zbudowano takie rozwiązanie.

public class CatchException {

    public static <T> T catchException(T obj) {
        return processException(obj, Exception.class, false);
    }

}

Czyli mamy delegację pracy do metody processException(), która zwraca obiekt tego samego typu, jaki został przekazany w argumencie. Dzięki temu możemy używać biblioteki w sposób jaki pokazałem powyżej. Zobaczmy co kryje się za tą metodą:

private static <T, E extends Exception> T processException(T obj,
        Class<E> exceptionClazz, boolean assertException) {

    if (obj == null) {
        throw new IllegalArgumentException("obj must not be null");
    }

    return new SubclassProxyFactory().<T> createProxy(obj.getClass(),
            new ExceptionProcessingInterceptor<E>(obj, exceptionClazz,
                    assertException));

}

Po upewnieniu się, że argument nie jest null’em tworzymy (jak można się domyśleć po nazwach) proxy dla naszej klasy. Brnąc dalej w las, jeżeli klasa nie jest ani prymitywem, ani finalna, to proxy tworzone jest  w ten sposób:

proxy = (T) ClassImposterizer.INSTANCE.imposterise(
        interceptor, targetClass);

czyli wykorzystywany jest ExceptionProcessingInterceptor z poprzedniegu listingu, wewnątrz którego znajduje się następująca metoda, gdzie już widać całą magię:

public Object intercept(Object obj, Method method, Object[] args,
        MethodProxy proxy) throws Throwable {

    beforeInvocation();

    try {
        Object retval = proxy.invoke(target, args);
        return afterInvocation(retval);
    } catch (Exception e) {
        return afterInvocationThrowsException(e, method);
    }

}

Metoda beforeInvocation() czyści ostatnio złapany wyjątek, np. z wywołania poprzedniej metody. Następnie w bloku try wywoływana jest nasza rzeczywista metoda (linia 5), a następie zwracana jest (w naszym sposobie wykorzystania biblioteki) wartość wygenerowana przez oryginalną metodę. Jak coś pójdzie nie tak, to w bloku catch jest zapamiętywany rzucony wyjątek (zajmuje się tym metoda afterInvocationThrowsException()). Bardzo sprytny sposób na łapanie wyjątków, a jaki banalny zarazem.

Z ciekawostek jeszcze, to biblioteka korzysta z Mockito, a dokładniej cglib. Nie działa ona z klasami finalnymi (gdyż dla nich nie można teoretycznie tworzyć proxy), no chyba że skorzystamy w PowerMock’a i odpowiednich adnotacji w deklaracji klasy testowej:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ SomeClass.class })
public class SomeClassFinalPowerTest {
    // ...
}


Wtedy zadziała :)



Na koniec wpisu, jeszcze informacja skąd się dowiedziałem o tej bibliotece. Mianowicie powstaje teraz ciekawa książka o testach: Practical Unit Testing with TestNG and Mockito. Pisana jest ona przez Tomka Kaczanowskiego i już niedługo ujrzy światło dzienne. Książka poszła już do recenzji do Szczepana Fabra, więc lipy nie będzie. Będzie można się z niej dowiedzieć m.in. o catch-exception, jak i o testach mutacyjnych, o których pisałem ostatnio. Na razie wyjdzie wersja angielska, ale będzie też robione tłumaczenie na nasz ojczysty język. Zachęcam więc do śledzenia informacji o książce jak i do jej zakupu.

Więcej informacji o tym jak powstawała książka w kolejnych wpisach.