Testování private static final proměnné

Při testování se můžeme setkat s tím, že potřebujeme otestovat funkcionalitu, která není přes vystavené API přístupná. Jako příklad zde uvedu třídu Extractor. Jedná se o třídu, která čte soubor z přesně zadaného umístění. Z toho důvodu je soubor zadán jako private static final. Jak otestovat metodu getContent(), když nemáme přístup k souboru C:\input.txt (nemůžeme tento soubor modifikovat a dokonce tento soubor ani nemusí v době testů existovat)? Řešením je použít reflexi.

Extractor.java

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class Extractor {
	private static final File INPUT = new File("C:\\input.txt");
	
	public Extractor() {}
	
	public String getContent() {
		String result = "";
		try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(INPUT), "UTF-8"))) {
			String line;
			while ((line = br.readLine()) != null) {
				result = result + line;
			}
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
		return result;
	}
}

Testovací soubor testinput.txt umístěný v src/test/resources.

Číst dálTestování private static final proměnné

Ignorování JUnit testů

V případě, že potřebujete, aby se některé JUnit testy nespouštěly, nabízí JUnit anotaci @Ignore (javadoc).

import org.junit.Ignore;
import org.junit.Test;

public class JUnitIgnoreTest {

	@Ignore
	@Test
	public void test1() {
		assertEquals(11, 22);
	}

	@Test
	public void test2() {
		assertEquals(2, 2);
	}
}

junit-ignore-annotace

Je vidět, test byl ignorován. Pokud bychom anotaci @Ignore v tomto případě zakomentovali, test by spadl. Anotaci je možné použít na celou třídu. V tom případě se neprovede žádný test z této třídy.

OutOfMemoryError při testech

V případě, že dostanete následující chybu při Maven buildu (mvn clean install)

Tests run: 226, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 316.116 sec - in TestSuite
Exception in thread "main" Exception in thread "Thread-0" Picked up JAVA_TOOL_OPTIONS: -Djava.vendor="Sun Microsystems Inc."
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Thread-0"

Results :

Tests run: 226, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] CEB UMA Parent ..................................... SUCCESS [ 5.639 s]
[INFO] CEB UMA API ........................................ SUCCESS [ 15.656 s]
[INFO] CEB UMA Impl ....................................... FAILURE [05:37 min]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 05:59 min
[INFO] Finished at: 2016-03-29T15:08:52+02:00
[INFO] Final Memory: 43M/272M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.16:test (default-test) on project project_name: Execution de fault-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.16:test failed: The forked VM terminated without saying properly goodbye . VM crash or System.exit called ?

Zkuste změnit nastavení paměti určené pro surefire plugin.

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-surefire-plugin</artifactId>
 <configuration>
 <argLine>-Xmx4096m</argLine>
 </configuration>
</plugin>

Surefire plugin se používá pro spouštění testů v lifecycle default, fázi test a výše uvedená chyba může signalizovat nedostatek paměti. Volba -Xmx určuje maximální velikost paměti (maximální velikost Java heap). Java heap (halda) je paměť určená pro Java objekty.

Testování error kódu výjimky v TestNG

Testovací framework TestNG umožňuje testovat nejen to, že metoda vrací výjimku a zároveň typ (třídu) vracené výjimky, ale též umožňuje otestovat zprávu, kterou tato výjimka obsahuje. To je vhodné například u vlastní výjimky, která vrací nějaký chybový kód. Otestujeme tedy nejen, že metoda v určitých případech vyhazuje chybu, ale také, zda vrací správný chybový kód.

Enum obsahující chybové kódy

public enum ErrorCode {

	X_IS_TO_BIG("X1111"),
	Y_IS_TO_BIG("Y1122"),
	Y_IS_BIGGER_THAN_X("Y1133");
	
	private final String code;
	
	private ErrorCode(String code) {
		this.code = code;
	}
	
	public String getCode() {
		return this.code;
	}
}

Třída představující naši chybu (exception)

public class MyException extends RuntimeException {

	public MyException(String errorCode) {
		super(errorCode);
	}
}

Třída, jejíž metodu budeme testovat

public class MyObject {
	
	private static final int X_MAX_VALUE = 100;
	private static final int Y_MAX_VALUE = 50;

	public int count(int x, int y) {
		
		if (x > X_MAX_VALUE) {
			throw new MyException(ErrorCode.X_IS_TO_BIG.getCode());
		}
		
		if (y >= Y_MAX_VALUE) {
			throw new MyException(ErrorCode.Y_IS_TO_BIG.getCode());
		}
		
		if (y > x) {
			throw new MyException(ErrorCode.Y_IS_BIGGER_THAN_X.getCode());
		}
		
		return (x + y) / (x + y);
	}
}

Budeme testovat, zda metoda správně vrací výsledek (public void countOk()) a zda vyhazuje chybu se správných kódem v případě, že zadané číslo x nebo y je příliš velké (public void countXToBig(), public void countYToBig()), nebo když je y větší než x (public void countYBiggerThanX()).

import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;

public class MyObjectTest {

	@Test(expectedExceptions = {MyException.class}, expectedExceptionsMessageRegExp = ".*X111.*")
	public void countXToBig() {
		int result = new MyObject().count(10_000, 12);
	}
	
	@Test(expectedExceptions = {MyException.class}, expectedExceptionsMessageRegExp = "Y.*22")
	public void countYToBig() {
		int result = new MyObject().count(9, 5_000);
	}
	
	@Test(expectedExceptions = {MyException.class}, expectedExceptionsMessageRegExp = "Y1133")
	public void countYBiggerThanX() {
		int result = new MyObject().count(22, 27);
	}
	
	@Test()
	public void countOk() {
		int result = new MyObject().count(8, 5);
		assertEquals(result, 1);
	}
}

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:

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.