31. prosince 2009

Největší problémy při zavádění testování

Mám teď možnost zavádět testování do již existujícího projektu. Projekt je to celkem velký - přes 10 vývojářů, přes 100 tabulek, přes 50 tisíc tříd, několik let vývoje. Technologicky je to postavené nad Springem, Hibernate a spoustou dalších knihoven. O zavedení testování se již pár lidí přede mnou snažilo, ale vždy bez úspěchu, tak jsem zvědavý, jak teď dopadnu já. Hodně důležité je, že testování chce nyní i vedení, což je moc důležitý předpoklad - psaní a údržba testů stojí spoustu úsilí a času a pokud není podpora ze shora, tak se to musí pak "pytlíkovat" za zády a to není moc dobré.

Mám dojem, že většina vývojářů by testy psala celkem ráda, ale vždy diskuze na toto téma skončí u toho, že aplikace je to velice složitá, a že je problém vůbec nějak nastavit počáteční data pro testy.

Ještě je brzy na to, abych psal jak to celé dopadlo, to ještě bude nějaký čas trvat. Dnes bych chtěl psát o největších problémech, které jsem zatím musel řešit, abych vůbec mohl napsat nějaké testy:

  • "špagety kód" - určitě největší problém. Aplikace se vyvíjí již řadu let a zejména ten starý kód není nic moc. Když to přeženu, tak vše souvisí se vším a pak je problém inicializovat pro testy jen ty části aplikace, které potřebuji. Musel jsem refaktorovat, psát stub objekty, abych se dokázal vymezit. Tady není možné praštit do stolu a říci, že zítra se celá aplikace zrefaktoruje a bude to ok. Je to postupný proces. Tento problém se netýká jen samotného kódu, ale také konfigurace aplikace. Musel jsem jí rozdělit do částí tak, abych mohl např. inicializovat jen připojení k databázi nebo webové služby.

  • propojení s dalšími aplikacemi - aplikace závisí na spoustě dalších dílčích aplikací a proto je nezbytné se umět vymezit, nejlépe pomocí nové implementace konektorů pro testy (stub vs. mock objekty).

  • špatná dokumentace - to neplatí jen pro testy, ale obecně. Abych mohl co nejlépe a nejjednodušeji používat nebo implementovat rozhraní, tak potřebuji kvalitní dokumentaci (JavaDoc), jinak si pak musím vše dohledávat v kódu sám.

  • používání auto-wiringu - Springovský auto-wiring se používá pro konfiguraci celé aplikace, takže na první pohled nejsou vůbec jasné vazby mezi třídami, komponentami. Zde jsem musel hodně zapojit IDEU (zejména výborný nástroj na analýzu závislostí v kódu), abych se dopátral všech možných závislostí. Držel bych se doporučení autorů Springu a auto-wiring bych používal jen pro testy.

  • generované ID beanů - pokud v konfiguraci beanů neuvedeme atribut id nebo name, tak to neznamená, že daný bean identifikátor nemá, ale že se automaticky generuje, standardně podle jména třídy (rozhraní BeanNameGenerator). Takže když pak změním jméno třídy, tak se mi změní i identifikátor beanu. Je proto určitě lepší vždy definovat identifikátor, ideálně přes atribut id.

  • transakční propagace REQUIRES_NEW - testy nemají mít žádný vedlejší efekt, žádná uložená data v databázi po testech. Sem tam je v aplikaci použita propagace REQUIRES_NEW, což znamená, že v případě existující transakce se aktuální transakce pozastaví (suspenduje), spustí se nová transakce, která se pak kamitne a pak se znovu aktivuje původní transakce. Toto nemám ověřené, ale vidím zde možný problém pro testy, kdy by se vlastně měly všechny transakce na konci rollbacknout, ale co tyto vnořené transakce, které již jsou po komitu? REQUIRES_NEW se používá minimálně, tak možná to nebude potřeba ani testovat a když bude potřeba, tak zkusím následující možnosti - upravit transakčního managera nebo refaktorovat produkční kód.

Opět se mi potvrdila moje dřívější zkušenost, že čím kvalitnější produkční kód bude, tím lépe se mi budou psát testy. V tomto také vidím jeden z největších přínosů testování.

20. prosince 2009

Generování class diagramů

Class diagramy dnes umí vygenerovat mnoho nástrojů, ale přesto jsme raději nakonec použili vlastní řešení pro generování class diagramů. Mnohdy nám přišla nedostatečná kvalita vygenerovaných diagramů, jindy zase bylo málo možností konfigurace generování a nakonec se ukázalo, že bychom rádi celý proces generování class diagramů zautomatizovali, což u většiny nástrojů nebylo možné.

Takto vygenerované class diagramy používáme jednak pro vlastní potřebu, abychom se v určitém kódu sami lépe vyznali a pak je také daváme jako součást naší dokumentace, zejména pro rozhraní webových služeb.

Pro naše potřeby jsme použili knihovnu UmlGraph. Tato knihovna se stará o zjišťování vazeb mezi třídami a funguje na principu JavaDoc docletu. Buď je možné zvolit zcela automatický řežim, kdy knihovna se sama snaží podle konfigurace zjistit vazby mezi třídami a nebo je možné každou třídu doplnit v JavaDocu třídy speciálními tagy, např. pro vyjádření dědičnosti. Automatický řežim celkem funguje, ale není to 100% - pokud bych rád zobrazoval např. kardinalitu vazeb, pak musím použít tagy. My standardně používáme automatický řežim a jen jednou se nám stalo, že určité vazby se nezobrazily, tak jak jsme chtěli - potom jsme použili tagy a bylo vše ok.

UmlGraph pouze generuje DOT soubor, který je nutný následně zpracovat pomocí nástroje Graphviz, který teprve vykreslí výsledný obrázek. Bohužel Graphviz je nutné lokálně instalovat, což je snad jediná nevýhoda celého řešení. Je možné zase spoustu věcí nastavit dle vlastních potřeb, včetně výstupního formátu obrázku.



Přikládám ukázku kódu:

package cz.marbes.daisy.modules.generator.diagramgenerator;

import org.apache.commons.lang.StringUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.io.PrintWriter;

/**
* Generator class diagramu.
*
* <p>Generator je spousten pres {@link #main(String[]) main} metodu a generuje
* class diagram pro zadany balik (package) trid.
*
* @author <a href="mailto:petr.juza@marbes.cz">Petr Juza</a>
* @see <a href="http://www.graphviz.org">graphviz</a>
* @see <a href="http://www.umlgraph.org/">UML graph</a>
*/
public class ClassDiagramGenerator {

private final static String DOT_FILENAME = "graph.dot";
private static final String UMLGRAPH_MAIN_CLASS = "org.umlgraph.doclet.UmlGraph";

/**
* Vystupni format obrazku [png|gif]
*/
private static final String OUTPUT_IMAGE_FORMAT = "gif";


private ClassDiagramGenerator() {
}


/**
* Main metodu, vstupni bod pro generovani class diagramu.
*
* @param args Vstupni parametry generovani:
* <ol>
* <li>Package, pro ktery se ma generovat class diagram
* (napr. {@code cz.marbes.daisy.modules.aa.wscommon.komu.v1_1_2})
* <li>Vystupni adresar pro DOT soubor a vysledny obrazek
* <li>Absolutni cesta k adresari projektu se zdrojovymi soubory, kde je zadany package
* (napr. {@code /Volumes/Obelix/projects/daisy/apl/aa/trunk/aa-core/src/main/java})
* <li>Cesta ke graphviz DOT souboru (napr. {@code /usr/local/bin/dot})
* <li>Seznam nazvu trid (staci pouze nazev, nemusi byt vcetne baliku), ktere budou vynechany
* pri vykreslovani class diagramu. Jedn. tridy budou oddeleny carkou.
* </ol>
* @throws IllegalArgumentException pokud "nesedi" vstupni parametry
* @throws ClassNotFoundException pokud neni dostupna UmlGraph knihovna
*/
public static void main(String[] args) throws Exception {
// args = new String[]{
// "cz.marbes.daisy.modules.aa.wscommon.smlouvy.v1_1_3",
// "/Volumes/Obelix/projects/daisy/diagram_output",
// "/Volumes/Obelix/projects/daisy/apl/aa/trunk/aa-core/src/main/java",
// "/usr/local/bin/dot",
// "TypPripadService,SqlBuilderQuery,AaSqlBuilderHook"
// };

//kontrola existence UmlGraphu v classpath
try {
Class.forName(UMLGRAPH_MAIN_CLASS);
}
catch (ClassNotFoundException ex) {
throw new ClassNotFoundException("UmlGraph neni dustupny - " +
"pridejte knihovnu UmlGraph do classpath", ex);
}


//validace a zpracovani vstupnich hodnot
if (args == null
|| args.length < 4
|| args.length > 5) {
throw new IllegalArgumentException("Generator ocekava 4 povinne a 1 nepovinny vstupni parametr ...");
}

String packagePath = args[0];
String outputFolder = args[1];
String srcFolder = args[2];
File graphvizDotFile = new File(args[3]);

String excludeClasses = null;
if (args.length == 5) {
excludeClasses = args[4];
}

//kontrola existence graphviz DOT souboru
if (!graphvizDotFile.exists()
|| !graphvizDotFile.canExecute()) {
throw new IllegalArgumentException("Graphviz DOT soubor neexistuje ["
+ graphvizDotFile.getAbsolutePath() + "].");
}

System.out.println("--------------------------------------------------------------");
System.out.println("Generovani class diagramu pro - " + packagePath);
System.out.println("--------------------------------------------------------------");

//generovani
ClassDiagramGenerator generator = new ClassDiagramGenerator();
File diagramDotFile = generator.generateDotFile(packagePath, outputFolder, srcFolder, excludeClasses);

if (diagramDotFile != null) {
generator.generateImage(graphvizDotFile, diagramDotFile);
}

System.out.println("Konec generovani ...");
}


/**
* Generuje DOT soubor.
*
* <p>DOT soubor (format pro <a href="http://www.graphviz.org">graphviz</a>)
* je generovan knihovnou <a href="http://www.umlgraph.org/">UML graph</a>.
*
* @param packagePath Package, pro ktery se ma generovat class diagram
* (napr. {@code cz.marbes.daisy.modules.aa.wscommon.komu.v1_1_2}).
* @param outputFolder Vystupni adresar pro DOT soubor
* @param srcFolder Absolutni cesta k adresari se zdrojovymi soubory, kde je zadany package
* @param excludeClasses Seznam nazvu trid, ktere budou vynechany pri vykreslovani class diagramu.
* Jedn. tridy budou oddeleny carkou.
* @return generovany DOT soubor
*/
private File generateDotFile(String packagePath,
String outputFolder,
String srcFolder,
String excludeClasses) {
File fileDot = new File(outputFolder + File.separator + DOT_FILENAME);

if (StringUtils.isNotEmpty(excludeClasses)) {
//vymazou se mezery a nahradi se carky lomitky (, -> |)
excludeClasses = StringUtils.deleteWhitespace(excludeClasses);
excludeClasses = StringUtils.replaceChars(excludeClasses, ',', '|');
}

excludeClasses = StringUtils.trimToNull(excludeClasses);

//viz http://www.umlgraph.org/doc/cd-opt.html,
// http://java.sun.com/j2se/1.5.0/docs/tooldocs/windows/javadoc.html#javadocoptions
String[] args = new String[]{
"-all", //=-attributes -operations -visibility -types -enumerations -enumconstants
"-hide",
"^(java|com\\.sun|org.apache)" + (excludeClasses != null ? "|" + excludeClasses : ""),
"-inferrel",
"-inferdep",
"-inferdepinpackage",
"-collpackages",
"java.util.*",
"-verbose",
"-output",
fileDot.getAbsolutePath(), //napr.: "/Volumes/Obelix/D/graph.dot",
"-sourcepath",
srcFolder,
packagePath
};


//vytvoreni writeru kvuli logovani
PrintWriter errWriter = new PrintWriter(System.err);
PrintWriter warnWriter = new PrintWriter(System.out);
PrintWriter noticeWriter = new PrintWriter(System.out);

System.out.println("Generovani DOT souboru: ");
System.out.println(" Source folder - " + srcFolder);
System.out.println(" Package path - " + packagePath);
System.out.println(" Dot file - " + fileDot.getAbsolutePath());
System.out.println(" Exclude classes - " + excludeClasses);

//vygenerovani DOT souboru
com.sun.tools.javadoc.Main.execute("UmlGraph",
errWriter, warnWriter, noticeWriter, UMLGRAPH_MAIN_CLASS, args);

return fileDot;
}


/**
* Generuje vysledny obrazek class diagramu.
*
* @param graphvizDotFile Graphviz DOT soubor
* @param diagramDotFile DOT soubor ke zpracovani
* @throws Exception v pripade problemu pri generovani obrazku
*/
private void generateImage(File graphvizDotFile, File diagramDotFile) throws Exception {
try {
String[] args = new String[]{
graphvizDotFile.getAbsolutePath(),
"-T" + OUTPUT_IMAGE_FORMAT,
"-O",
diagramDotFile.getAbsolutePath()
};

System.out.println("Generovani obrazku: ");
System.out.println(" Graphviz DOT soubor - " + graphvizDotFile.getAbsolutePath());
System.out.println(" DOT soubor ke zpracovani - " + diagramDotFile.getAbsolutePath());

Process p = Runtime.getRuntime().exec(args);

BufferedReader reader = new BufferedReader(new InputStreamReader(p.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}

int result = p.waitFor();
if (result != 0) {
System.err.println("Errors occured during running Graphviz - return code is " + result);
}
}
catch (Exception ex) {
System.err.println("Errors occured during running Graphviz - " + ex.getLocalizedMessage());
throw ex;
}
}
}


A ještě obecný ANT task pro spouštění generování diagramů:
    <target name="gen_diagram" description="Generovani class diagramu pro zadany package." depends="clean">
<input message="Zadejte package, pro ktery chcete generovat class diagram (napr. cz.marbes.daisy.modules.aa.wscommon.pvs.v1_1_1)?"
addproperty="packagePath" />

<input message="Zadejte cestu ke zdrojovym souborum relativni k projektu, kde je zadany package (napr. /apl/aa/trunk/aa-core/src/main/java)?"
addproperty="srcPath" />

<input message="(nepovinne) Zadejte seznam nazvu trid oddelenych carkou (nemusi byt vcetne baliku), ktere budou vynechany z class diagramu."
addproperty="excludeClasses" />

<java classname="cz.marbes.daisy.modules.generator.diagramgenerator.ClassDiagramGenerator"
classpathref="project.class.path" fork="true">
<arg value="${packagePath}"/>
<arg value="${diagram.output.folder}"/>
<arg value="${basedir}/${srcPath}"/>
<arg value="${graphviz.dot.path}"/>
<arg value="${excludeClasses}"/>
</java>
</target>

7. prosince 2009

Výhody spolupráce s externisty

Rád bych dnešním příspěvkem navázal na předcházející článek, kde jsem psal o tom, jaké je to pracovat na volné noze. Dnes bych rád navázal a podívám se na výhody, které firmy mohou získat z najímání vývojářů na kontrakt, tzv. body-shopping.

(Pozn.: jen pro upřesnění - práci formou kontraktu si představuji tak, že veškeré náklady spojené s výkonem mé práce jsou čistě mojí záležitostí, neočekávám žádné zaměstnanecké výhody, pouze domluvenou finanční odměnu. Tedy žádný Švarc systém).

  • flexibilní zdroje - vždycky se nedá vše naplánovat a ovlivnit na 100%, často se objevují nové požadavky během vývoje, ale termín zůstává, analýza se opozdila, je nutné řešit více projektů najednou atd. Podobné situace asi všichni dobře znáte, a proto může být výhodné pro firmy mít "někoho" v záloze, kdo může v těchto situacích pomoci. Pokud si navíc firma může vybrat lidi s přesně danými znalostmi a zkušenostmi, tak pak je to opravdu velká výhoda. Určitě nejčastější důvod využívání externích vývojářů.

  • flexibilní náklady - pokud nemusím někoho vést jako zaměstnance, nemusím vést jeho mzdy, vydávat mu stravenky a řešit jeho zaměstnanecké výhody, nemusím mu dávat počítač, mobil, auto, tak budu schopen ušetřit nemalé náklady a hlavně to bude pro mě znamenat velkou flexibilitu z pohledů nákladů firmy - skončí projekt, skončí potřeba.

  • získání know-how - lidi na volné noze mají často hodně co nabídnout, protože vidí jak to funguje v jiných firmách, pracovali na více projektech než standardní zaměstnanci a mohou tedy vnést do firmy nové myšlenky a znalosti. Bohužel tento přínos je hodně často opomíjen a zbytečně ze strany firem nevyužíván.

  • dobře fungující firma - čím lépe bude mít firma nastaveny interní procesy vývoje (konvence psaní kódu, kvalitní analýza, odpovědnosti členů týmu, komunikace v týmu, ...), tím lépe dokáže využít externích spolupracovníků. Pro firmu to tedy může být hodnotná zpětná vazba, zda ve firmě vše funguje jak má.

  • přidaná hodnota - není nutné mít vlastní vývojový tým složený ze všech profesí od analytika, přes architekta až po testera. Často může být pro firmy zajímavé držet know-know projektu a na dílčí "akce" si najímat externisty. Firma se bude soustředit na věci s největší přidanou hodnotou a ostatní potřebné profese si bude najímat.


Doplnili by jste ještě nějaké další výhody?

Samozřejmě, že vše může mít i své nevýhody (fluktuace lidí, problém s údržbou a opravou chyb na projektech, držení know-how, kvalita znalostí externistů, ...), ale myslím si, že vhodnou firemní strategií pro spolupráci s externisty lze mnohem více získat než ztratit.

24. listopadu 2009

Jaké je to pracovat na volné noze?

Často se mě kamarádi a kolegové ptají, jaké to vlastně je být na volné noze, jaké výhody či nevýhody mi to přináší. Tímto článkem bych rád zrekapituloval moje zkušenosti, které jsem za dobu jednoho a půl roku získal.

  • Vždy jsem se hodně snažil učit nové věci, četl články, zkoušel nové knihovny. Ale bylo to takové "umělé". Nyní jsem za relativně krátkou dobu získal spoustu praktických zkušeností, které bych normálně získával mnohem déle. Nemyslím teď jen technické znalosti (různé projekty, různé přístupy k řešení problémů, nové knihovny, atd.), ale zejména jsem měl možnost nahlédnout do mnoha různorodých firem, vidět různé styly komunikace a řízení, různé přístupy k vývoji, přemýšlet o tom, co kde funguje a nefunguje atd. Připadám si jak houba, která se v každé nové firmě pořádně nasákne.

  • Práce na volné noze mě drží v neustálé pohotovosti. Musel jsem si zvyknout na to, že nic není jisté, a že jediný důvod, proč si mě někdo bude chtít najmout bude ten, že budu "dobrý", že budu nabízet něco více než ostatní - znalosti, zkušenosti, profesionální přístup. To mě pořád nutí přemýšlet o sobě, o mých znalostech a to mě tedy žene dopředu. Samozřejmě to je zároveň i nevýhoda, protože vždy není jednoduché se prosadit a není vždy jednoduché přijít do nové firmy a začínat od začátku.

  • Vždy jsem byl placen od hodiny a je to tedy jen o vzájemné důvěře, že opravdu odpracuji uvedený počet hodin, který na konci měsíce vykážu, a který chci zaplatit. Jako zaměstnanec jsem takový tlak nikdy necítil, ale teď si prostě nemohu dovolit číst hodinu články na internetu a pak to vykázat jako nějakou práci. Samozřejmě člověk není robot a někdy se mi pracovat nechce nebo to prostě ani moc nejde - pak to ale musím zohlednit ve výkazu, bohužel.

  • Jsem sám a nikam vlastně nepatřím. Musím tedy zapomenout na jakékoliv zaměstnanecké výhody - tím nemyslím jen stravenky, připojištění, služební auto, atd., ale i to, že se mě netýkají školení, výuka angličtiny, společné výjezdní zasedání apod. To cítím asi jako největší nevýhodu, někdy jsem prostě sám. Na druhou stranu jsem oproštěn od interních dohadování a problémů, což je zase mnohdy výhoda.

  • Jsem sám svým pánem, ale stejně vždy šéfa mám. Spíše se tedy cítím volněji, ale i tak musím plnit termíny, pořád mi někdo zadává práci a pak ji kontroluje, takže v tomto změna není. Mohu si ale vybrat počítač, mobil, auto jaký chci aniž bych porušoval firemní předpisy a jsem větším pánem svého času. Také se dívám na věci více z nadhledu, protože vím, že nikde nebudu napořád.

  • Člověk pozná spoustu nových kolegů, známých, což je určitě také příjemná výhoda.

Svého rozhodnutí přejít na volnou nohu jsem nikdy nelitoval a jsem za něj moc rád. Pokud to jen půjde, tak v nejbližších letech bych rád neměnil.

A jaké jsou vaše zkušenosti? Zkoušeli jste pracovat na volné noze? Myslíte, že pro firmy i do budoucna bude zajímavé si najímat lidi na kontrakt?

14. listopadu 2009

Definice rozhraní dohromady se třídami

Nedávno jsem procházel cizí kód a narazil jsem na definici rozhraní dohromady s definicí použitých tříd, viz následující příklad (příklad je uměle vytvořen):


public interface Connector {

Status getStatus();


/**
* Connection status.
*/
public static class Status {
private int code;


/**
* Constructor
*
* @param code Connection status code
*/
public Status(int code) {
this.code = code;
}


public int getCode() {
return this.code;
}
}
}

Mě by tato konstrukce nikdy nenapadla, proto mě to hned praštilo. Čím více jsem ale o tom přemýšlel a rozebíral s autorem, tak jsem vlastně zjišťoval, že moc argumentů, proč by to tak nemělo být, vlastně nemám.

Zde jsou mé dva argumenty, proč se mi to "nezdá":
  • První argument asi moc neobstojí, ale pro mě je celkem důležitý - rozhraní se definují pomocí rozhraní, třídy pomocí tříd. Proč to tedy míchat dohromady? Autor mi na to řekl, že chtěl zdůraznit spojitost mezi rozhraním a použitými třídami. Tomu moc nerozumím, protože všechny třídy použité v rozhraní jsou automaticky součástí API. Možná měl na mysli to, že kdykoliv budu pracovat se třídou Status, tak budu muset používat prefix rozhraní, tedy Connector.Status a tím tedy vždy bude zřejmá příslušnost třídy k danému rozhraní.

  • Všechny mé další výtky směřují na možné úpravy, rozšíření v budoucnu - to je jeden z důležitých aspektů kvalitního API, aby bylo možné zachovat zpětnou kompatibilitu a přitom mít možnost dané API rozšiřovat. Co když budu chtít třídu Status použít v budoucnu i v jiném rozhraní? Co když budu chtít vytvořit třídu StatusExt jako rozšíření třídy Status?

Přijde mi tedy, že kvůli minimální "výhodě" mohu mít v budoucnu problémy.

Spousta lidí má představu, co to asi tak API je, ale už jen pár lidí si dokáže představit, jak je těžké vytvořit kvalitní API. Já sám se v této oblasti hodně učím a to zejména z knížky "Practical API Design" od Jaroslava Tulacha. Ale je hlavně potřeba praxe a mít vůbec možnost taková API navrhovat a rozvíjet, což se ve standardních webových aplikacích moc často neděje.

Proto by mě moc zajímal názor ostatních - máte nějaký další argument na mojí podporu? Nebo se vám zdá, že to ani nemá cenu řešit, a že je vše v pořádku?