Pokud to někdo myslí s testováním vážně, tak se asi bez mockování neobejde. Já jsem dříve používal jMock a musím říct, že mě to celkem na nějaký čas od mockování odradilo - samotný zápis mi přišel velice upovídaný, měl jsem problémy s refactoringem a hlavně to bylo celé hodně náročné na údržbu. Zhodnotil jsem to tehdy, že je to moc práce bez velkého přínosu. Ještě bych měl možná podotknout, že jsem psal testy vždy až po produkčním kódu.
Na nedávném setkání s vývojáři v Ostravě mi byla doporučena knihovna Mockito. Sice jsem s ní ještě nic nedělal, ale už mě chytla - kód je hezky čitelný (stylem mi to připomíná hamcrest), bezproblémový refactoring, není nutné psát metody pomocí řetězců a celé mi to prostě přijde takové intuitivní. Jinak o důvodech vzniku Mockita je možné si přečíst na stránkach autorova blogu.
Pokud se vám Mockito líbit nebude, tak zkuste něco z následujících knihoven (na které mám uloženy odkazy): jMockit, EasyMock, jMock a nebo rMock (pokud máte s těmito knihovnami nějaké zkušenosti, tak se rád přiučím).
Nakonec ještě posílám odkaz na zajímavý článek "Mocks Aren't Stubs" od Martina Fowlera.
28. října 2009
Testování s Mockitem
Přidal Petr Jůza v 22:37 0 komentářů
Jaký build nástroj používáte? - výsledky
V poslední anketě mě zajímalo, jaký build nástroj se nejčastěji používá. Celkem hlasovalo 86 lidí s tímto výsledkem:
- Maven (59%)
- ANT (55%)
- Něco jiného (9%)
- GANT (1%)
Dnes bez komentáře, protože k tomu není potřeba nic dodávat :).
Přidal Petr Jůza v 22:21 3 komentářů
1. října 2009
Programátoři vs. kodeři
Při své práci na volné noze jsem již pár firem navštívil a všude mi to přijde stejné - část vývojářů jsou programátoři a část kodeři.
Programátor
Schopnost myslet - asi to zní úsměvně, ale toto je největší schopnost (a tedy rozdíl oproti koderovi) programátora. O své práci přemýšlí, přemýšlí, zda to co dělá dává smysl, zda zadání, které dostal neobsahuje nějaké nesmysly, nesrovnalosti. Programátorovi jde také často o "dobrý pocit" z práce - nejde vždy jen o to udělat nějaký úkol co nejrychleji, něco narychlo zbastlit, ale dělat věci tak, aby s nimi byl člověk sám spokojený.Programátor bude obecně asi také více nespokojený člověk, protože mu záleží na tom, aby dělal v "prostředí", které dává smysl, které ho někam posouvá dále.
Kodér
Kodéra ze všeho nejvíce zajímá zdrojový kód - rád kód vytváří, ale nebaví ho si vymýšlet zadání, přemýšlet nad ním, diskutovat o něm. Ideálně dostane zadání, které obsahuje úplně vše - od informace, jaké metody má udělat a kam je umístit, přes informaci, že je nutné přidat tlačítko "Odeslat" k formuláři (toto je realita, jeden kolega udělal webový formulář bez tlačítek na odeslání). Díky velké orientaci na samotný kód dokáží psát velice efektivně a kvalitně.To, zda je vývojář programátorem nebo kodérem nesouvisí ani tak s tím, jak kdo je dobrý v Javě, jak moc zná různé knihovny, frameworky, ale asi spíše s tím, jaký má přístup k práci, co ho baví více. Někdo je od přírody přemýšlivý, zkoumavý, zodpovědný, pečlivý, ...
Z pohledu sestavení vývojového týmu je zřejmé, že každý tým potřebuje oba dva typy vývojářů - někoho, kdo bude udávat směr a přemýšlet o aplikaci a někdo, kdo bude dělat (kvalitní) kód. Jen je otázka v jakém poměru to namíchat - má zkušenost je zatím taková, že poměr programátorů vs. koderů je přibližně 1:2.
Přidal Petr Jůza v 07:21 7 komentářů
27. září 2009
Vytváření testovacích dat
Každý, kdo píše testy, tak řeší problém s tím, jak nainicalizovat strukturu svých (doménových) objektů, aby mohl otestovat určitou funkcionalitu. Způsobů řešení je více.
"ruční" inicializace pomocí Javy
Pokud test potřebuje nějaké objekty, tak si je v rámci samotného testu prostě vytvoříme resp. přes IoC napojíme, stejně jako když vytváříme produkční kód.Nevýhodou tohoto přístupu je, že ve výsledku vzniká spoustu duplicitního kódu, protože každý si vytváří své objekty. A naopak pokud se objekty vytvářejí centrálně, tak pak je většinou ten problém, že chybí možnost si pro určité testy nastavit určité atributy jinak než implicitně.
externí popis dat
Požadovaná data nadefinuji pomocí vhodného frameworku, tak abych oddělil vlastní testovací kód a testovací data. Ideálním představitelem pro databázové systémy je DbUnit, ale vlastně se jedná o jakýkoliv systém, který mi umožňuje externě definovat data, např. pomocí XML.Výhod v tomto případě vidím několik - testy obsahují pouze kód k ověření funkčnosti a již nemusí řešit "omáčku" okolo, vytváření podobných a často se opakujících testů bude rychlejší a nakonec také testovací data a samotné testy nemusí vytvářet jedna osoba.
Na druhou stranu vím, že tímto způsobem nelze moc dobře vytvářet nějaké větší grafy objektů a také údržba aktuálnosti dat dá celkem zabrat, zejména když se pustíme do refactoringu.
testovací databáze
Pro účely testování bude k dispozici testovací databáze, která bude obsahovat nějaký konzistentní stav a k tomuto stavu budou psány testy.Kromě náročnosti na údržbu dat zde vidím problém v případě, kdy nad touto testovací databází bude pracovat více vývojářů, kteří si budou navzájem zamykat data (např. při krokování programu). Také je jen otázka času, kdy někdo omylem komitne změny v testech. Řešením by zde bylo, kdyby každý vývojář měl svojí testovací databázi.
automatické vytváření grafu objektů
Při vytváření testovacích dat nám jde většinou o několik málo atributů, které ověřujeme a ostatní atributy mohou být zcela libovolné. To je základní myšlenka našeho jednoduchého řešení, které se nám stará o centrální vytváření grafu objektů, aniž bychom museli sami tyto objekty vytvářet pro každý test.Postup je asi následující: každý objekt, který potřebujeme vytvářet během testů, bude mít odpovídající třídu "testData", která se bude starat o jeho inicializaci (nejen jednoduchých atributů, ale i o referencí na další objekty). Pro psaní těchto "testData" tříd jsme napsali dvě podpůrné třídy AbstractReadTestData a AbstractCreateTestData, takže stačí implementovat jen jednu resp. dvě metody ve vlastní "testData" třídě. Kdykoliv pak během testu potřebuji nějaký objekt, tak si zavolám "testData" třídu, (metodu get() nebo createAndSave()) a ta mi testovací objekt vrátí. Buď to může být objekt s hodnotami nastavenými v "testData" třídě nebo s hodnotami přebranými ze šablony - tím mám možnost si nastavit určité atributy nebo i celé objekty dle svých potřeb.
Ještě musím dodat, že jsme na našem projektu rozlišovali dva typy objektů - jeden typ objektu byl pouze pro čtení a byl vždy součástí testovací databáze. Typicky se jedná o číselníky - ty se nemění, ty se můžou pouze číst. Druhý typ objektu je normální objekt, který je nejdříve potřeba vytvořit a uložit do databáze.
Dle mého názoru tento přístup k vytváření testovacích objektů řeší většinu nevýhod zmíněných v předchozích způsobech inicializace. Sami jsme to otestovali na projektu, na kterém dělalo cca 15 programátorů a kde byl velmi košatý a složitý datový model. Snad jediná nevýhoda tohoto a podobných řešení je ta, že vkládaná data do databáze v rámci testů nejsou nikdy komitnuta, takže nemáme možnost si prohlédnout co skutečně v databázi je, např. během ladění.
Tento nápad nebyl pouze z mé hlavy, ale rád bych uvedl spoluautora Pavla Jetenského, který o našem řešení mluvil na letošním jOpenSpace, přednáška Lepší než Dbunit.
Samotný kód určitě napoví více než nějaká slova, tak přikládám jednotlivé třídy včetně ukázek použití.
AbstractReadTestData:
/**
* Abstraktni trida pro nacitani testovacich dat.
*
* <p>Tato trida nabizi pristup k jiz existujicim datum v DB (napr. registry).
* Pokud chcete testovaci data i vytvaret, pak je nutne pouzit tridu {@link AbstractCreateTestData}.
*
*/
public abstract class AbstractReadTestData<BO extends IIdentifiable> {
private static final String SET_METHOD_PREFIX = "set";
private static final String GET_METHOD_PREFIX = "get";
protected final Calendar now = Calendar.getInstance();
/** Mapa objektu, se kterymi se pracovalo. [ID objektu, BO] */
protected Map<Serializable, BO> dataMap = new TreeMap<Serializable, BO>();
/**
* Constructor
*/
protected AbstractReadTestData() {
}
/**
* Metoda vrati prvni (libovolnou) instanci test. dat z DB.
* Pokud dosud zadna takova instance nebyla vytvorena, tak je vyhozena vyjimka.
* Data v DB musi existovat.
*
* @return prvni (libovolnou) instanci test. dat
* @throws IllegalStateException pokud DB neobsahuje data pozadovaneho typu
*/
public BO get() {
BO data;
if (dataMap.isEmpty()) {
data = getExistingRegistryEntity(getDataDao());
if (data != null) {
dataMap.put(data.getId(), data);
}
else {
throw new IllegalStateException("There is no test data for loading.");
}
}
else {
data = dataMap.values().iterator().next();
}
return data;
}
/**
* Metoda vrati BO z DB.
*
* @param dataDao DAO pro pristup k datum
* @return BO nebo null pokud neexistuje
*/
protected BO getExistingRegistryEntity(IPlainDaoSupport<BO> dataDao) {
BO byId = null;
List<BO> findList = dataDao.findList();
if (findList.size() > 0) {
byId = findList.get(0);
}
return byId;
}
/**
* Metoda vyresetuje mapu testovacich dat, se kterymi se dosud pracovalo.
*/
public void cleanTestDataCache() {
dataMap.clear();
}
/**
* Metoda je urcena k implementaci v podtridach a vraci referenci na DAO
* pro nacitani dat z DB (a pripadne manipulaci s daty).
*
* @return DAO pro pristup k datum
*/
protected abstract IPlainDaoSupport<BO> getDataDao();
/**
* Metoda automaticky najde vsechny atributy (fieldy) tridy typu {@link Calendar}
* a nastavi jim aktualni datum a cas (pokud jsou null).
*
* @param data BO
*
* @throws UnsupportedOperationException pokud neni mozne nastavit pres setter Calendar
*/
protected void fillAllCalendarFields(IIdentifiable data) {
Method[] methods = data.getClass().getMethods();
for (Method method : methods) {
try {
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getName().startsWith(SET_METHOD_PREFIX)
&& parameterTypes.length == 1
&& Calendar.class.isAssignableFrom(parameterTypes[0])) {
// nastavime aktualni cas pouze pokud neni nic nastaveno
String getterMethodName = method.getName().replaceFirst(SET_METHOD_PREFIX, GET_METHOD_PREFIX);
Method getter = data.getClass().getMethod(getterMethodName);
Object value = getter.invoke(data);
if (value == null) {
method.invoke(data, this.now);
}
}
}
catch (Exception ex) {
throw new UnsupportedOperationException("Cannot automatically setup calendar method "
+ method.getName() + " of " + data.getClass().getName() + " to default not null value", ex);
}
} //end-for
}
}
AbstractCreateTestData:
/**
* Abstraktni trida pro vytvareni testovacich dat.
*
*/
public abstract class AbstractCreateTestData<BO extends IIdentifiable>
extends AbstractReadTestData<BO> {
/**
* Metoda vrati prvni (libovolnou) instanci test. dat (BO), ktera jiz byla drive vytvorena.
* Pokud dosud zadna takova instance nebyla vytvorena, tak se vytvori objekt s implicitnima hodnotama.
*
* <p><b>Pozor, tato metoda nenacita jiz existujici data z DB, pouze z interni cache po
* naslednem vytvoreni.</b>
*
* @return prvni (libovolnou) instanci BO - bud primo z DB
* @see #createAndSave
*/
final public BO get() {
BO data;
if (dataMap.isEmpty()) {
//vytvorime a ulozim impl. data
data = createAndSave();
}
else {
data = dataMap.values().iterator().next();
}
return data;
}
/**
* Metoda v databazi vytvori instanci, naplnenou vychozimi implicitnimi testovacimi daty.
* Instanci prideli nove logicke id s DB sekvence.
*
* @return BO s implicitnimi daty
*/
final public BO createAndSave() {
return createAndSave(null);
}
/**
* Metoda v databazi vytvori novou instanci test dat,
* naplnenou kombinaci vychozich testovacich hodnot a hodnot ze vstupni sablony.
*
* @param data Sablona pro vytvoreni test. objektu
* @return vytvoreny BO nebo null, pokud se nic nevytvori
*/
final public BO createAndSave(BO data) {
//vytvorime novy test. objekt
BO createdBo = create(data);
//ulozim je do DB a do cache
if (createdBo != null) {
getDataDao().save(createdBo);
dataMap.put(createdBo.getId(), createdBo);
}
return createdBo;
}
/**
* Metoda vytvari (neuklada!) novy testovaci objekt.
* Metoda je zodpovedna za vytvoreni nove instance objektu s nastavenim vsech "not-null" atributu, vcetne
* reference na dalsi objekty.
*
* <p>Vsude, kde je nektera z vlastnosti parametru sablony <i>data</i> nenulova, je pouzita pro novy objekt.
* V ostatnich pripadech se pouzije <i>nejaka</i> vychozi hodnota.
*
* @param data Sablona pro vytvoreni test. objektu
* @return nova testovaci data (BO)
*/
protected abstract BO create(BO data);
}
Aby byl přehled základních tříd kompletní, tak uvádím i rozhraní IIdentifiable, které používáme jako rozhraní pro všechny naše doménové objekty:
public interface IIdentifiable<T extends Serializable> {
/**
* Identifikator.
*
* @return Vraci identifikator objektu.
*/
T getId();
/**
* Metoda zjistuje, jestli ma objekt <i>id</i>.
*
* @return Vraci <tt>true</tt>, pokud ma objekt korektni id, jinak <tt>false</tt>.
*/
boolean isIdentified();
}
A nakonec ukázka použití - jsou použity doménové objekty IPersonBo a IRoleBo, vztah je takový, že osoba (Person) může mít jednu roli (Role). Nejdříve "testData" třídy:
public class PersonReadTestData extends AbstractReadTestData<IPersonBo> {
@Autowired
private IPersonDao personDao;
protected IPlainDaoSupport<IPersonBo> getDataDao() {
return personDao;
}
}
public class PersonCreateTestData extends AbstractCreateTestData<IPersonBo> {
@Autowired
private IPersonDao personDao;
@Autowired
private RoleCreateData roleCreateData;
protected IPersonBo create(IPersonBo data) {
if (data == null) {
data = personDao.createNewInstance();
}
if (StringUtils.isEmpty(data.getName())) {
data.setName("Petr");
}
if (StringUtils.isEmpty(data.getSurname())) {
data.setSurname("Kolousek");
}
if (data.getRole() == null) {
data.setRole(roleCreateData.createAndSave());
}
return data;
}
protected IPlainDaoSupport<IPersonBo> getDataDao() {
return personDao;
}
}
public class RoleCreateTestData extends AbstractCreateTestData<IRoleBo> {
@Autowired
private IRoleDao roleDao;
protected IRoleBo create(IRoleBo data) {
if (data == null) {
data = roleDao.createNewInstance();
}
if (StringUtils.isEmpty(data.getName())) {
data.setName("READ");
}
return data;
}
protected IPlainDaoSupport<IRoleBo> getDataDao() {
return roleDao;
}
}
... a pak použití v rámci testů:
public class CreateDataTest extends AbstractTransactionalJUnit4SpringContextTests {
@Autowired
private PersonCreateTestData personCreateData;
@Autowired
private RoleCreateTestData roleCreateData;
@Before
public void cleanCache() {
personCreateData.cleanTestDataCache();
roleCreateData.cleanTestDataCache();
}
@Test
public void testCreatingNewData() {
//vytvorime zaznamy v DB
IPersonBo person = personCreateData.createAndSave();
assertThat(person, is(notNullValue()));
assertThat(person.getName(), is("Petr"));
assertThat(person.getSurname(), is("Kolousek"));
assertThat(person.getRole(), is(notNullValue()));
assertThat(person.getRole().getName(), is("READ"));
assertThat(countRowsInTable("T_PERSON"), is(1));
IPersonBo retPerson = personCreateData.get();
assertThat(retPerson, is(person));
}
@Test
public void testCreatingNewDataWithGet() {
//vytvorime zaznamy v DB
IPersonBo person = personCreateData.get();
assertThat(person, is(notNullValue()));
IPersonBo retPerson = personCreateData.get();
assertThat(retPerson, is(person));
}
@Test
public void testCreatingExistingData() {
//vytvorime roli podle sablony
IRoleBo roleTempl = new RoleBoImpl();
roleTempl.setName("EXEC");
IRoleBo role = roleCreateData.createAndSave(roleTempl);
assertThat(role, is(notNullValue()));
//vytvorime person ze sablony
IPersonBo personTempl = new PersonBoImpl();
personTempl.setName("Jan");
personTempl.setSurname("Vavra");
personTempl.setRole(role);
IPersonBo person = personCreateData.createAndSave(personTempl);
assertThat(person, is(notNullValue()));
assertThat(person.getName(), is("Jan"));
assertThat(person.getSurname(), is("Vavra"));
assertThat(person.getRole(), is(notNullValue()));
assertThat(person.getRole().getName(), is("EXEC"));
assertThat(countRowsInTable("T_PERSON"), is(1));
IPersonBo retPerson = personCreateData.get();
assertThat(retPerson, is(person));
}
}
Přidal Petr Jůza v 19:56 5 komentářů
14. září 2009
Distribuované transakce bez JEE kontejneru
Dlouho dobu jsem si myslel, že pokud potřebuji řídit transakce přes více datových zdrojů, pak se nikdy neobejdu bez plnohodnotného JEE kontejneru resp. serveru.
Před pár lety jsem na projektu potřeboval propojit do jedné transakce události ze dvou datových zdrojů (databáze, souborový systém) a bez znalosti dané problematiky jsem si sám napsal jednoduchý mechanismus, který mi dokázal v 98% případů zaručit, že se uloží buď obě události a nebo žádná. Ve zbylých dvou procentech případů to nefungovalo, ale to mi z pohledu logiky aplikace moc nevadilo. Pro mě bylo důležitější, že jsem si vystačil pouze se Springem a Tomcatem, a že to celé běhalo celkem rychle.
Proč o tom píšu? Nedávno jsem si všimnul, že samotný Spring bude mít tento mechanismus zabudovaný přímo v jádře.
Já se vždy snažím vyhnout "těžkým" řešením při návrhu aplikací a zde je hezky vidět, že i distribuované transakce se dají řešit "lehkou" cestou. Je ale potřeba si předem jasně říci, co je pro nás nejdůležitější - zda 100% spolehlivost bez výjimek nebo rychlost celého transakčního systému nebo výběr technologií. Nejde bohužel splnit všechny tři kritéria najednou k plné spokojenosti, je potřeba si vybrat jedno a ostatní k tomu přizpůsobit.
Doplňující články:
Přidal Petr Jůza v 07:28 0 komentářů