Matteo Manferdini

Mnoho vývojářů považuje testování jednotek za matoucí. Zhoršují to pokročilé techniky, které jsou potřeba k testování tříd nebo asynchronního kódu, jako je ten pro síťové požadavky.

Ve skutečnosti jsou základem unit testování jednoduché základní pojmy. Jakmile je pochopíte, stane se unit testing najednou přístupnějším.

Obsah

  • Jednotkové testování vysvětlené jednoduše, srozumitelných termínech
  • Jednotkové testování umožňuje kontrolovat těžko dosažitelné okrajové případy a předcházet chybám
  • Psaní jednotkových testů v Xcode
  • Dobrá architektura usnadňuje testování kódu
  • Přistupování k typům hodnot jako k čistým funkcím pro zjednodušení jednotkového testování
  • Ověřování kódu pomocí funkcí assertions z frameworku XCTest
  • Měření pokrytí kódu testem v Xcode
  • Proč je snaha o konkrétní pokrytí kódu nesmyslná a co byste měli dělat místo toho

Jednotkové testování vysvětlené jednoduše, srozumitelných termínech

Když píšete aplikaci, procházíte cyklem psaní kódu, jeho spouštění a kontroly, zda funguje, jak má. Jak ale váš kód roste, je stále těžší testovat celou aplikaci ručně.

Automatické testování může tyto testy provádět za vás. Prakticky řečeno je tedy testování jednotek kódem, který zajišťuje, že kód vaší aplikace je správný.

Druhým důvodem, proč může být testování jednotek těžké pochopit, je to, že je kolem něj mnoho pojmů: testování v Xcode, architektura aplikace, dvojí testy, vstřikování závislostí a tak dále.

V jádru je však myšlenka testování jednotek poměrně jednoduchá. Jednoduché unit testy můžete psát v obyčejném Swiftu. Nepotřebujete žádnou speciální funkci Xcode ani sofistikovanou techniku a můžete je dokonce spouštět uvnitř Swift Playground.

Podívejme se na příklad. Níže uvedená funkce vypočítá faktoriál celého čísla, což je součin všech kladných celých čísel menších nebo rovných vstupu.

1
2
3
4
5
6
7

func factorial(of number: Int) -> Int {
var result = 1
for factor in 1…number {
result = result * factor
}
return result
}

Chcete-li tuto funkci otestovat ručně, nakrmíte ji několika čísly a zkontrolujete, zda výsledek odpovídá očekávání. Takže byste ji například vyzkoušeli na čísle čtyři a zkontrolovali, že výsledek je 24.

To můžete zautomatizovat tak, že napíšete funkci Swift, která tento test provede za vás. Například:

1
2
3
4
5

func testFactorial() {
if factorial(of: 4) != 24 {
print(„Faktoriál 3 je špatně“)
}
}

Toto je jednoduchý kód Swiftu, který pochopí každý. Jediné, co dělá, je kontrola výstupu funkce a vypsání zprávy, pokud není správný.

To je unit testing v kostce. Všechno ostatní jsou zvonky a píšťalky, které přidal Xcode, aby testování zjednodušil.

Aspoň by to tak bylo, kdyby byl kód v našich aplikacích tak jednoduchý jako funkce faktoriál, kterou jsme právě napsali. Proto potřebujete podporu Xcode a pokročilé techniky, jako je zdvojování testů.

Tato myšlenka je nicméně užitečná jak pro jednoduché, tak pro složité testy.

Jednotkové testování umožňuje kontrolovat těžko dosažitelné okrajové případy a předcházet chybám

Jednotkové testování vám může přinést několik výhod.

Nejzřejmější je, že nemusíte svůj kód neustále testovat ručně. Spustit aplikaci a dostat se na konkrétní místo s funkcí, kterou chcete otestovat, může být zdlouhavé.

Ale to není všechno. Unit testy vám také umožňují testovat okrajové případy, které by bylo obtížné vytvořit v běžící aplikaci. A okrajové případy jsou místem, kde žije většina chyb.

Co se například stane, když do naší funkce factorial(of:) z výše uvedeného příkladu vložíme nulu nebo záporné číslo?

1
2
3
4
5

func testFactorialOfZero() {
if factorial(of: 0) != 1 {
print(„Faktoriál z 0 je chybný“)
}
}

Náš kód se rozbije:

V reálné aplikaci by tento kód způsobil pád. Nula je v cyklu for mimo rozsah.

Náš kód však neselže kvůli omezení rozsahů Swiftu. Selhává proto, že jsme v našem kódu zapomněli zohlednit nepozitivní celá čísla. Podle definice faktoriál záporného celého čísla neexistuje, zatímco faktoriál čísla 0 je 1.

1
2
3
4
5
6
7
8
9
10
11
12
13

func faktoriál(čísla: Int) -> Int? {
if (číslo < 0) {
return nil
}
if (number == 0) {
return 1
}
var result = 1
for factor in 1…number {
result = result * factor
}
return result
}

Můžeme nyní znovu spustit naše testy a dokonce přidat kontrolu záporných čísel.

1
2
3
4
5

func testFactorialOfNegativeInteger() {
if factorial(of: -1) != nil {
print(„Faktoriál -1 je špatně“)
}
}

Tady vidíte poslední a podle mého názoru nejdůležitější přínos unit testování. Testy, které jsme napsali dříve, zajišťují, že při aktualizaci naší funkce factorial(of:) nerozbijeme její stávající kód.

To je klíčové ve složitých aplikacích, kde musíte kód neustále přidávat, aktualizovat a mazat. Testování jednotek vám dává jistotu, že vaše změny nerozbily kód, který fungoval dobře.

Psaní jednotkových testů v Xcode

S porozuměním jednotkovým testům se nyní můžeme podívat na testovací funkce Xcode.

Při vytváření nového projektu Xcode máte možnost rovnou přidat jednotkové testy. Jako příklad použiji aplikaci pro sledování kalorií. Celý projekt Xcode najdete zde.

Pokud tuto možnost zaškrtnete, bude váš vzorový projekt obsahovat již nakonfigurovaný cíl testování. Vždy jej můžete přidat později, ale já tuto možnost obvykle zaškrtávám ve výchozím nastavení. I když se nechystám psát testy hned, vše bude nastaveno, až se pro to rozhodnu.

Testovací cíl již začíná s kódem šablony pro naše testy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import XCTest
@testable import Calories
class CaloriesTests: XCTestCase {
override func setUpWithError() throws {
// Zde vložte kód nastavení. Tato metoda se volá před voláním každé testovací metody ve třídě.
}
override func tearDownWithError() throws {
// Zde vložte kód pro ukončení. Tato metoda se volá po volání každé testovací metody ve třídě.
}
func testExample() throws {
// Toto je příklad funkčního testovacího případu.
// Použijte XCTAssert a související funkce k ověření, že vaše testy dávají správné výsledky.
}
func testPerformanceExample() throws {
// Toto je příklad testovacího případu výkonnosti.
self.measure {
// Zde vložte kód, jehož čas chcete měřit.
}
}
}

  • Řádek @testable import Calories vám zpřístupní všechny typy v hlavním cíli vašeho projektu. Kód modulu Swift není přístupný zvenčí, pokud nejsou všechny typy a metody explicitně deklarovány jako veřejné. Klíčové slovo @testable vám umožňuje toto omezení obejít a získat přístup i k soukromému kódu.
  • Třída CaloriesTests představuje testovací případ, tj. seskupení souvisejících testů. Třída testovacího případu musí pocházet z XCTestCase.
  • Metoda setUpWithError() umožňuje nastavit standardní prostředí pro všechny testy v testovacím případu. Pokud potřebujete po testech provést úklid, použijete metodu tearDownWithError(). Tyto metody se spouštějí před a po každém testu v testovacím případu. Nebudeme je potřebovat, takže je můžete smazat.
  • Metoda testExample() je test podobný těm, které jsme napsali pro náš faktoriální příklad. Metody testů můžete pojmenovat podle libosti, ale musí začínat předponou test, aby je Xcode rozpoznal.
  • A konečně metoda testPerformanceExample() ukazuje, jak změřit výkon kódu. Výkonnostní testy jsou méně standardní než jednotkové testy a používáte je pouze u klíčového kódu, než který musí být rychlý. Ani tuto metodu nebudeme používat.

Všechny své testovací případy a relativní testy najdete v seznamu v navigátoru Testy v Xcode.

Nové testovací případy můžete do projektu přidat jednoduše vytvořením nového .swift souborů s kódem, jako je ten výše uvedený, ale jednodušší je použít nabídku přidat v dolní části navigátoru testů.

Dobrá architektura usnadňuje testování kódu

V reálném projektu obvykle nemáte volné funkce jako v našem příkladu s faktorem. Místo toho máte struktury, výčty a třídy, které reprezentují různé části vaší aplikace.

Klíčové je uspořádat kód podle návrhového vzoru, jako je MVC nebo MVVM. Dobrá architektura kódu neznamená jen to, že bude váš kód přehledný a snadněji se bude udržovat. Usnadňuje také unit testování.

To nám také pomůže odpovědět na častou otázku: „Který kód byste měli testovat jako první?“

Odpověď zní: začněte od typů modelů.

Důvodů je několik:

  • Pokud se řídíte vzorem MVVC nebo některým z jeho derivátů, budou typy modelů obsahovat obchodní logiku domény. Na správnosti takového kódu závisí celá vaše aplikace, takže i několik testů má významný dopad.
  • Modelové typy jsou často struktury, které jsou hodnotovými typy. Ty se testují mnohem snadněji než referenční typy (za chvíli uvidíme proč).
  • Referenční typy, tedy třídy, mají závislosti a způsobují vedlejší efekty. Abyste pro ně mohli napsat unit testy, potřebujete testovací dvojníky (dummies, stubs, fakes, spies a mocks) a použít sofistikované techniky.
  • Kontroléry (ve smyslu MVC) nebo view modely (ve smyslu MVVM) jsou často připojeny ke zdrojům, jako je diskové úložiště, databáze, síť a senzory zařízení. Mohou také spouštět paralelní kód. Ty ztěžují jednotkové testování.
  • Kód pohledu se mění nejčastěji, což vytváří křehké testy, které se snadno rozbijí. A nikdo nerad opravuje rozbité testy.

Přistupování k hodnotovým typům jako k čistým funkcím pro zjednodušení unit testů

Přidáme tedy do naší aplikace nějaké typy modelů.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

struct FoodItem {
let name: String
let caloriesFor100Grams: Int
let grams: Int
}
struct Meal {
private(set) var items: =
var calories: Int {
var calories = 0
for item in items {
calories += (item.caloriesFor100Grams / 100) * item.grams
}
return calories
}
mutating func add(_ item: FoodItem) {
items.append(item)
}
}

Struktura Jídlo obsahuje jednoduchou logiku pro přidávání položek jídla a výpočet celkového počtu kalorií jídla, které obsahuje.

Jednotkové testování má takový název proto, že byste měli testovat kód v samostatných jednotkách a odstranit všechny závislosti (společné testování závislostí se místo toho nazývá integrační testování). S každou jednotkou se pak zachází jako s černou skříňkou. Nakrmíte ji nějakým vstupem a otestujete její výstup, abyste zjistili, zda je takový, jaký očekáváte.

To bylo v případě našeho příkladu faktoriálu snadné, protože se jednalo o čistou funkci, tj. funkci, jejíž výstup závisí pouze na vstupu, a která nevytváří žádný vedlejší efekt.

To se ale nezdá být případem našeho typu Meal, který obsahuje stav.

Hodnota vrácená vlastností vypočtenou z kalorií nemá žádný vstup. Závisí pouze na obsahu pole items. Metoda add(_:) naopak žádnou hodnotu nevrací a pouze mění vnitřní stav jídla.

Struktury jsou však hodnotové typy a můžeme je považovat za čisté funkce. Protože nemají závislosti, můžete počáteční stav struktury považovat za vstup metody a její stav po zavolání metody za výstup.

(To je jeden z důvodů, proč byste se měli zdržet vkládání referenčních typů dovnitř hodnotových typů).

Ověřování kódu pomocí funkcí assertions z frameworku XCTest

Máme nyní nějaký kód k testování a místo v našem projektu Xcode, kam testy napsat.

Chybí nám poslední ingredience: způsob, jak vyjádřit, zda je náš kód správný, nebo ne.

V příkladu na začátku tohoto článku jsem použil jednoduché příkazy print. To není pro skutečné testování jednotek praktické. Nechcete ztrácet čas procházením protokolů, abyste zjistili, které testy selhaly. Potřebujeme způsob, který na neúspěšné testy přímo ukáže.

V rámci XCTest najdete řadu funkcí assertion, díky kterým Xcode ukáže na testy, které neprošly.

S jejich pomocí můžeme napsat náš první test.

Jak jsem se zmínil výše, jednotkové testy jsou užitečné pro kontrolu okrajových případů, takže se začněme ujišťovat, že prázdné jídlo nemá žádné kalorie.

1
2
3
4
5
6

třída CaloriesTests:
func testEmptyMeal() throws {
let meal = Meal()
XCTAssertEqual(meal.calories, 0, „Prázdné jídlo by mělo mít 0 kalorií“)
}
}

Test XC nabízí obecnou funkci XCTAssert, která umožňuje tvrdit libovolnou podmínku. Tu můžete použít pro jakýkoli test, který napíšete, ale je lepší používat specializovanější assertion funkce, pokud je to možné, jako XCTAssertEqual (výše), XCTAssertNil a další. Ty v Xcode vytvářejí lepší chyby než XCTAssert.

Jednotlivý test můžete spustit kliknutím na kosočtverec vedle jeho názvu ve žlábku editoru kódu.

Pokud test projde, bude zelený, zatímco neúspěšné testy budou označeny červeně. Abyste viděli, jak vypadá neúspěšný test, můžete změnit 0 v testu na libovolné jiné číslo.

Klepnutím na kosočtverec vedle deklarace třídy můžete spustit všechny testy v testovacím případu. Ke spuštění jednotlivých testů nebo testovacích případů můžete také použít Navigátor testů, který jsme viděli výše.

Často chcete spustit všechny testy v projektu najednou, což můžete rychle provést stisknutím kombinace kláves cmd+U na klávesnici nebo výběrem možnosti Test v nabídce Product v Xcode.

Měření pokrytí kódu testem v Xcode

Další, nejčastější otázka týkající se testování jednotek zní: „Jak velkou část kódu byste měli testovat?“

Obvykle se to měří pomocí pokrytí kódu, tj, procento kódu pokrytého vašimi testy. Než si tedy odpovíme na tuto otázku, podívejme se, jak můžete v Xcode měřit pokrytí kódu vašich testů.

Nejdříve musíte nechat Xcode shromáždit pokrytí pro váš cíl testů. Klepněte na tlačítko s názvem projektu v segmentovém ovládacím prvku na panelu nástrojů Xcode (vedle tlačítka stop). Poté v kontextové nabídce vyberte možnost Upravit schéma…

Tam v osnově vyberte možnost Test a přejděte na kartu Možnosti. A nakonec vyberte možnost Shromáždit pokrytí pro. Jeho volbu můžete ponechat na všech cílech.

Nyní můžete znovu spustit testy, což umožní Xcode shromáždit údaje o pokrytí. Výsledek najdete v navigátoru Report, když v osnově vyberete položku Code coverage.

Zde můžete zkontrolovat procenta kódu pokrytého vašimi testy pro celý projekt a jednotlivé soubory.

Tato čísla nejsou pro náš příklad tak významná, protože jsme nenapsali téměř žádný kód. Přesto vidíme, že metoda add(_:) typu Meal má pokrytí 0 %, což znamená, že jsme ji ještě netestovali.

Vlastnost calories computed má naopak pokrytí 85,7 %, což znamená, že v našem kódu jsou nějaké cesty provádění, které náš test nespustil.

V našem jednoduchém příkladu je snadné pochopit, která cesta to je. Testovali jsme pouze kalorie prázdného jídla, takže kód ve smyčce for se nespustil.

U složitějších metod to však nemusí být tak jednoduché.

Pro to si můžete v editoru Xcode vyvolat proužek pokrytí kódu. Tu najdete v nabídce Upravit možnosti editoru v pravém horním rohu.

Tím se zobrazí proužek, který vám ukáže pokrytí pro každý řádek kódu.

Čísla vám řeknou, kolikrát byl každý řádek během testování proveden. Červeně vidíte, které řádky nebyly spuštěny vůbec (plné) nebo byly spuštěny jen částečně (pruhované).

Proč je cílení na konkrétní pokrytí kódu nesmyslné a co byste měli dělat místo toho

Takže, jaké procento je dobré procento pokrytí kódu?

Názory se značně liší. Někteří vývojáři si myslí, že stačí 20 %. Jiní se přiklánějí k vyšším číslům, například 70 % nebo 80 %. Existují dokonce vývojáři, kteří se domnívají, že přijatelných je pouze 100 %.

Podle mého názoru je pokrytí kódu nesmyslná metrika. Můžete ji použít jako informaci při rozhodování o testování, ale neměli byste ji považovat za cíl, kterého musíte dosáhnout.

Abyste viděli proč, napišme další test, který pokryje kód, který jsme ještě netestovali.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

třída CaloriesTests:
let banana = FoodItem(name: „Banana“, caloriesFor100Grams: 89, grams: 118)
let steak = FoodItem(name: „Steak“, caloriesFor100Grams: 271, grams:
let goatCheese = FoodItem(name: „Goat Cheese“, caloriesFor100Grams: 364, grams: 28)
func testEmptyMeal() throws {
let meal = Meal()
XCTAssertEqual(meal.calories, 0, „Prázdné jídlo by mělo mít 0 kalorií“)
}
func testCalories() {
var meal = Meal()
meal.add(banán)
meal.add(steak)
meal.add(kozí sýr)
XCTAssertEqual(meal.items.count, 3)
XCTAssertEqual(meal.calories, 534)
}
}

Pokud znovu spustíte všechny testy, získáte 100% pokrytí souboru Model.swift. Vypadá to dobře, že?“

Nyní jděte a odstraňte metodu testEmptyMeal(). Pokud spustíte zbývající testy samostatně, zjistíte, že pokrytí pro naše typy Meal je stále 100 %.

Už to vám ukazuje, že číslo 100 % může dávat falešný pocit bezpečí. Víte, že nyní netestujeme všechny okrajové případy pro vypočtenou vlastnost kalorie. A přesto to naše pokrytí testy neodráží.

Dodal bych, že 100% pokrytí je nejen zavádějící, ale dokonce škodlivé. Některý kód ve vašem projektu je náchylný k neustálým změnám, zejména nejnovější kód a zejména v pohledech.

Pokrytí 100 % znamená, že budete muset vytvářet pouze křehké testy, které se neustále rozbíjejí a vy je musíte opravovat. Tyto testy neodhalí chyby. Rozbíjejí se proto, že se mění funkčnost vašeho kódu.

A tady se dostáváme k bodu, o kterém nikdo nemluví.

Psaní testů není žádná zábava, přestože to někteří vývojáři tvrdí. Opravování testů je ještě menší zábava. Když to budete dělat příliš často, budete unit testy nenávidět a odsunete je úplně stranou.

Psychologie je pro vývoj softwaru stejně důležitá jako osvědčené postupy. Nejsme stroje.

Na jaké číslo byste se tedy měli zaměřit? To vyžaduje rozsáhlejší diskusi, ale zde vám shrnu svůj názor.

Pište užitečné testy, začněte nejstarším kódem ve vašem projektu, který se nebude měnit. Pokrytí kódu používejte pouze ke zjištění, který kód ještě není otestován, ale nepoužívejte ho jako cíl nebo k rozhodování, kdy napsat další testy. Pokrytí 20 % je lepší než 80 %, pokud jsou vaše testy smysluplné a testujete správný kód.

Jak jsem zmínil výše, typy modelů obsahují nejkritičtější kód ve vašem projektu, který se také obvykle nejméně mění. Začněte tedy tam.

Obvykle se neobtěžuji s testováním každé jednotlivé cesty provádění. Například v reálném projektu bych nepsal metodu testEmptyMeal(), kterou jsem vám ukázal výše. Ano, je zásadní testovat okrajové případy, ale nejsou ani všechny důležité.

Obvykle bych napsal pouze test testCalories(). Ten mi řekne, že můj kód funguje, a upozorní mě, kdybych později při změně této metody udělal chybu.

Jistě, nepokryje všechny cesty, ale tento kód je tak jednoduchý, že je to jen ztráta času. Trávit čas psaním skutečného kódu pro funkce, které pomáhají uživatelům, je důležitější než testovat každou cestu provádění.

Vím, že za tento názor dostanu od některých vývojářů flack, ale je mi to jedno. Pokrytí testů je jedna z těch debat, které nikdy neskončí.

Kód, který napíšete, je většinou správný. Není třeba být paranoidní. Mít spoustu testů, které se rozbijí pokaždé, když něco změníte, je přítěž, ne výhoda.

Váš čas a vůle jsou omezené. Věnujte je důležitějším úkolům.

Architektura aplikací SwiftUI s MVC a MVVM

Je snadné vytvořit aplikaci tak, že hodíte dohromady nějaký kód. Bez osvědčených postupů a robustní architektury však brzy skončíte s nezvládnutelným špagetovým kódem. V této příručce vám ukážu, jak správně strukturovat aplikace SwiftUI.

ZÍSKEJTE KNIHU ZDARMA hned teď

.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.