EasyMock i IAnswer, recepta na zachowanie metod

Filed under Java, Porady, Testy jednostkowe by Łukasz Dywicki

Złym zwyczajem jest modyfikowanie argumentów zamiast zwracania nowej wartości, jakkolwiek trafiają się sytuacje gdy testowany kod powinien weryfikować takie wywołania. W EasyMock mamy do dyspozycji w takim przypadku interfejs IAnswer. Jego użycie jest w miarę proste – dobieramy się do tablicy argumentów i robimy z nią co potrzeba.

Oto przykład – interfejs Populator dodaje do przekazanej tablicy określoną wartość w puste miejsca. Zwraca też ilość zmian, które zaprowadził. Całość jest oparta na bardzo prostym kodzie, zaledwie jedna pętla, jakkolwiek na potrzeby przykładu jest to wystarczające. Pragnę jednak nadmienić, że w podobny sposób można testować bardziej złożony kod, gdzie kolaborują ze sobą dwa obiekty – tj. Observator i Observable czy też Visitor i Visitable.

package org.code_house.test.mock;

interface Populator {
    int fill(String value, String[] arguments);
}

Implementacja interfejsu jest bardzo prosta:

package org.code_house.test.mock;

class FillAnswer implements IAnswer<Integer> {
    public Integer answer() throws Throwable {
        // pobranie argumentow
        Object[] arguments = EasyMock.getCurrentArguments();
        String name = (String) arguments[0];
        Object[] values = (Object[]) arguments[1];

        // logika potrzebna do testu
        int populated = 0;
        for (int i = 0; i < values.length; i++) {
            if (values[i] == null) {
                populated++;
                values[i] = name;
            }
        }
        return populated;
    }
}

Test jest również niezbyt skomplikowany – bazuje on na poprzedniej nocie – “Testowanie tablic argumentów z EasyMock”:

package org.code_house.test.mock;

import static org.easymock.EasyMock.*;
import org.easymock.IAnswer;

import java.util.Arrays;

import junit.framework.TestCase;

public class AnswerTest extends TestCase {

    public void testPopulate() {
        Populator populator = createMock(Populator.class);
        expect(populator.fill(eq("Code House"), aryEq(new String[3])))
            .andAnswer(new FillAnswer());
        expect(populator.fill(eq("Code House"), aryEq(new String[] {"1"})))
            .andAnswer(new FillAnswer());
        replay(populator);

        // tablica do wypełnienia
        String[] populated = new String[3];
        int added = populator.fill("Code House", populated);
        assertEquals(3, added);
        // w odpowiedzi oczekujemy dodania 3 nowych elementów
        assertTrue(Arrays.equals(
            new String[] {"Code House", "Code House", "Code House"},
            populated
        ));

        // w tym przypadku nie oczekujemy nowości
        added = populator.fill("Code House", new String[] {"1"});
        assertEquals(0, added);
    }
}

2 responses so far

Testowanie tablic argumentów z EasyMock

Filed under Java, Porady, Testy jednostkowe by Łukasz Dywicki

Często zdarza się że metody, które piszemy i później testujemy mają argumenty w postaci tablic. EasyMock wówczas potrafi zgłosić wyjątek, że przekazana tablica jest różna od oczekiwanej mimo, że zawartość tablic jest identyczna.

java.lang.AssertionError:
  Unexpected method call find([Ljava.lang.String;@1ad77a7):
    find([Ljava.lang.String;@b8f82d): expected: 1, actual: 0
package org.code_house.test.mock;

import static org.easymock.EasyMock.aryEq;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;

import java.util.Collections;
import java.util.List;

import junit.framework.TestCase;

interface Finder {
    List<String> find(String[] names);
}

public class FinderTest extends TestCase {

    public void testFind() {
        Finder mock = createMock(Finder.class);
        expect(mock.find(aryEq(new String[] {"Amy", "Luke"}))).andReturn(
            Collections.EMPTY_LIST);
        replay(mock);

        List<String> list = mock.find(new String[] {"Amy", "Luke"});
        assertTrue(list.isEmpty());
    }
}

Rozwiązaniem tych problemów jest użycie mechanizmu “matcherów”, które są wbudowane w sam szkielet. Najprostszym sposobem na ich użycie jest odwołanie przy pomocy statycznego wywołania którejś z metod EasyMock. Poniżej znajduje się lista dostępnych matcherów. Wartość “given” to argument przekazany w wywołaniu metody.

  • eq(X value)
    Argument pasuje jeśli wyrażenie value.equals(given) zwróci true. Dostępny dla wszystkich typów prostych i obiektów.
  • anyBoolean(), anyByte(), anyChar(), anyDouble(), anyFloat(), anyInt(), anyLong(), anyObject(), anyShort()
    Dowolna wartość zostanie przepuszczona niezależnie od tego co będzie przekazane. Dostępny dla wszystkich typów.
  • eq(X value, X delta)
    Pasuje jeśli aktualna jeśli wyrażenie given jest równe value z dokładnością do delta. Dostępne dla typów float oraz double.
  • aryEq(X value)
    Pasuje jeśli wyrażenie Arrays.equals(value, given) zwraca true. Dostępne dla tablic typów prostych jak i dla obiektów.
  • isNull()
    Pasuje jeśli given == null. Dostępne tylko dla obiektów.
  • notNull()
    Pasuje jeśli given != null. Dostępne tylko dla obiektów.
  • same(X value)
    Pasuje jeśli wyrażenie value == given jest prawdziwe. Dostępne tylko dla obiektów.
  • isA(Class clazz)
    Pasuje jeśli given instanceof clazz zwraca true. Given może być instancję clazz jak i pochodną.
  • lt(X value), leq(X value), geq(X value), gt(X value)
    Zwraca true jeśli given jest mniejsze, mniejsze równe, większe równe bądź większe niż value. Dostępne dla wszystkich numerycznych typów prostych.
  • startsWith(String prefix), contains(String substring), endsWith(String suffix)
    Zwraca true jeśli given zaczyna się, zawiera bądź kończy się daną wartością. Dostępne dla argumentów typu String.
  • matches(String regex), find(String regex)
    Pasuje jeśli given pasuje w całości do wyrażenia/fragment pasuje. Dostępne dla argumentów typu String.
  • and(X first, X second)
    Zwraca true jeśli matcher first oraz second zwracają prawdę.
  • or(X first, X second)
    Zwraca true jeśli matcher first bądź second pasuje.
  • not(X value)
    Zwraca true jeśli matcher value zwrócił fałsz.

Uzbrojeni w taki zestaw matcherów możemy poprawić kod testu tak by był on poprawny:


public class FinderTest extends TestCase {

    public void testFind() {
        Finder mock = createMock(Finder.class);
        expect(mock.find(aryEq(new String[] {"Amy", "Luke"}))).andReturn(
            Collections.EMPTY_LIST);
        replay(mock);

        List<String> list = mock.find(new String[] {"Amy", "Luke"});
        assertTrue(list.isEmpty());
    }
}

Inny przykład:

public class FinderTest extends TestCase {

    public void testFind() {
        Finder mock = createMock(Finder.class);
        expect(mock.find("Lucy", aryEq(new String[] {"Amy", "Luke"}))).andReturn(
            Collections.EMPTY_LIST);
        replay(mock);

        List<String> list = mock.find("Lucy", new String[] {"Amy", "Luke"});
        assertTrue(list.isEmpty());
    }
}

Warto zwrócić uwagę na to, że jeśli korzystamy dla matchera aby dopasować argument to musimy zrobić to dla wszystkich, inaczej poleci wyjątek:

java.lang.IllegalStateException: 2 matchers expected, 1 recorded.

Stąd nasz test w końcowej postaci powinien wyglądać następująco:

package org.code_house.test.mock;

import static org.easymock.EasyMock.aryEq;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;

import java.util.Collections;
import java.util.List;

import junit.framework.TestCase;

interface Finder {
    List<String> find(String[] names);

    List<String> find(String name, String[] names);
}

public class FinderTest extends TestCase {

    public void testFind() {
        Finder mock = createMock(Finder.class);
        expect(mock.find(eq("Lucy"), aryEq(new String[] {"Amy", "Luke"}))).andReturn(
            Collections.EMPTY_LIST);
        replay(mock);

        List<String> list = mock.find("Lucy", new String[] {"Amy", "Luke"});
        assertTrue(list.isEmpty());
    }
}

No responses yet

Uruchom inny język w Javie

Filed under Java, JavaScript, Porady by Łukasz Dywicki

Do Javy 6.0 zostało dołączone API (JSR 223) umożliwiające wywoływanie różnych języków wewnątrz wirtualnej maszyny. Można w ten sposób przesunąć chociażby moment kompilowania kodu na później bądź od razu podpiąć język interpretowany.

Poniżej przykład:

package org.code_house.scripting;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;

public class JSMain {

    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();

        System.out.println("Dostępne języki");
        for (ScriptEngineFactory engine : manager.getEngineFactories()) {
            System.out.println(engine.getNames());
        }

        ScriptEngine engine = manager.getEngineByName("js");
        engine.put("y", 12);

        if (engine != null) {
            engine.eval("var x = 1; print(x + y);");
        }
    }

}

W taki oto sposób na naszej konsoli powinno wylądować coś takiego:

[js, rhino, JavaScript, javascript, ECMAScript, ecmascript]
13

Swing + Java Script

Możemy również uruchomić plik js. Wystarczy do metody eval przekazać FileReader. Po uruchomieniu kodu który jest niżej powinno pokazać się okienko podobne do tego, które widzicie przy tym akapicie.

package org.code_house.scripting;

import java.io.*;
import javax.script.*;

public class JavaScriptMain {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        engine.eval(new FileReader(new File("print.js")));
    }
}
frame = new javax.swing.JFrame;
frame.defaultCloseOperation = javax.swing.JFrame.EXIT_ON_CLOSE;
frame.title = "Test"
frame.setSize(new java.awt.Dimension(180, 80));

button = new javax.swing.JButton("Kliknij mnie")
button.addActionListener(function(event) {
    print(event.source);
});
frame.add(button);

frame.show()

Z dodatkowych języków można podpiąć między innymi PHP, Java FX, Groovy, BeanShell i wiele innych. Aby to zrobić należy ściągnąć ze strony scripting.dev.java.net rozszerzenia (dostępne w sekcji Documents & files) i dodać JAR dla języka którego potrzebujemy do classpath.

No responses yet

« Newer Entries - Older Entries »