Čo to obnáša napísať dobrú Arduino knižnicu?

Arduino knižnice by sa mali dať jednoducho používať, mali by byť dobré zdokumentované a pokryté unit testami. Ako sa to robí?

2 years ago   •   17 min read

By Vladimír Záhradník
Zdroj: Daan Lenaerts, Pixabay

Napísať slušnú knižnicu nie je jednoduché. Názory, ako by mala taká knižnica vyzerať, sa medzi vývojármi rôznia. Myslím si, že robustná knižnica by mala mať podrobnú dokumentáciu a vývojári by ju mali pokryť unit testami. Taktiež by slušná knižnica mala skryť všetko zložité a ponúknuť rozhranie, ktoré je intuitívne a jednoduché na použitie.

Väčšinu svojich rokov som strávil pri softvérovom vývoji, pričom som pracoval na veľkých projektoch. Stavali sme ich na robustných frameworkoch a vo väčšine prípadov sme tiež napísali správne unit testy a integračné testy. Verím, že dobre napísané testy vám môžu ušetriť čas, ktorý by ste inak strávili ladením a inými aktivitami.

Svoju Arduino súpravu som dostal asi pred rokom a pol, pričom som framework pochopil veľmi rýchlo. Keď sa navrhovalo Arduino, tvorcovia mysleli na programátorov, pre ktorých je to hobby a ktorí sa programovať naučili sami. Na jednej strane to láka veľkú komunitu ľudí k elektronike, na druhej strane sa kvalita zverejneného kódu dramaticky mení. Keď som potreboval nejakú knižnicu pre moje Arduino projekty, často som si pozrel kód na GitHube, ktorý bol mnohokrát neudržiavaný. Knižnice, ktoré boli udržiavané, často nemali správnu dokumentáciu. Väčšina z nich nemala unit testy.

Prečo by ste mali testovať váš kód? Pozrite si nasledujúce video.

V roku 1996 skončil premiérový let rakety Ariane 5 masívnou katastrofou, ktorú spôsobila programátorská chyba. Prečítajte si celý príbeh tu.

Arduino nie je takto kritické a drahé, ale stále si myslím, že má zmysel písať preňho testy. Nevýhodou je to, že na to potrebujete ďalší vývojársky čas. Povedal by som, že každý projekt je iný a vy sa potrebujete rozhodnúť, či čas na písanie testov za to stojí. Podľa môjho názoru budete výrazne ťažiť z testov, ak implementujete nejaký druh stavového automatu. V Arduine je všadeprítomný a testy vám umožňujú meniť kód s dôverou. Taktiež platí, že Arduino knižnice sú zvyčajne malé, majú iba stovky riadkov kódu, v porovnaní s tisíckami riadkov v Android aplikáciách alebo inde. Aby ste vašu knižnicu dôkladne otestovali, často stačí napísať menej ako 10 testov. Napríklad ja som dokázal napísať unit testy pre relé knižnicu za menej ako tri hodiny a to som sa stále učil, ako sa používa testovací framework.

V tejto dobe pracujem na dlhodobom Arduino projekte, ktorý implementuje poloautomatickú prevodovku. Predstavte si volant s dvoma páčkami vedľa neho.

Pretekársky volant, s láskavým dovolením JSC electronics

Keď vodič stlačí páčku napravo, Arduino preradí na vyšší prevodový stupeň, a keď stlačí páčku naľavo, Arduino podradí. Taktiež Arduino ovláda spojku. Pri pretekoch na rýchlej odozve záleží, prejaví sa to v lepších časoch a potenciálne celkových výsledkoch.

Aby som zistil stlačenia páčky, na začiatku som použil Arduino knižnicu OneButton. Avšak tá sa dobre neintegrovala s mojím existujúcim kódom. Taktiež nevysielala udalosti ako napríklad, že bolo stlačené nejaké tlačidlo. Tieto typy udalostí sú pre mňa zásadné, pretože potrebujem okamžite vedieť, že nastala nejaká akcia. Oneskorenie 50 až 100 milisekúnd má obrovský dopad na latenciu. Nakoniec som sa rozhodol napísať vlastnú knižnicu.

ObjectButton

ObjectButton je Arduino knižnica na detekciu bežných akcií tlačidiel podľa zmien na vstupných pinoch GPIO. Túto knižnicu by malo byť jednoduché integrovať s C++ objektami, podľa toho ten názov.

Obmedzenia callback funkcie

Väčšina Arduino knižníc vám umožní pripojiť callback funkciu, ktorá sa zavolá hneď, ako sa udeje nejaká akcia. Callback sa zvyčajne implementuje ako smerník na funkciu bez vstupných parametrov.

extern "C" {
    typedef void (*callbackFunction)(void);
}

V praxi zaregistrujete callback takto:

...
// Vytvorenie callback funkcie pre kliknutie
void onClick() {
    Serial.println("Bolo kliknuté na tlačidlo");
};

// Registrovanie tejto funkcie v knižnici
OneButton button = OneButton(INPUT_PIN, /* activeLow */ true);
button.attachClick(onClick);

Nasledujúci prístup funguje dokonale pre registrovanie klasických funkcií ako v C-čku, ale už nie pre členské funkcie C++ objektov. Je to preto, že všetky členské funkcie majú implicitný (prvý) parameter — this.

Aby ste toto obmedzenie obišli, mohli by ste:

  • Odovzdať adresu statickej členskej funkcie, ktorá zavolá členskú funkciu bežiacu na konkrétnom objekte
  • Vytvoriť nejakú C-čkovskú funkciu mimo objektu, ktorá zavolá členskú funkciu na objekte
...
LedController controller = LedController();

void onClick() {
    controller.turnLedOn();
};

Poznámka: Lambda/anonymné funkcie sú, pokiaľ viem, v Arduine nedostupné. Arduino nepodporuje kompletný C++ štandard.

Listenery

Namiesto odovzdávania odkazu na callback funkciu, ObjectButton zavádza Listenery. Takýto prístup vo veľkom využíva Android. Listener definujeme ako abstraktnú triedu. Listener definuje kontrakt medzi našou knižnicou a kódom, ktorý napísal používateľ.

Napríklad listener na prijatie onClick udalostí je definovaný takto:

class IOnClickListener {
public:
    virtual ~IOnClickListener() = default;

    virtual void onClick(ObjectButton &button) = 0;
};

Používateľ napíše C++ triedu, ktorá dedí z tejto abstraktnej triedy. Musíte implementovať všetky virtuálne funkcie. Keď takúto triedu zaregistrujete v knižnici, interný kód v knižnici môže bezpečne zavolať funkciu onClick(...).

#include <ObjectButton.h>
#include <interfaces/IOnClickListener.h>

class OnClickListener : private virtual IOnClickListener {
public:
    OnClickListener() = default;
    ....

private:
    void onClick(ObjectButton &button) override;
    ...
};

void OnClickListener::onClick(ObjectButton &button) {
  // Reakcia na kliknutie na tlačidlo
}

Listener zaregistrujete v knižnici takto:

// Vytvorenie inštancie ObjectButton
ObjectButton button = ObjectButton(...);

OnClickListener onClickListener = OnClickListener();
button.setOnClickListener(onClickListener);

Ako štruktúrovať kód

Keď píšete vašu vlastnú Arduino knižnicu, je to oproti písaniu obyčajného Arduino projektu úplne nová skúsenosť. Tentokrát píšete kód, ktorý v projekte používa niekto iný. V knižnici nemáte žiadne funkcie ako setup() a loop(). Arduino vám dáva pravidlá, ako štruktúrovať kód. Niektoré z nich sú povinné, iné sú voliteľné, ale odporúčané. Vaším cieľom by malo byť vytvorenie takej knižnice, ktorá sa každému jednoducho používa, dokonca aj začiatočníkom. Napríklad sa uistite, že v nej máte nejakú begin() funkciu, ktorá inicializuje váš kód. V Arduine tento vzor vídate často.

Predpokladajme o vašich používateľoch následovné:

  • Nemusia mať žiadne predchádzajúce programátorské skúsenosti
  • Nerozumejú smerníkom
  • Nevedia, čo je to konštruktor a deštruktor
  • Poznajú z iných knižníc funkcie begin() a end()
  • Očakávajú názvy funkcií ako camelCase, t.j. getPressure()

Jedným z vašich hlavných cieľov by malo byť skrytie všetkého zložitého a poskytnutie mnohých príkladov na použitie vašej knižnice. Arduino poskytuje niekoľko referenčných príručiek, vy by ste mali pozrieť aspoň tieto:

Potom ako poznáte vašich používateľov a odporúčané pravidlá Arduino komunity, je čas definovať si niektoré ciele, ktoré chcete splniť.

Ciele na dosiahnutie

Ako môžete vidieť, pokrytie unit testami a dokumentácia sú len niektoré z cieľov, ktoré potrebujeme splniť.

Vysokoúrovňová abstrakcia

Keď sa snažíte prísť so slušným návrhom knižnice, začnite perom a papierom. Pre inšpiráciu sa pozrite na oficiálne Arduino knižnice. Všetky sú dobre napísané. Stručne spomeniem knižnicu Servo. Predstavte si, že chcete ovládať servomotor. Ktoré funkcie by ste chceli mať? Asi budete potrebovať nejakú funkciu na zadanie výstupného pinu, na ktorom bude servomotor pripojený. Taktiež budete potrebovať funkciu, ktorá servomotoru povie, o koľko sa má pootočiť a funkciu, ktorá hlási aktuálnu pozíciu servomotora. A to je asi tak všetko čo potrebujete ako používateľ. Používateľov neobťažujete implementačnými detailami, všetky sú skryté.

V mojom prípade som chcel poskytnúť spôsob na registrovanie callbackov, spôsob, ako upraviť debounce interval a spôsob, ako prispôsobiť hodnoty časových limitov použité na detekciu kliknutí, dvojklikov a dlhých stlačení tlačidla. Väčšinu času používatelia len zaregistrujú callback a predvolené hodnoty ich nebudú zaujímať, ale je dobrou praxou dať používateľom možnosť správanie prispôsobiť. Avšak nesprístupňujte všetky hodnoty. Namiesto toho premýšľajte nad scenármi použitia a rozhodnite sa, čo má zmysel sprístupniť. Potom, čo som urobil svoje návrhové rozhodnutia, sa moja knižnica inicializuje jedným alebo dvoma riadkami kódu. Paráda! Pokojne si pozrite príklady toho, ako sa knižnica používa.

Ďalšou vecou, nad ktorou musíte premýšľať, je to, ako štruktúrovať kód s ohľadom na testovanie. Keď píšete kód, skúste premýšľať, ako ho testovať. Typicky by ste sa mali vyhýbať jednej veľkej triede. Namiesto toho rozbite kód do niekoľkých menších tried. Neskôr ich môžete testovať izolovane od zvyšku systému. Ich funkcie by mali robiť iba jednu vec a ich názvy by vám mali napovedať, čo asi robia, bez toho, aby ste čítali dokumentáciu. Za bežných okolností sú tieto funkcie krátke — majú menej ako desať riadkov kódu. Kód, ktorý implementuje stavový automat je zvyčajne veľký. Avšak veľa funkčnosti môžete presunúť do pomocných funkcií. Keď tak spravíte, váš stavový automat poskytne pohľad z výšky na to, čo robí. Tu je príklad z môjho kódu, kde sa oznámi listenerom výskyt udalosti kliknutia.

Namiesto tohto kódu:

if (timeDelta > m_clickTicks) {
    m_state = State::BUTTON_NOT_PRESSED;
    if (m_onClickListener != nullptr)
        m_onClickListener->onClick(*this);
}

Presuniete logiku do inej funkcie, takto:

if (timeDelta > m_clickTicks) {
    m_state = State::BUTTON_NOT_PRESSED;
    notifyOnClick();
}

// presunutá interná logika
void ObjectButton::notifyOnClick() {
    if (m_onClickListener != nullptr)
        m_onClickListener->onClick(*this);
}

Ak sa pozriete na zrefaktorovaný kód, na prvý pohľad uvidíte, čo robí. Stavový automat môže mať veľa klauzúl if ... else, takže všetko, čo to sprehľadní, je dobré. Taktiež môžete teraz písať testy pre ObjectButton::notifyOnClick(), čo je oveľa jednoduchšie ako písanie testov, ktoré overujú rovnaký kód priamo v stavovom automate.

Ak by ste chceli vidieť príklad zložitejšej knižnice, ktorá zvláda prístupové práva, rozbor správ a mnoho ďalších vecí, pozrite si našu nedávno zverejnenú automatizačnú knižnicu Adeon. Veľmi sme sa snažili, aby sme skryli všetku tú zložitosť, a tiež sme napísali podrobnú dokumentáciu.

Dokumentácia

Takže máte knižnicu, ktorá robí to, čo ste chceli. V ďalšom kroku váš kód zdokumentujte.

Existujú dva prístupy:

  • Dokumentáciu k vášmu kódu napíšete osobitne. Pozrite sa na knižnicu Sphinx, aby ste videli príklady. Sphinx používa ako značkovací jazyk reStructuredText.
  • Dokumentáciu k vášmu kódu píšete vnútri kódu. Takýto prístup sa používa vo veľkom vo svete Javy a jej generátora dokumentácie Javadoc.

Navrhujem vám skombinovať oba prístupy. Začnite zdokumentovaním, čo vaša knižnica robí. Pridajte vývojové diagramy, prípadne hocičo, čo môže byť užitočné. Markdown na tento účel slúži dokonale. Navyše môžete pridať dokumenty do toho istého repozitára, kde je váš hlavný kód. Napríklad môžete na uloženie takýchto dokumentov vytvoriť priečinok docs. Pokiaľ používate GitHub alebo GitLab, dokumenty môžete uložiť do Wiki. Technicky ide o samostatný git repozitár, ale je prepojený s vašim projektom a ľahko dostupný.

GitHub Wiki pre ObjectButton

Potom, ako ste napísali vysokoúrovňový prehľad vašej knižnice, je čas napísať komentáre priamo do kódu. Rozhodol som sa použiť Doxygen: ide o de facto štandard a je jednoduché ho nastaviť. Taktiež podporuje niekoľko štýlov komentárov, vrátane štýlu Javadoc.

Doxygen

Ak chcete vygenerovať Doxygen dokumentáciu, potrebujete vytvoriť Doxyfile. Je to šablóna pre Doxygen s rôznymi nastaveniami, napríklad názov projektu, umiestnenie zdrojového kódu, atď. Mal som šťastie, že som našiel Doxyfile, ktorý už niekto upravil pre Arduino. Jedným z nevýhnutných nastavení bolo zahrnutie *.ino súborov. Doxygen zvyčajne nevie, že tieto súbory obsahujú platný C/C++ kód. Ak by ste niekedy takúto šablónu potrebovali, stiahnite si moju a zmeňte názov projektu a cestu.

Ak chcete vedieť podrobnosti o tom, ako pridať komentáre do kódu, najlepšie bude, ak si pozriete webstránku Doxygen.

Tu je príklad komentára vnútri kódu:

/**
  * Smerník na objekt, ktorý počúva na kliknutia. Pokiaľ nie je listener pre udalosti nastavený,
  * takáto udalosť nebude oznámená.
  *
  * @see setOnClickListener(IOnClickListener *listener)
  */
IOnClickListener *m_onClickListener = nullptr;

/**
 * Smerník na objekt, ktorý počúva na dvojité kliknutia. Pokiaľ nie je listener pre udalosti nastavený,
 * takáto udalosť nebude oznámená.
 *
 * @see setOnDoubleClickListener(IOnDoubleClickListener *listener)
 */
IOnDoubleClickListener *m_onDoubleClickListener = nullptr;

Javadoc komentár začína s /**, čo je stále platný C++ komentár. Alternatívna syntax vyzerá nejako takto: ///. Tiež si všimnite značky ako @see. Tieto vám pomôžu vygenerovať hypertextové odkazy na iné triedy a funkcie.

Potom, čo ste váš kód zdokumentovali, vygenerovanie dokumentácie je jednoduché. Váš Doxyfile má všetky informácie.

doxygen <cesta k súboru doxyfile>

Vygenerovanú dokumentáciu môžete umiestniť, kam len chcete. Ide o statickú HTML stránku bez externých závislostí. Ja mám dokumentáciu zadarmo na GitHub Pages. Pre inšpiráciu, tu je vygenerovaná dokumentácia pre jeden z mojich projektov.

keywords.txt

Super, teraz máte slušnú dokumentáciu. Vaša knižnica je v lepšom stave ako možno 80-percent knižníc. V ďalšom kroku vytvorte súbor keywords.txt. Arduino IDE má minimálne schopnosti zvýrazňovania syntaxe. Preto potrebujeme do tohto súboru spísať všetky funkcie, triedy a premenné, a priradiť im správny typ. Kód sa zvýrazní iba vtedy, ak Arduino IDE nájde zhodu medzi kľúčovým slovom a vašou definíciou. Pokiaľ chcete vašu knižnicu zahrnúť do oficiálneho registra Arduino knižníc, vytvorenie súboru keywords.txt je povinné.

#######################################
# Dátové typy (KEYWORD1)
#######################################

IOnClickListener    KEYWORD1
IOnDoubleClickListener  KEYWORD1
IOnPressListener    KEYWORD1

#######################################
# Metódy a funkcie (KEYWORD2)
#######################################

getId   KEYWORD2
setOnClickListener  KEYWORD2
setOnDoubleClickListener    KEYWORD2

Ako môžete vidieť, najprv vypíšete všetky vaše verejné dátové typy, funkcie, dokonca triedy, a potom im priradíte jedno z definovaných kľúčových slov, napríklad KEYWORD1. Každé kľúčové slovo odpovedá inej farbe vnútri Arduino IDE. Keď budete potrebovať tento súbor vytvoriť, pozrite si dokumentáciu alebo iné knižnice.

Príklady

Nakoniec potrebujete napísať nejaké príklady, ako používať vašu knižnicu. Príklady sú voliteľné, ale pre používateľa sú veľmi hodnotné. Je to častokrát prvá vec, ktorú ľudia hľadajú, keď si vyberajú knižnicu. Môžete sa k nim dostať priamo z Arduino IDE. Popremýšľajte o nejakých základných úlohách, ako budú ľudia vašu knižnicu používať, a tiež skúste napísať aspoň jeden pokročilý príklad. Čím viac príkladov napíšete, tým lepšie pre vašich používateľov.

Všetky príklady sú projektové súbory pre Arduino (*.ino), ktoré viete skompilovať priamo z Arduino IDE. Ak ste pracovali s Arduinom, som si istý, že presne viete, čo treba.

Pre inšpiráciu sa pozrite na príklady z mojej knižnice.

Automatizované testovanie

Teraz máte knižnicu, ktorá je dobre zdokumentovaná a zdá sa byť jednoduchá na použitie. V ďalšom kroku napíšte nejaké unit testy, aby ste overili, že váš kód robí to, čo by mal robiť. Automatizované testovanie stále nie je pri Arduino knižniciach bežné a ani ma to neprekvapuje. Ak sa pozrieme na ďalšie programovacie prostredia, často prídu s oficiálnym testovacím frameworkom alebo majú aspoň dobre známy framework tretej strany. V Jave a Androide je to JUnit, Python má vstavaný modul unittest a tiež sa dosť používa pytest, C++ svet používa Googletest. Arduino neposkytuje žiadny oficiálny testovací framework a k tejto téme nenájdete veľa článkov.

Všade ručné testovanie

Väčšina Arduino vývojárov testuje svoj kód ručne. Keď ich kód nejako funguje, považujú prácu za hotovú. Ak pri budúcich aktualizáciách kód rozbijú, dlho si to nemusia vôbec všimnúť. To nie je dobré!

Ďalší prípad je overenie toho, že váš kód funguje na viacerých hardvérových doskách. Ak ho ručne otestujete na Arduine Uno, ako viete, že vaša knižnica funguje aj na Leonarde? Ak ručne overujete váš kód na niekoľkých platformách, strácate čas. A pokiaľ všetky podporované platformy nekontrolujete, nemáte žiadnu záruku, že bude kód fungovať na všetkých doskách.

Kompilácia knižnice pre rôzne dosky

Snáď sa zhodneme na tom, že automatizovaná kompilácia pre niekoľko dosiek dáva zmysel. Je jednoduché ju nastaviť a preto si myslím, že by to mala využívať každá Arduino knižnica!

Máte niekoľko možností, ako to docieliť:

Prístup pomocou PlatformIO

PlatformIO projekty majú zakaždým súbor platformio.ini. V ňom definujete celú konfiguráciu projektu, vrátane platforiem, pre ktoré sa majú zostaviť binárky. Každá platforma predstavuje inú Arduino dosku alebo dokonca inú architektúru procesora.

; Arduino Uno: Prvá cieľová doska
[env:uno]
platform = atmelavr
board = uno
framework = arduino

; Arduino Mega: Druhá cieľová doska
[env:megaatmega2560]
platform = atmelavr
board = megaatmega2560
framework = arduino

Keď chcete zostaviť kód pre viacero cieľových platforiem, jednoducho zadajte nasledujúce príkazy:

# pio run -e <cieľové prostredie>
pio run -e uno
pio run -e megaatmega2560

Tieto príkazy môžete pridať do shellovského skriptu, ktorý môžete púšťať opakovane. Do niekoľkých sekúnd sa dozviete, či sa váš kód skompiluje alebo či v ňom máte nejakú chybu.

Prístup so zostavovaním príkladov

Zvyšné dve spomenuté voľby od vás vyžadujú, aby ste vytvorili ukážkové projekty, ktoré demonštrujú použitie vašej knižnice. Tieto príklady by ste mali umiestniť do priečinka examples, ako je definované v špecifikácii Arduino knižníc. Keď spustíte skript, prejde všetky vaše príklady a skompiluje ich pre všetky uvedené platformy. Keď Arduino IDE zostaví vaše príklady, zostaví aj vašu knižnicu, pretože príklady na nej závisia.

Hoci písanie príkladov je voliteľné, každá dobre podporovaná knižnica by mala mať aspoň jeden. A keď už taký príklad máte, úprava kódu, aby fungoval so skriptami pre zostavenie je otázka pár minút. Nevidím dôvod nepoužiť to!

Písanie unit testov

Unit testy skúmajú kód vašej knižnice v izolácii. Občas je náročnejšie ich napísať, ale klady prevyšujú nad zápormi.

Našiel som niekoľko testovacích frameworkov tretích strán, ktoré sa dajú s Arduinom použiť:

Každý z týchto frameworkov podporuje spúšťanie testov na natívnych strojoch, čo znamená, že môžete spúšťať testy na obyčajnom počítači bez pripojenej Arduino dosky. Je to výhodné, obzvlášť keď chcete nastaviť priebežnú integráciu (continuous integration) na vzdialenom serveri.

Pre ObjectButton som si ako testovací framework vybral ArduinoCI. Ale nedá sa povedať, že si môžete vybrať zle. Pozrite sa na všetky možnosti a rozhodnite sa, čo je pre vás najlepšie.

Prečo ArduinoCI?

ArduinoCI má dobrý prehľad testovacích frameworkov. Moju pozornosť si získalo tým, že podporuje mockovanie Arduino hardvéru. Predstavte si, že chcete nasimulovať kliknutie na tlačidlo. Ako to spravíte bez kliknutia na fyzické tlačidlo? Ak máte kontrolu nad vstupnými pinmi a vnútorným časovačom, gesto dokážete nasimulovať programovo. A práve to nám dáva ArduinoCI k dispozícii.

Taktiež platí, že tento framework kompiluje všetky testy do natívneho kódu a podporuje priebežnú integráciu. Jediné, čo mi chýba, sú správy o percentách pokrytia kódu testami.

ArduinoCI unit testy spúšťané na Travis CI

Testovanie funkcií

Všetky vaše testy idú do priečinka test. Zvyčajne ich nazvete rovnako ako testované funkcie. ObjectButton by mal vedieť rozpoznať tri typy odlišných akcií.

Na to, aby som ich reprezentoval, som vytvoril tri testovacie súbory a jeden na overenie základnej funkčnosti:

  • action_click.cpp na spustenie všetkých unit testov týkajúcich sa kliknutí na tlačidlo
  • action_double_click.cpp na spustenie unit testov týkajúcich sa dvojitých kliknutí
  • action_press_release.cpp na spustenie unit testov týkajúcich sa stlačenia tlačidla, dlhého stlačenia, atď.
  • sanity_checks.cpp na overenie funkčnosti ako že getId vráti správne id tlačidla

Každý testovací súbor obsahuje jeden alebo viac unit testov týkajúcich sa funkcie. Všetky testy viete zapísať do jedného veľkého súboru a pobežia v pohode. Avšak keď ich rozdelíte do niekoľkých súborov podľa funkcie, máte jemnejšiu kontrolu nad spúšťaním podmnožiny testov. Taktiež ak jeden z testov zlyhá, mali by ste ho vedieť rýchlejšie nájsť.

Písanie unit testov

Prejdime si test, ktorý overí, že knižnica správne rozpozná kliknutie na tlačidlo.

Aby ste napísali test na kliknutie na tlačidlo, najprv potrebujete pochopiť, čo je to vlastne kliknutie. Už ste nad tým premýšľali? Zvyčajne keď niečo používame, nepremýšľame, ako presne to funguje, ale keď chceme otestovať kliknutie na tlačidlo, musíme si zadefinovať, čo je to kliknutie a ako ho rozpoznáme. Potom môžeme napísať test, ktorý toto správanie zachytí.

Definícia: Kliknutie je akcia, ktorá sa udeje, keď používateľ stlačí tlačidlo a potom ho pustí. Stlačenie a pustenie by mali nastať skôr ako uplynie časový limit pre kliknutie, a tiež by sa počas tejto doby tlačidla používateľ nemal dotknúť (pravdepodobne by šlo o dvojité kliknutie).

Teraz, keď sme si zadefinovali, čo je to kliknutie, môžeme ho nasimulovať v našom teste:

  • Používateľ stlačí tlačidlo
  • Používateľ pustí tlačidlo
  • Stlačenie a pustenie tlačidla sa udialo v rámci časového limitu definovaného pre kliknutie

Nižšie je skutočný príklad z mojej testovacej suity:

unittest(receive_on_click_event_after_default_click_ticks) {
    ListenerMock testMock = ListenerMock(INPUT_PIN, true);
    GodmodeState* state = GODMODE();

    // stlačenie tlačidla
    state->digitalPin[INPUT_PIN] = LOW;
    testMock.getButton().tick();

    // poslanie udalosti o stlačení tlačila po uplynutí debounce doby
    state->micros = (DEFAULT_DEBOUNCE_TICKS_MS + 1) * 1000;
    testMock.getButton().tick();

    // pustenie tlačidla
    state->digitalPin[INPUT_PIN] = HIGH;
    state->micros = (DEFAULT_CLICK_TICKS_MS + 1) * 1000;
    testMock.getButton().tick();

    // obnova stavového automatu pre získanie udalosti kliknutia
    testMock.getButton().tick();

    // validácia, že nastalo kliknutie
    assertEqual(1, testMock.getPressEventsReceivedCount());
    assertEqual(1, testMock.getReleaseEventsReceivedCount());
    assertEqual(1, testMock.getClickEventsReceivedCount());
    assertEqual(0, testMock.getDoubleClickEventsReceivedCount());
    assertEqual(0, testMock.getLongPressStartEventsReceivedCount());
    assertEqual(0, testMock.getLongPressEndEventsReceivedCount());
}

Stav: používateľ stlačil tlačidlo V tomto prípade považujeme tlačidlo za stlačené vtedy, keď je úroveň napätia na vstupnom pine 0 voltov. Vďaka ArduinoCI to vieme nasimulovať veľmi jednoducho:

state->digitalPin[INPUT_PIN] = LOW;

state je smerník na štruktúru ArduinoCI, ktorá ovláda „virtuálne“ Arduino, na ktorom beží náš test.

Teraz sme stlačili tlačidlo a potrebujeme ho po určitom čase pustiť. Keby sme len zmenili napätie na vstupnom pine, vnútorný časovač by stále vrátil tú istú hodnotu — 0. Doslova neuplynul žiaden čas. Aby sme nasimulovali plynutie času, potrebujeme nastaviť pole micros, t.j.:

state->micros = (DEFAULT_DEBOUNCE_TICKS_MS + 1) * 1000;

Všimnite si, že časovač ovládate určením času v mikrosekundách, nie milisekundách (ako to používa funkcia millis()).

Stav: používateľ pustil tlačidlo Používateľ už nejaký čas drží tlačidlo, je čas ho pustiť.

state->digitalPin[INPUT_PIN] = HIGH;
state->micros = (DEFAULT_CLICK_TICKS_MS + 1) * 1000;

Vstupný pin sme nastavili na hodnotu 3,3 voltu, čo simuluje pustenie tlačidla. Ak by ste to potrebovali, môžete tiež posunúť interný časovač, aby ste nasimulovali reálne podmienky.

Rozpoznala knižnica kliknutie na tlačidlo? Náš unit test presne simuluje kliknutie na tlačidlo. Ak naša knižnica pracuje správne, o tejto udalosti dá vedieť OnClickListeneru. Zvyčajne výsledky overujeme na konci testu:

assertEqual(1, testMock.getClickEventsReceivedCount());

Kód vyššie kontroluje, či knižnica ObjectButton odoslala presne jedno oznámenie o kliknutí na tlačidlo. Aby som zachytil všetky udalosti hlásené z knižnice, vytvoril som si pomocný listener registrovaný na všetky udalosti podporované knižnicou. Tento listener inkrementuje interný čítač zakaždým, keď obdrží udalosť. Ukazuje to, že nie ste obmedzení len na schopnosti frameworku, ale že si ho viete rozšíriť pre vaše konkrétne potreby.

Dôvera

Prečo je užitočné mať unit testy? Ide o dôveru, že môj kód robí to, čo by mal. Podobne ako predošlý test napíšete aj ďalšie. Mali by zachytávať očakávané správanie — čo by mala vaša knižnica robiť — a overiť, že tomu tak skutočne je.

Písanie testov je časovo náročné, ale ich spúšťanie už nie. Testovaciu suitu môžete opakovane spustiť v priebehu niekoľkých sekúnd. Je to obzvlášť užitočné, keď pracujete na novej funkcii a chcete sa uistiť, že ste do existujúcich funkcií nezaviedli chybu. V prípade ObjectButton sa všetka interná logika deje v stavovom automate. Tí z vás, ktorí už implementovali stavový automat, viete, aké zradné to je. Občas nie je na prvý pohľad jasné, čo kód robí. Ak zmeníte implementáciu stavového automatu, je veľká pravdepodobnosť, že ste tam zaviedli chybu. Ale ak máte fungujúcu testovaciu suitu, môžete si overiť, že všetko stále funguje a stále budete mať voči vášmu kódu dôveru.

Nová funkcia == nové testy

Ak implementujete novú funkciu, ktorá mení existujúce správanie knižnice alebo pridáva nové vlastnosti, mali by ste taktiež napísať nové unit testy alebo upraviť tie existujúce. Udržiavanie testovacej suity by malo byť pravidelnou súčasťou vaších vývojárskych praktík. Inak časom vaša dôvera poklesne, pretože novú funkčnosť nemáte otestovanú.

Záver

V tomto príspevku som písal o potrebných akciách na vytvorenie kvalitných Arduino knižníc. Tieto princípy platia aj vo vašich projektoch. Stále by ste mali dokumentovať, čo váš kód robí, a mali by ste aspoň zvážiť, že napíšete zopár unit testov. Testovaciu suitu môžete spustiť z príkazového riadka. Weboví vývojári už tento postup dobre poznajú.

Aj keď ste váš kód zdokumentovali a otestovali, stále ešte máte prácu. Nabudúce vám vysvetlím, ako automatizovať vývojový proces a nastaviť vo vašom repozitári priebežnú integráciu, aby sa po každom commite automaticky spustili testy a zostavila sa dokumentácia.

Ak máte nejaké otázky alebo máte pocit, že som na niečo zabudol, dajte mi o tom vedieť v komentároch. Vďaka!

Spread the word

Keep reading