Qeetup vol 10 🍾
Radim Štěpaník
Testování v Node.js
Slido
O čem to dnes bude?
O tom jak jsme se dostali k téměř 100% coverage aniž by nám na tom nějak obzvlášť záleželo
( a už nás to nebolí 🤗)
Testování?
Proč vůbec testovat?
Nebudeme mít žádné bugy 🐛
Proč vůbec testovat?
Testování je drahé
Proč vůbec testovat?
Budeme testovat až nakonec
Automatizaceautomatizace a zase ta automatizace
Co není automatizované tak prostě ve světě IT nefunguje.
(motivace pro začátek - ukazujte si čísla)
Automatizaceautomatizace a zase ta automatizace
Easy part - just npm run test 🍾
Automatizaceautomatizace a zase ta automatizace
Hard part
- databáze
- vývojový proces
- dobré návyky vývojářů
Co nám říká teorie
- rozdělení testů
- unit testy
- integrační testy
- funkční testy
Sedí tento model na to co děláme?
- monolitické aplikace
- serverless, mikroslužby, služby
- nechci psát kód který nepotřebuji
- v kódu navíc můžu udělat chybu
Sedí tento model na to co děláme?
- integrační testy rulezz 💪
- implementační detaily
- integrated testy
Co mockovat a co ne
- obecně je dobré mít "naostro" vše co dokážeme v rámci aplikace ovlivnit
- databáze - používáme
- API 3. stran - mockovat
Dopady na psaní kódu
Příklad - Aplikace Cirkus.io🎪
- Aplikace 🎪 Cirkus - webová aplikace, představení, uživatelé kupují lístky na představení
- It's to simple!
- Zaměříme se na use case získání uživatele
export const getUser = async (req: Request, res: Response) => {
const user = await UserMongooseModel.findOne({entityId: req.params.userId}).exec()
return res.json(user);
};
Příklad - Aplikace Cirkus.io🎪
- Pokud uživatel nemá nastaveného avatara přiřaď default
const DEFAULT_AVATAR = "/defaultAvatar.jpg"
export const getUser = async (req: Request, res: Response) => {
const user = await UserMongooseModel.findOne({entityId: req.params.userId}).exec()
const avatar = user.avatar ?? DEFAULT_AVATAR
return res.json({...user, avatar});
};
Příklad - Aplikace Cirkus.io🎪
- Všem uživatelům s více než 50 vstupy nastav členství gold
const DEFAULT_AVATAR = "/defaultAvatar.jpg"
export const getUser = async (req: Request, res: Response) => {
const user = await UserMongooseModel.findOne({entityId: req.params.userId}).exec()
const avatar = user ?? DEFAULT_AVATAR
const numberOfVisits = await EntranceHistory.find({userId: req.params.userId})
.count()
.exec()
let membership = "basic"
if (numberOfVisits > 50 ) {
membership = "gold"
}
return res.json({...user, avatar, membership});
};
Příklad - Aplikace Cirkus.io🎪
- Pokud se jedná o personál cirkusu přiřaď speciální příznak
const DEFAULT_AVATAR = "/defaultAvatar.jpg"
export const getUser = async (req: Request, res: Response) => {
const user = await UserMongooseModel.findOne({entityId: req.params.userId}).exec()
const avatar = user ?? DEFAULT_AVATAR
const numberOfVisits = await EntranceHistory.findOne({userId: req.params.userId}).count().exec()
let membership = "basic"
if (numberOfVisits > 50 ) {
membership = "gold"
}
const hrSystemApi = axios.create(config.internalHRSystemUrl)
const response = await hrSystemApi.get(`/user/${req.params.userId}`)
const meta = {hrSystemInfo: response.data}
return res.json({...user, avatar, membership, meta});
};
Příklad - Aplikace Cirkus.io🎪
- Pokud je uživatel zaměstnanec, byl narozen za úplňku a zároveň je dneska venku hezky ☀️
const DEFAULT_AVATAR = "/defaultAvatar.jpg"
export const getUser = async (req: Request, res: Response) => {
const user = await UserMongooseModel.findOne({entityId: req.params.userId}).exec()
const avatar = user ?? DEFAULT_AVATAR
const numberOfVisits = await EntranceHistory.findOne({userId: req.params.userId}).count().exec()
let membership = "basic"
if (numberOfVisits > 50 ) {
membership = "gold"
}
const hrSystemApi = axios.create(config.internalHRSystemUrl)
const response = await hrSystemApi.get(`/user/${req.params.userId}`)
const meta = {hrSystemInfo: response.data}
const calendarApi = axios.create(config.calendarApiUrl)
const isFullMoon = (await calendarApi.get(`/date/${user.birthDate}/isFullMoon`)).data
const wheaterApi = axios.create(config.wheaterAppUrl)
const wheater = (await wheaterApi.get(`/whater`)).data
if (meta.hrSystemInfo && isFullMoon && wheater.temperature > 30 && wheater.currentConditions === "sunlight") {
membership = "supergold"
}
return res.json({...user, avatar, membership, meta});
};
Příklad - Aplikace Cirkus.io🎪
- Co to pro nás znamená
- v rámci jedné funkce sleduji několik různých stavů
- pro otestování business funkcionality musím jít poměrně hluboko do implementace
- počáteční nastavení testu se stává velmi neprůhledné, čím více vstupních proměnných do funkce, tím větší pravědpodobnost že se stane chyba
- side effects - co když selže některé API?
Jak se tomu vyvarovat
- nastavit si robustnější přístup k architektuře
- závislosti v kódu
- OOP - dependency injection
- functional prorgramming - pure functions
- psát kód tak, abychom nepředávali zbytečné závislosti❗️
Aplikace jako složený container
- všechny součástky aplikace vnímáme stejně, jsou to dílky skládačky
- v rámci zdrojového kódu existují pouze implementace jednotlivých komponent
- každá komponenta, služba, controller definuje rozhraní se kterým komunikuje s ostatními
- jednoznačná definice závislostí, toho co je zapotřebí
Aplikace jako složený container
- v rámci integrace - jednoduše mohu otestovat jakoukoliv službu nebo controller, služba je prostě vytažena z kontejneru a testována
- v rámci izolovaného testu - díky jasně definovanému rozhraní mohu vytvořit službu pro potřeby testu (předám mock definice)
- mockování - díky tomu, že jsou služby předané v kontejneru a mohu si je odsud vytáhnout, mohu namockovat jakoukoliv službu nebo její metodu
Dopady na psaní kódu
- integrační testy rulezz 💪
- unit testování "těžkých"
komponent - aplikace container
Příklad - Aplikace Cirkus.io🎪
- Jak to udělat lépe?
Integration tests
Special test for HRSystem
Testing of business logic
Příklad - Aplikace Cirkus.io🎪
- Jak to udělat lépe?
export const getUser = (fullUserService: FullUserService) => {
return async (req: Request, res: Response) => {
const user = fullUserService.getUser((req.params.id))
return res.json(user);
};
}
class FullUserService {
constructor(
private dataProvider: FullUserDataProvider,
private membershipResolver: MembershipResolver
) {}
async getUser(id: string): FullUser {
const user = this.dataProvider.getUser(id)
const [ hrSystemInfo, wheater, numberOfVisits] = await Promise.all([
this.dataProvider.getWheater(),
this.dataProvider.isFullMoon(user.birthDate),
this.dataProvider.numberOfVisits(user)
])
const membershipData = {user, hrSystemInfo, wheater, numberOfVisits}
const membership = this.membershipResolver.resolve(membershipData)
return {...user, membership}
}
}
Dopady na psaní kódu
- psát kód tak, aby bylo možné otestova jakoukoliv část aplikace bez obtíží
- nese nároky na strukturu aplikace
- škálovat kód do menších bloků
- využití dependency injection nebo funkcionálního programování
Testy blízko vašeho kódu
- zjednodušení při code reviews
- jasně viditelné změny
- neděláme rozdíly co je integrační test a co unit test
Testování jako dokumentace aneb archeologie v praxi 🙉
- jako správně dokumentovat funkcionalitu systému?
- proč je v kódu takhle divná věc
- jak se používá tahle knihovna?
- myslete na to, že testy a jejich popis můžou někomu jednou hodně pomoct
Slovo závěrem 👋
- budťe připraveni na změnu a nebojte se je dělat ❗️
- nebuďte klikači UI a postmana
- zdokumentujte to jakým způsobem jste přemýšleli nad tvorbou featury
- myslete na ty co přijdou po vás
Díky za pozornost
Node.js tooling - supertest/apollo
- jest, mocha
- supertest - pro
První pokusy o testování
- skvělý projekt pro testování
- skvělé nástroje proto to udělat
- proč to nefungovalo?
Qeetup
By Radim Štěpaník
Qeetup
- 436