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
https://engineering.atspotify.com/2018/01/11/testing-of-microservices/

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