neděle 17. července 2011

Pětiminutové intro do Mockito knihovny

Pár lidí se mě ptalo na Mockito a po té co ji vyzkoušeli mi dali za pravdu, že se jedná opravdu o zásadní nástroj pro psaní testů. Není mým cílem popsat všechny vlastnosti této knihovny, ukážu pouze to nejzásadnější s pár drobnými komentáři. I tak čtenář neznalý této knihovny dostane do rukou návod, který mu vystačí na 80% případů, se kterými se střetne při testování.

Jedním z důvodu proč mají vývojáři odpor pro psaní testů je omáčka, kterou je potřeba vytvořit pro nastavení vlastního prostředí testovaného objektu a složitost verifikace výsledků samotného testu. To je přesně situace, ve které Mockito tahá kaštany z ohně.

Mockito slouží k mockování tříd a jejich chování a kontrolu jejich interakce s okolním světem. Neznám český překlad slova mock a mockování a dosti silně pochybuji, že vůbec existují. Mock je dynamicky vytvořený objekt s rozhraním dané třídy. Mockování je definice chování mocku při interakci s okolním světem, příklad volám na mocku metodu X, chci aby se vrátila hodnota 1.

Poznámka na okraj. Mockito mi pomohlo v pochopení, že kód který není dobře objektově navržený není možné efektivně otestovat. Kromě všech vlastností, kterými mi pomáhá při psaní testů, zároveň nastavuje zrcadlo objektovému návrhu a nutí mě přemýšlet jakým způsobem jej vylepšit, což zpětně vede k lepší testovatelnosti.

K jednoduché ukázce Mockita jsem si vybral dva objekty Basket a Item představující nákupní košík respektive zboží v něm uložené. Nákupní košík má metody pro přidání nového zboží, vyčištění košíku a vrácení celkové ceny vloženého zboží. Zboží má pro jednoduchost pouze metodu, která vrací jeho hodnotu.

    public class Item {

        private final double price;

        public Item(double price) {
            super();
            this.price = price;
        }

        public double getPrice() {
            return price;
        }
    }
    public class Basket {

        private final List<Item> items;

        public Basket() {
            this(new ArrayList<Item>());
        }

        Basket(List<Item> items) {
            super();
            this.items = items;
        }

        public Basket addItem(Item item) {
            this.items.add(item);
            return this;
        }

        public List<Item> getItems() {
            return items;
        }

        public void clear() {
            this.items.clear();
        }

        public double getTotalPrice() {
            double totalPrice = 0;
            for (Item item : items) {
                totalPrice += item.getPrice();
            }
            return totalPrice;
        }
    } 

Klíčovou třídou pro použití Mockita představuje org.mockito.Mockito. Ta má spoustu statických metod a vyplatí se proto udělat její statický import. Nejzásadnější metody jsou:

  • Mockito.mock - jako argument dostává třídu, ze které vytváří mock (třída nesmí být final).
  • Mockito.when - umožňuje definovat chování (vrať hodnotu, vyhoď vyjímku) při volání dané metody třídy
  • Mockito.verify - umožňuje zkontrolovat, jestli daná metoda mocku byla volaná a to včetně argumentů, se kterými byla volaná
  • Na prvním příkladu si ukážeme jak otestovat očekávané chování metody pro celkovou cenu zboží uložené v košíku. Připravíme si dvě mock instance zboží, které vložíme do košíku.

        public void testGetTotalPrice() {
            Basket basket = new Basket();
            Item item1 = mock(Item.class);
            when(item1.getPrice()).thenReturn(10d);
            Item item2 = mock(Item.class);
            when(item2.getPrice()).thenReturn(20d);
    
            basket
                .addItem(item1)
                .addItem(item2);
    
            assertThat("The total price must be sum of all prices in basket", basket.getTotalPrice(), is(30d));
        }
    

    Při mockování zboží, představovaného třídou Item jsme nejdříve vytvořili mock a po té jsme určili chování metody getPrice. V tomto případě by bylo možné namítnout, že není důvod třídu Item mockovat a použít přímo její instanci. Osobně se snažím všude používat mocky z toho důvodu, že testovací třídy a metody jsou na sobě méně závislé.

    Dalším testem chceme ověřit, že košík se při volání metody clear opravdu vyčistí. K tomuto účelu můžeme použít právě techniku ověření interakce s okolním světem. Já jsem zamockoval interní list, ve kterém si košík drží všechny položky a na něm ověřil volání jeho metody pro vyčištění.

        @Test
        public void testClear() {
            List<Item> items = mock(List.class);
            Basket basket =  new Basket(items);
            basket.clear();
            verify(items).clear();
        }
    

    Poslední řádek právě verifikuje volání metody clear na mocku. Podobným způsobem se zamockováním interního listu lze otestovat i přidávání zboží do košíku. O tom, že je tato technika trochu kontroverzní se zmíním vzápětí.

        @Test
        public void testAddItem() {
            Item item = mock(Item.class);
            List<Item> items = mock(List.class);
            Basket basket =  new Basket(items);
            basket.addItem(item);
            verify(items).add(item));
        }
    

    Při používání mocků a verifikace jejich interakce s testovaným objektem má několik nevýhod. V případě, že začnete tuto verifikaci provádět, tak si ověřujete interní implementaci, která by měla být testu absolutně ukradená, pokud se bavíme o black box přístupu. Je na zvážení, co je potřeba ještě verifikovat, protože změna implementace může znamenat, že testy přestanou procházet jenom z toho důvodu, že byly vázané na implementační detaily. Další nevýhodou je fakt, že někdy musíte pro testy porušit zapouzdření a vystavit právě onu interní část, kterou je potřeba zamockovat.

    Mockito toho umí daleko více než metody zmíněné v tomto článku. Můžete například verifikovat i argumenty se kterými se volá mock, můžete otestovat, že nedochází k interakci. Můžete dělat částěčně mocky. Každopádně základ práce s Mockitem pokrývají právě tyto tři metody.