Testing in Action

16.09.2024

cover

Har qanday kashfiyot ommaga taqdim etilishidan oldin turli xil testlardan o'tadi. Albatta dasturchilar tomonidan ishlab chiqiladigan tizimlar va dasturlar ham bundan mustasno emas. Bugun e'tiborimizni ko'proq dasturlarimizni testlashga qaratamiz.

UI Design vs User Testing

Unit test

Unit so'zini ingliz tilidan "nuqta" yoki "qism" deb tarjima qilsangiz bo'ladi. Odatda dasturni aniq bir qismini test qilmoqchi bo'lsangiz siz unga unit testlar yozasiz. Tasavvur qiling, universitetda professor bo'lib ishlaysiz, va 10 000 talabani yozgan kodlarini test qilayabsiz. Inson sifatida ularni tez tekshirish imkonsiz, ammo kompyuterlar buni qila olishadi. Oddiyroq misol sifatida:

# Input: join_words("hello", "it's", "me", "Otabek")
# Output: "hello it's me Otabek"
def join_words(*words: tuple[str], sep: str = " ") -> str:
	return sep.join(words)

Yuqoridagi funksiyani olaylik, siz bir nechta so'zlarni yozsiz, funksiya ularni birlashtirib beradi (sep orqali istalgan shaklda so'zlarni ajratishingiz mumkin). Barcha o'quvchilar sizga uy ishlarini topshirishdi ularni test qilish uchun oddiy unit test yozish kifoya qiladi.

import unittest # unittest ni import qilasiz

class TestJoinWords(unittest.TestCase): # class nomini Test... bilan boshlang
	# metod nomlari ham test_... bilan boshlangani yaxshi
	def test_simple(self):
		# assertEqual shunchaki tenglikni tekshiradi, ostida esa -> add(1, 2) == 3
		# ya'ni funksiya("qiymat") yuritganingizda uning natijasi "natija"ga teng bo'lishi kerak degani
        self.assertEqual(join_words("it's", "me"), "it's me")
        self.assertEqual(join_words("testing", "in", "action", sep="-"), "testing-in-action")

if __name__ == '__main__':
	unittest.main()

Unit Test haqida batafsilroq yana izlanishni va bundan keyin ko'proq testlar yozishni tavisya qilaman. Unit testlar klasslar, metodlar, va funksiyalar uchun yozilgani yaxshi. Ularni har qanday xolatda to'g'ri ishlayotganini tekshirish uchun eng zo'r tanlov deb bilaman.

Integration test

Sizda bir dastur bor, minglab foydalanuvchilarga yaxshi xizmat qilayabdi. Unga yangi imkoniyatlar qo'shdingiz. Yangi imkoniyat dasturga muvaffaqiyatli qo'shilganini, boshqa servislar bilan birga xatosiz ishlashi va boshqa shunga o'xshash xolatlarni test qilish uchun aynan Integration testlar yoziladi.

Integration so'zi bu birlashish ma'nosida keladi. Yani ishlab turgan yoki bor narsaga yangi narsani birlashtirsangiz yoki ulasangiz uni buzilib qolmasligi uchun shunday testlar yozasiz.

Integration testlarni men bilgan 2 turi mavjud, Big bang va Incremental. Big bang nomidan ma'lumki dasturni barcha qismlarini bittada test qilasiz, va bu odatda kichik loyihalarda yoziladi, chunki katta loyihalarda juda ko'p vaqt va resurs olishi tabiiy. Incremental turida esa har bir modullarni va imkoniyatlarni guruhlarga bo'lib chiqgan xolda test qilasiz. Asosan katta dasturlarda ko'rishingiz mumkin bunday testlarni. Incremental yo'ldan asosan monorepo yo'lidan yurgan kompaniyalar foydalanishadi.

Unit testlar bir kichik funksiya va class metodlarni test qilgan bo'lsa, integration testlar ularni birgalikda to'g'ri ishlab, to'g'ri natija qaytarishini test qiladi.

# Misol uchun oddiy API serverni test qilayabmiz deb olaylik
def user_info(name: str, age: int) -> dict:
    return {"user": name, "age": age}

def display_user_info(data: dict) -> str:
    return f"User {data['user']} is {data['age']} years old."

def get_user_info(name: str, age: int) -> str:
    data = get_data(name, age)
    return display_user(data)

# Integration test qismi
assert get_user_info("Otabek", 23) == "User Otabek is 23 years old."
assert get_user_info("John", 14) == "User John is 14 years old."

Functional test

Functional testlardagi g'oya bu "O'zi bu narsa biz kutgandek ishlayabdimi?" savoliga javob olish xisoblanadi. Odatda bunday testlarni yozishdan maqsad biznes logikalar to'g'ri ishlashini tekshirishdan iborat. Biznes logika bu "biznes yechayotgan muammoni, biznes tasvirlagandek yechish". Asosiy maqsad dasturni qanday ishlashi emas, balkim uni to'g'ri yecha olishi. Menimcha bunga misol keltirish shart emas : )

E2E (End-to-End) test

End-to-end tushunchasi bir qarashda boshidan-oxirigacha deb tarjima qilinishi mumkin, ammo bu ancha kengroq tushuncha. Bu yerda 2ta end so'zi ishlatilayabdi:

  • birinchi end: bu server. Asosan xizmat ko'rsatish va kelgan so'rovlarni bajarish vazifasini bajaradi.
  • ikkinchi end: bu user. Asosan serverni xizmatlaridan foydalanuvchi tomon deb olsak bo'ladi.

User uchun server xabarni yetkazish kerak bo'lgan eng so'nggi manzil xisoblanasa, serverlar uchun bu teskarisi, ya'ni foydalanuvchiga xabar yetib borsa vazifa bajarildi degan xisoblanadi. End-to-end test qilish degani siz yozgan testlar xuddi foydalanuvchidek o'zini tutishi kerak degani. Ya'ni sizni serverlaringiz real foydalanuchilarga xizmat ko'rsatishdan oldin xuddi real foydalanuvchidek tizimni ishlatadigan testlarga muvaffaqiyatli xizmat ko'rsatishi kerak degani. Qisqacha kod bilan misol keltirish qiyin, ammo meni tushundingiz deb umid qilaman (kod yozilsa bu maqola emas, git repo bo'lib qolishi mumkin).

End-to-End testing meme

Canary test

Konda ishlaydigan ishchilar konga kirishdan oldin biror qushni kon ichiga uchirib ko'rish orqali kon xavfsizligini tekshirishgan. Agar qush o'lib qolmasa kon xavfsiz bo'ladi, o'lsa konga hech kim kirmaydi. Va xuddi shunday tushuncha dasturlashda ham bor. Biz kichik foydalanuvchilar guruhi uchun tizimdagi xatoliklarni topishlari uchun alohida kichik deployment qilishimizga to'g'ri keladi. Bu jarayonni canary deployment ham deyishadi, va siz tanigan insonlar yangi dastur imkoniyatlarini boricha ishlatib ko'rishadi, xatoliklar bo'lsa ular haqida texnik bo'limga xabar qilishadi. Agar hammasi muvaffaqiyatli tugasa demak uni butun ommaga taqdim qilishadi.

Canary test meme

Stress test

Tizimni down (dumalatib) qo'yadigan darajada og'ir so'rovlar yoki ishlarni qildirish orqali uni sinashingiz stress testing xisoblanadi. Misol uchun bir foydalanuvchi cheksiz loop orqali tizimga so'rovlar yuborsa server o'chib qolmasligini ta'minlash, foydalanuvchi bir emas bir nechta og'ir fayllar ustida amal bajarganda dastur qotmasdan yaxshi ishlashini ta'minlash uchun uni stress test qilasiz. Misol uchun mana bunday oddiy test:

def test_infinite_loop():
	try:
		while True:
			append_new_users()
	except MemoryError:
		print("Oops! We hit a breaking point!")

def test_heavy_tasks():
	try:
		while True:
			do_heavy_task()
	except MemoryError:
		print("Oops! We hit a breaking point!")

UI Testing

UI ni test qilish uchun UI testlar yoziladi. Dasturingiz pixel perfect yoki siz istayotgan matnlar aynan o'sha saxifada bormi, unga berilgan classlar, ranglar va attribute'lar to'g'rimi yo'qmi degan savollaringizga javobni aynan UI test qilish orqali olasiz. Misol uchun React orqali oddiy bir matn borligini tekshiradigan bo'lsak tahminan shunday test yozamiz :

// React bilan oddiy UI test yozamiz
import { render, screen } from '@testing-library/react'
import App from './App'

test('renders welcome message', () => {
  render(<App />)
  expect(screen.getByText(/welcome/i)).toBeInTheDocument()
})

Monkey test

Monkey test meme

Biror qurilmani maymunga hech berib ko'rganmisiz? Bermagan bo'lsangiz borib berib ko'ring, men ham bermaganman (xazil). Agar maymun qo'liga yangi qurilma tushsa, maymun uni har ko'yga soladi, turli tugmalarni bosib ko'radi. Eng asosiysi siz uni nima qilishini tahmin qila olmaysiz. Netflix tomonidan o'ylab topilgan ushbu test turi juda samarali. Agar maymunni testidan dastur yaxshi o'tsa, odamnikidan xavotir olmasangiz bo'ladi.

import random

def click_button(buttons):
    return random.choice(buttons)

# Tugmalarni tartibsiz tarzda bosishi uchun test
buttons = ['Save', 'Cancel', 'Delete']
clicked_button = click_button(buttons)
assert clicked_button in buttons  # buzilmaydi deb umid qilamiz

Yuqoridagi test shunchaki sizga overview berish uchun. Aslida monkey test ancha murakkabroq bo'ladi.

Fuzz Testing

Misol uchun first_name, last_name degan kataklar berildi. Ularga ism, familya yozish kerakligini hamma biladi. Ammo jpriqlar shu yerga ham raqam, true/false, {"name": "Jprqjon"}, 👑KING👑 deb yozib tashlashadi. Siz yozgan funksiya faqat raqam qabul qiladi deylik, ammo dastur har qanday xolatda ham o'chib qolmasligi va to'g'ri xabar ko'rsatish orqali foydalanuvchini ogohlantirishi muhim. Bunday xolatlarda esa fuzz (noaniq) testlar yozishingiz kerak. Go dasturlash tilida oddiyroq test yozamiz (chunki ma'lumot turlari static typed):

package main

import (
    "fmt"
    "testing"
)

// stringlarni teskari qiluvchi funksiya
func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

// fuzz test uchun funksiya
func TestReverse(f *testing.F) {
    testCases := []string{"hello", "world", "", "a", "racecar", "Go is fun!"}
    for _, tc := range testCases {
        f.Add(tc)
    }

    f.Fuzz(func(t *testing.T, input string) {
        reversed := Reverse(input)
        doubleReversed := Reverse(reversed)
        if input != doubleReversed {
            t.Errorf("Expected %q but got %q", input, doubleReversed)
        }
        if len(reversed) != len(input) {
            t.Errorf("LengthError: input length %d, reversed length %d", len(input), len(reversed))
        }
    })
}

func main() {
    fmt.Println("Run `go test` to execute fuzz testing.")
}

Others

Va boshqa juda ko'plab test turlari mavjud. Ushbu maqolada imkon qadar kod orqali misol keltirsa bo'ladiganlarini yoritishga harakat qildim, qolganlari internetda ham bor yoki tajriba qilib ko'rsangiz bo'ladi.

Regression test, security test, A-B test, smoke test, load test, performance test, acceptance test va boshqalar haqida balkim kelajakda ham gaplasharmiz.