Time test

JUnit i TestNG umožňují pomocí parametru timeout anotace @Test nastavit podmínku testu tak, aby test, pokud má být úspěšný, proběhl do určité doby (definované v milisekundách). V případě, že test trvá déle, jeho běh je ukončen a test je označen jako neúspěšný (failure).

Metoda, která se bude testovat.

public void longMethod() {
	for (int i = 0; i < 1_000_000; i++) {
		System.out.println("myMethod");
	}
}

Test.

@Test(timeout=1000)
public void longMethodTest() {
	longMethod();
}

Kód je stený jak pro JUnit tak pro TestNG. Pro JUnit test se používá anotace org.junit.Test a pro TestNG org.testng.annotations.Test. Pokud bude výše uvedený test trvat déle jak 1000 milisekund, bude označen jako neúspěšný.

Mockování s JMockit

JMockit umožňuje mockování. Mockování je nahrazení objektů za mock objekty. Mock objekty jsou objekty, které představují (napodobují) nahrazovaný objekt a simulují jeho funkcionalitu tak, jak programátor potřebuje. Mockování se používá při testech a mockují se objekty, na kterých testovaná třída závisí, ale nejsou součástí testu, nebo je programátor nemůže ovlivnit. Může se jednat o dotazy do databáze, na webové služby, ….

Programátor například potřebuje otestovat, jak se jeho objekt chová, když zavolá službu a ta mu vrátí false, nebo nějaký složitý objekt. Tato služba zatím třeba ani neexistuje, nebo je velmi složité napsat na ni dotaz tak, aby vrátil požadovaný výsledek (dotaz musí projít validací a musí se přetransformovat do dalšího objektu tak, aby nevyhazoval výjimky atd.).

V následujícím příkladu použiji pro mockování knihovnu JMockit. Vytvořím si svoji třídu MyService, která bude nabízet metodu getAuthorizedPerson, která autorizuje id a v případě, že dané id bude autorizované, vrátí na jeho základě objekt Person. Moje třída bude volat dvě další služby, které jako programátor nemohu ovlivnit a nejsou ani součástí mého testu.

Třída Person.

public class Person {

	private long id;
	private String name;
	
	public long getId() {
		return id;
	}
	public void setId(long id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

Externí služba pro autorizaci.

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class AuthorizationService {
	
	private Connection connection;

	public boolean isAuthorized(long id) throws SQLException {
		PreparedStatement ps = connection.prepareStatement("select * from t_person where id = ?");
		ps.setLong(1, id);
		ResultSet rs = ps.executeQuery();
		if (rs.next()) {
			return true;
		}
		return false;
	}
}

Třída pro dotazování do databáze.

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class PersonDao {

	private Connection connection;
	
	public Person getPerson(long id) throws SQLException {
		PreparedStatement ps = connection.prepareStatement("select id, name from t_person where id = ?");
		ps.setLong(1, id);
		ResultSet rs = ps.executeQuery();
		if (rs.next()) {
			Person person = new Person();
			person.setId(rs.getLong("id"));
			person.setName(rs.getString("name"));
			return person;
		}
		return null;
	}
}

Třída, kterou potřebuji otestovat

import java.sql.SQLException;

public class MyService {
	
	public Person getAuthorizedPerson(long id) {
		
		// Find out if the person is authorized.
		AuthorizationService as = new AuthorizationService();
		boolean isAuthorized = false;
		try {
			isAuthorized = as.isAuthorized(id);
		} catch (SQLException e1) {
			throw new RuntimeException("Exception while authorizating");
		}
		
		// Get the person.
		if (isAuthorized) {
			PersonDao pd = new PersonDao();
			try {
				Person person;
				person = pd.getPerson(id);
				return person;
			} catch (SQLException e) {
				throw new RuntimeException("Exception while getting person");
			}
		} else {
			throw new RuntimeException("Person is not authorized");
		}
	}
}

Pro tento příklad je třeba přidat závislosti na JUnit a JMockit do projektu:

<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.11</version>
</dependency>
<dependency>
	<groupId>org.jmockit</groupId>
	<artifactId>jmockit</artifactId>
	<version>1.20</version>
</dependency>

Můj test.

import static org.junit.Assert.*;

import java.sql.SQLException;

import mockit.Mock;
import mockit.MockUp;

import org.junit.Before;
import org.junit.Test;

public class MyServiceTest {

	private MyService myService;
	private AuthorizationService authorizationService;
	private PersonDao personDao;

	@Before
	public void prepare() {
		myService = new MyService();
		authorizationService = new AuthorizationService();
		personDao = new PersonDao();
	}

	@Test(expected = RuntimeException.class)
	public void getAuthorizedPersonFailTest() {

		// Mock AuthorizationService to return false.
		mockAuthorizationService(false);

		// Test MySevice#getAutohorizedPerson.
		myService.getAuthorizedPerson(1L);
	}

	@Test
	public void getAuthorizedPersonOKTest() {
		
		// Mock AuthorizationService and its isAuthorized method to return true.
		mockAuthorizationService(true);
		
		// Mock PersonDao#getPersonMethod to return specific Person object.
		mockGetPersonMethod();

		// Test MySevice#getAutohorizedPerson.
		Person p = myService.getAuthorizedPerson(1L);
		assertNotNull(p);
		assertTrue(p.getId() == 101L);
		assertTrue(p.getName().equals("František Koudelka"));
	}

	private void mockAuthorizationService(boolean isAuthorized) {
		new MockUp<AuthorizationService>() {
			@Mock
			public boolean isAuthorized(long id) throws SQLException {
				return isAuthorized;
			}
		};
	}

	private void mockGetPersonMethod() {
		new MockUp<PersonDao>() {
			@Mock
			public Person getPerson(long id) throws SQLException {
				Person p = new Person();
				p.setId(101L);
				p.setName("František Koudelka");
				return p;
			}
		};
	}
}

Vysvětlení:

new MockUp<PersonDao>() {
	@Mock
	public Person getPerson(long id) throws SQLException {
		Person p = new Person();
		p.setId(101L);
		p.setName("František Koudelka");
		return p;
	}
};

Vytvoří mock objekt PersonDao, jehož metoda getPerson vždy vrátí objekt Person s id 101 a name „František Koudelka“. Podobné je to pro AuthorizedService, jejíž metoda isAuthorized vrátí buď true nebo false dle toho, co zadáme jako parametr při volání mockAuthorizationService.

@Before
public void prepare() {
	myService = new MyService();
	authorizationService = new AuthorizationService();
	personDao = new PersonDao();
}

Tento kód se provede před každým testem (díky anotaci @Before). To znamená, že před každým testem se znovu inicializují tyto proměnné.

Zdroje:

Guava Optional.class

Null může znamenat mnoho věcí. Aby bylo jasnější, co vlastně má null představovat, a také proto, aby uživatel ošetřil stavy, kdy může dojít k NullPointerException je tady třída Optional z knihovny Guava. Tato třída je vhodná právě pro případy, kdy null má představovat prázdnou hodnotu.

Pokud proměnná obsahuje prázdnou hodnotu (například záznam nenalezen) použije se Optional.absent(). Pokud neprázdnou hodnotu pak je to Optional, který obsahuje referenci na danou hodnotu. Pokud by v tomto případě proměnná obsahovala null, znamenalo by to, že proměnná nebyla inicializována a byla by to chyba.

Tímto způsobem je přesně řečeno, kdy proměnná něco obsahuje a kdy ne. Dalším důvodem pro tuto třídu je přimět uživatele ke kontrole, a předejít případné NullPointerException.

Hodnotu (referenci na objekt) získlám z Optional voláním metody get(). Když už voláme get(), mělo by nás trknout, že hodnota může být absent a proto bychom měli nejdříve zavolat isPresent(). Pokud tato metoda vrátí true můžeme zavolat get() a s objektem dále pracovat. Určitou nevýhodou je neustálé volání get() metody.

Třída Person.

import com.google.common.base.Optional;

public class Person {
 
       private long id;
       private String name;
       private String surname;
       private Optional<String> nickname;
      
       public Person(long id, String name, String surname) {
             this.id = id;
             this.name = name;
             this.surname = surname;
             this.nickname = Optional.absent();
       }
       public Person(long id, String name, String surname, String nickname) {
             this.id = id;
             this.name = name;
             this.surname = surname;
             this.nickname = Optional.of(nickname);
       }
      
       public long getId() {
             return id;
       }
       public void setId(long id) {
             this.id = id;
       }
       public String getName() {
             return name;
       }
       public void setName(String name) {
             this.name = name;
       }
       public String getSurname() {
             return surname;
       }
       public void setSurname(String surname) {
             this.surname = surname;
       }
       public Optional<String> getNickname() {
             return nickname;
       }
       public void setNickname(Optional<String> nickname) {
             this.nickname = nickname;
       }
}

Třída Service s metodou, která vrací Optional.absent v případě, že daný objekt Person není v seznamu nalezen. V případě, že nalezen je, vrací Optional s referencí na nalezený objekt.

import java.util.List;

import com.google.common.base.Optional;

public class Service {

	public Optional<Person> findLongInList(List<Person> list, Long id) {
		for (Person person : list) {
			if (person.getId() == id) {
				return Optional.of(person);
			}
		}
		return Optional.absent();
	}
}

Třída App, ve které je ukázána práce s Optional.

import java.util.Arrays;
import java.util.List;

import com.google.common.base.Optional;
 
public class App {
       private static List<Person> persons = Arrays.asList(
                    new Person(1L, "František", "Koudelka"),
                    new Person(2L, "John", "Smith", "johny"),
                    new Person(3L, "James", "Bond"));
 
       public static void main(String[] args) {
             Service service = new Service();
             Optional<Person> personId2 = service.findLongInList(persons, 2L);
             Optional<Person> personId3 = service.findLongInList(persons, 3L);
             Optional<Person> personId4 = service.findLongInList(persons, 4L);
 
             // Pravujeme s objektem personId2
             // Zde většinu programátorů napadně kontrola na null.
             if (personId2.isPresent()) {
                    System.out.println("Jméno člověka s id 2: " + personId2.get().getName());
                    // Je ale jednoduché zapomenout na to, že getNickname může vrátit null. S Optional je to zřejmější.
                    if (personId2.get().getNickname().isPresent()) {
                           System.out.println("Jeho přezdívka je: " + personId2.get().getNickname().get());
                    }
             }
 
             // Pravujeme s objektem personId3
             if (personId3.isPresent()) {
                    System.out.println("Jméno člověka s id 3: " + personId3.get().getName());
                    if (personId3.get().getNickname().isPresent()) {
                           System.out.println("Jeho přezdívka je: " + personId3.get().getNickname().get());
                    }
             }
 
             // Pravujeme s objektem personId4
             if (personId4.isPresent()) {
                    System.out.println("Jméno člověka s id 4: " + personId4.get().getName());
                    if (personId4.get().getNickname().isPresent()) {
                           System.out.println("Jeho přezdívka je: " + personId4.get().getNickname().get());
                    }
             }
       }
}

Po spuštění dostaneme následující výsledek:

Jméno člověka s id 2: John
Jeho přezdívka je: johny
Jméno člověka s id 3: James

Zdroje:

Testování privátních metod

V některých případech obsahuje třída složité metody, které však používá pouze tato třída a je zbytečné měnit jejich viditelnost jen kvůli otestování. K otestování takových metod je nutné využít reflexi.

Třída, kterou budeme testovat. Tato třída obsahuje veřejnou metodu compute() a privátní metodu getLongFromString(). Obě tyto metody budeme chtít otestovat.

public class MyClass {
    
    public long compute(String num1, String num2) {
          return Math.abs(getLongFromString(num1) - getLongFromString(num2));
    }

    private long getLongFromString(String number) {
          return Long.parseLong(number);
    }
}

Třída, která slouží jako předek všech testovacích tříd. Tato třída obsahuje metodu getPrivateMethods, která na základě třídy, názvu metody a jejich parametrů dokáže vrátit a zpřístupnit danou metodu na daném objektu.

import java.lang.reflect.Method;

public class AbstractTest {
 
       protected Method getPrivateMethod(Class<?> clazz, String methodName, Class<?> ... parameterTypes) {
             try {
                    // get method vrací pouze public metody
                    // Method method = clazz.getMethod(methodName, parameterTypes);
                    // proto je potřeba volat getDeclaredMethod
                    Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
                    method.setAccessible(true);
                    return method;
             } catch (Exception e) {
                    throw new IllegalArgumentException("Cannot obtain method with name: " + methodName, e);
             }
       }
}

A nakonec třída s testy. Metodu getLongFromString testujeme dvakrát. Jednou pro případ, že zadáme správné parametry a podruhé testujeme, že vyhodí výjimku.

import static org.junit.Assert.assertTrue;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
 
import org.junit.BeforeClass;
import org.junit.Test;
 
public class MyClassTest extends AbstractTest {
       private static MyClass myClass;
      
       @BeforeClass
       public static void prepare() {
             myClass = new MyClass();
       }
 
       @Test
       public void computeOK() {
             String n1 = "120";
             String n2 = "330";
            
             long result = myClass.compute(n1, n2);
             assertTrue(result == 210);
       }
      
       @Test
       public void getLongFromStringOK() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
             String n = "30";
            
             Method testedMethod = getPrivateMethod(MyClass.class, "getLongFromString", String.class);
             long result = (Long) testedMethod.invoke(myClass, n);
            
             assertTrue(result == 30);
       }
      
       @Test(expected = InvocationTargetException.class)
       public void getLongFromStringThrowsException() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
             String s = "AB";
            
             Method testedMethod = getPrivateMethod(MyClass.class, "getLongFromString", String.class);
             testedMethod.invoke(myClass, s);
       }
      
}

Anotace @BeforeClass znamená, že daná metoda se volá jednou a to před voláním jakékoliv jiné metody ve třídě. Anotace @Test(expected = InvocationTargetException.class) definuje, jaký typ výjimky má být vyhozen. Metoda getLongFromString sice vyhodí NumberFormatException, ale ta pak způsobí InvocationTargetException. Z toho důvodu očekáváme při testu tuto výjimku.

Vytvoření řetězce z pole stringů

Níže uvedený kód předpokládá, že máme definováno následující pole: String[] arr = {"a", "b", "c", "d"}.

Použití StringBuilderu.

StringBuilder sb = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
       sb.append(arr[i]);
       if (i < arr.length - 1) {
              sb.append(", ");
       }
}
System.out.println(sb.toString());

Variace na předchozí způsob.

StringBuilder sbb = new StringBuilder();
for (int i = 0; i < arr.length; i++) { 
       if (i > 0) {
              sbb.append(", ");
       }
       sbb.append(arr[i]);
}
System.out.println(sbb.toString());

Použití StringUtils (vnitřně používá StringBuilder).

String result = StringUtils.join(arr, ", ");
System.out.println(result);

Do projektu je třeba přidat závislost.

<dependency>
 <groupId>org.apache.commons</groupId>
 <artifactId>commons-lang3</artifactId>
 <version>3.4</version>
</dependency>

A trochu divoký způsob pomocí třídy Arrays a regulárních výrazů.

String string = Arrays.toString(arr);
String regex = Pattern.quote("[") + "|" + Pattern.quote("]");
String string2 = string.replaceAll(regex, "");
System.out.println(string2);

Převede pole na řetězec (ten obsahuje [] a jednotlivé stringy oddělené čárkou). Následně provede odstranění hranatých závorek. Pattern.quote() provádí správně odescapování daného znaku či sekvence znaků.

Rozdíl mezi lambda a anonymní třídou

Lambda výraz je jiný způsob zápisu anonymních tříd v Javě.

Lambda

Arrays.sort(pole, (String first, String second) -> {
	return Integer.compare(first.length(), second.length());
});

Anonymní třída

Arrays.sort(pole, new Comparator() {
	@Override
	public int compare(String first, String second) {
		return Integer.compare(first.length(), second.length());
	}
});

Lambda výraz je možné použít proto, že Comparator je funkční rozhraní. To znamená, že toto rozhraní má jedinou abstraktní metodu. Pro tato rozhraní se používá anotace @FunctionalInterface. Tato anotace není povinná a pouze kontroluje, zda dané rozhraní splňuje podmínky funkčního rozhraní.

Problém s lambda v Eclipse

Pokud používáte pro vývoj v Javě Eclipse IDE a chcete vyzkoušet psaní lambda výrazů, což je novinka v Javě 8, může se stát, že Eclipse vám validní lambda kód označí jako chybu a to i přesto, že Java 8 máte nainstalovanou a přidanou do build path. Je totiž ještě nutné nastavit compiler compliance level: Window -> Preferences -> Java -> Compiler -> vybrat „Compiler compliance level“ 1.8 -> potvrdit.

Eclipe_ide_compliance_level

Pokud ani toto nepomohlo zkontrolujte nastavení projektu: pravým na projekt -> Properties -> Java Compiler -> vybrat „Compiler compliance level“ 1.8 -> potvrdit.