čtvrtek 26. července 2012

Self testy

Pokud konfigurace vaší aplikace přesáhne jednu jedinou možnost nastavení, pak zřejmě řešíte problém, jak zkontrolovat, jestli je aplikace po instalaci/releasu správně nakonfigurovaná a tím pádem nic nebrání jejímu provozu. To oceníte v případech, kdy aplikace běží v různých prostředích v různém nastavení, například produkční cluster, testovací a vývojové prostředí. Kontrola se nemusí týkat jenom vaší aplikace, ale i prostředí, ve kterém je nasazena. Celý cluster GoodData platformy je konfigurován pomocí nástroje Puppet. Může se stát, že některé skripty respektive jejich části mohou selhat. Proto jsme si vyvinuli jednoduchý framework, pomocí kterého píšeme testy, které ověří správnou konfiguraci aplikace a jejího prostředí.

Co testovat

V každé webové aplikaci existuje několik oblastí, které se vyplatí otestovat. Můžeme je rozdělit na externí a interní.

Interní

Konfigurační properties
Testy konfiguračních properties se skládají z obyčejné validace, že hodnota je jiná než defaultní. Defaultní hodnoty jsou nastavené s respektem k lokálnímu prostředí. Na produkci by se nemělo například používat stejné heslo jako na lokálním prostředí, proto zkontrolujeme hodnotu konfigurační proměnné, která ho nastavuje. Většinou tedy testujeme změnu proti defaultu, případně rozsahy hodnot. Například počet vláken určených pro obsluhu HTTP požadavků by měl být větší než 100.
Komponenty a jejich nastavení
Testy, které kontrolují nastavení jednotlivých komponent. Jedná se především o komponenty, které mají rozdílné chování v různých prostředích. Komponenta dokáže fungovat s defaultem, ale potřebujeme například ověřit, že v produkčním nasazení se bude komponenta chovat jinak. Jako příklad bych uvedl naší komponenty pro Single Sign On (zkráceně SSO) a nebo cache. Je potřeba ověřit, že SSO má k dispozici keystore a dokáže se dostat ke klíčům . U cache je potřeba ověřit, že bude fungovat replikace tj. hosty a porty uvedené pro replikaci jsou přístupné.

Externí

HTTP enpointy, Messagingingové enpointy
Každá aplikace používá externí služby a je dobré ověřit, že jsou skutečně k dispozici. Stačí jednoduchý test dostupnosti služby zasláním požadavku. Pokud to není možné díky charakteru služby např. není možné ověřit doručení, kontrolujeme dostupnost hostu a portu.
Databáze
V databázi, v našem případě MongoDB, kontrolujeme její dostupnost tím, že listujeme dostupné kolekce. Pokročilejší test může kontrolovat verzi aplikace proti verzi databáze, abychom předešli například nekonzistentním datům.
Běhové prostředí
Těmito testy si kontrolujeme nastavení JVM a Tomcatu. Jestli má JVM minimální velikost heapu a perm space oblasti, kterou očekáváme. Jestli se používá skutečně verze Tomcat, na které jsme testovali nebo jestli je nastavený security manager atd.

Jakým způsobem testovat

Jedním z kritérií, které jsme si při vývoji kladli, bylo izolovat vlastní test od toho jakým způsobem budou jeho výsledky reprezentovány. Zároveň jsme chtěli umožnit jednoduché přidávání testů bez nutnosti zásahů (registrace) do jádra systému. První problém jsme vyřešili použitím standardního logovacího objektů (org.slf4j.Logger), pod kterým jsme si implementovali zpracování výsledků.


Rozhraní

/**
 * Interface representing self test of java components of GDC platform.
 */
public interface ISelfTest {

  public static final String LOGGER_PREFIX = "SelfTest: ";

  /**
   * Tests desired functionality and logs results into logger property (of type {@link Logger}).
   * The pattern is that problems are logged as ERROR, ok messages as INFO.
   */
   public void test(Logger logger);

}

Test ověřující JVM typ

Poznámka: některé části AWT/Swing se velmi lišily fork od forku OpenJDK. Proto byl potřeba tento test, abychom si byli jistí, že je použitá správná JVM

/**
 * {@link ISelfTest} checking whether the active JVM comes from Oracle or Sun.
 */
public class SunJVMSelfTest extends SelfTest {

  @Override
  public void test(Logger result) {
    String vendor = System.getProperty("java.vm.vendor");
    String version = System.getProperty("java.version");

    if (vendor != null && (vendor.toLowerCase().contains("oracle") 
      || vendor.toLowerCase().contains("sun"))) {
      result.info(LOGGER_PREFIX + "Java vendor='{}', version='{}'.", vendor, version);
    } else {
      result.error(LOGGER_PREFIX + "Java vendor='{}', version='{}'.", vendor, version);
    }
  }
}

Aby nemusely jednotlivé komponenty registrovat testy někam do jádra systému, stačilo využít vlastností Inversion of Control frameworku, který dokáže vrátit všechny komponenty daného typu viz Automatický "sběr" objektů daného typu pomocí Springu. Komponenty načítáme dynamicky proto není potřeba žádný zásah do existující konfigurace jádra.

Spouštění testů a reprezentace výsledků

Mechanismus spouštění testů je nezávislý na vlastních testech. Testy jsme schopni teoreticky spustit přes JMX či message do RabbitMQ, ale prakticky máme implementované spuštění přes HTTP. Máme speciální servlet, který se stará o spuštění testů. Výsledky testů vracíme jako JSON v odpovědi na HTTP požadavek a zároveň jej logujeme. Tím pádem je nám online k dispozici pro případnou analýzu.

Ukázka výstupu

{
   "selfTestResult":{
      "testsCount":13,
      "errorsCount":3,
      "warningsCount":0,
      "infosCount":16,
      "errors":[
         "c.g.s.t.a.ApplicationCodeSelfTest: SelfTest: Master password is not changed.",
         "c.g.s.t.a.ApplicationCodeSelfTest: SelfTest: Pool size is set to default value.",
         "c.g.s.t.jvm.MemorySelfTest: SelfTest: 'Maximal memory (-Xmx)' is below desired bounds (current='516947968' desired='1073741824')."
      ],
      "warnings":[

      ],
      "infos":[
         "c.g.s.t.jvm.MemorySelfTest: SelfTest: 'Init memory (-Xms)' is within desired bounds (current='536870912' desired='268435456').",
         "c.g.s.t.jvm.MemorySelfTest: SelfTest: 'PermGen space' is within desired bounds (current='1073741824' desired='268435456').",
         "c.g.s.t.jvm.MemorySelfTest: SelfTest: Used memory '337501408'.",
         "c.g.s.t.jvm.SunJVMSelfTest: SelfTest: Java vendor='Oracle Corporation', version='1.7.0_02'.",
         "c.g.s.t.jvm.TmpPathSelfTest: SelfTest: Temp path='/mnt/tmp'.",
         "c.g.s.t.s.ApacheSelfTest: SelfTest: Project templates are accessible on uri='/projectTemplates'.",
         "c.g.s.t.s.C3SelfTest: SelfTest: C3 service is listening on host='localhost' and port='667'.",
         "c.g.s.t.s.C3SelfTest: SelfTest: C3 service FoodMart demo is accessible on uri='/gdc/c3/project/FoodMartDemo'.",
         "c.g.s.t.s.ConnectorsSelfTest: SelfTest: Connectors for FoodMart demo are accessible on uri='/gdc/projects/FoodMartDemo/connectors'.",
         "c.g.s.t.s.JBossCacheSelfTest: SelfTest: JBossCache works ok.",
         "c.g.s.t.s.MetadataSelfTest: SelfTest: Metadata for FoodMart demo are accessible on uri='/gdc/md/FoodMartDemo'.",
         "c.g.s.t.s.MongoSelfTest: SelfTest: Connection to mongo host='127.0.0.1' port='27017' ok. Available databases='[gdc, local]'.",
         "c.g.s.t.s.MongoSelfTest: SelfTest: Connection to mongo host='127.0.0.1' port='27017' database='gdc' ok. Available collections='[xxxx]'.",
         "c.g.s.t.s.RabbitSelfTest: SelfTest: Rabbit MQ service is listening (host='localhost', port='5672').",
         "c.g.s.t.s.StageDirectorySelfTest: SelfTest: Stage directory='/var/gdc/users' is accessible and writeable.",
         "c.g.s.t.tomcat.TomcatSelfTest: SelfTest: Tomcat major version is ok. ServerInfo='Apache Tomcat/7.0.23'."
      ],
      "includedTests":[
         "ApplicationCodeSelfTest",
         "MemorySelfTest",
         "SunJVMSelfTest",
         "TmpPathSelfTest",
         "ApacheSelfTest",
         "C3SelfTest",
         "ConnectorsSelfTest",
         "JBossCacheSelfTest",
         "MetadataSelfTest",
         "MongoSelfTest",
         "RabbitSelfTest",
         "StageDirectorySelfTest",
         "TomcatSelfTest"
      ]
   }
}

Další rozvoj

Možnosti rozšíření se nabízejí ve dvou oblastech. Jednak psaní nových testů a rozšiřování těch existujících, což je nikdy nekončící boj. Další oblastí je komplexnější testování clusterového deploymentu. Mám na mysli například orchestrace testů s dalšími službami, abychom ověřili, že nejenom naše služby, ale i celý cluster funguje. Na druhou stranu je to už trošku jiný typ úlohy.

Self test nám pomáhá odhalit problémy, které mohou vzniknout během releasu, který je zatím pro většinu komponent platformy čtrnáctidenní, ale existují i komponenty, které se releasují i několikrát týdně. Dlouhodobým cílem je dostat s ke continuous delivery a ten už si fungování bez podobného nástroje nedokážu představit.