ტესტირება & TDD-ის მე-2 ნაწილი. შესავალმა მოგცათ პირამიდა, TDD-ის ციკლი და ხუთი სატესტო დუბლიორი. ეს სესია სიღრმეში მიდის: გენერატორები, რომლებიც სასაზღვრო შემთხვევებს ნადირობენ, კონტრაქტები სერვისებს შორის, თქვენივე ტესტების ტესტირება და დროის, პარალელურობისა და იმ საგნების შავი ხელოვნება, რომლებიც თქვენ არ გეკუთვნით.
ეს ნაწილი 2-ია — პირდაპირ ეყრდნობა შესავალ დეკს, ამიტომ ვვარაუდობთ, რომ თავისუფლად ფლობთ მაგალითებზე დაფუძნებულ ტესტებსა და arrange-act-assert ფორმას. ნახტომი აქ ასეთია: ერთი ხელით შერჩეული შემავალი მნიშვნელობისთვის შედეგის მტკიცების ნაცვლად თქვენ აცხადებთ თვისებას, რომელიც ყველა შემავალისთვის უნდა სრულდებოდეს, შემდეგ კი ფრეიმვორკს ასობით მათგანს აწარმოებინებთ — მათ შორის ისეთ სასტიკებს, რომლებსაც არასდროს აკრეფდით.
fast-check, Hypothesis და jqwik ამ იდეას JS-ში, Python-სა და JVM-ში ატარებენ.მაგალითებზე დაფუძნებული ტესტები იქ იჭრებიან, სადაც უკვე იყურეთ. გენერატორები კი იქ, სადაც არა — შემდეგ კი ჩავარდნას ერთსტრიქონიან რეპროდუქციამდე კვეცავენ.
NaN-ს.// fast-check (TypeScript) import fc from "fast-check" test("decode(encode(x)) === x for ALL orders", () => { fc.assert( fc.property(orderArb, (order) => { const back = decode(encode(order)) expect(back).toEqual(order) // the invariant }), { numRuns: 500, seed: 42 } // reproducible ) })
გენერაცია → გაშვება → ჩავარდნისას შეკვეცა მინიმალურ კონტრმაგალითამდე, შემდეგ კი მისი მოხსენება seed-თან ერთად.
იშვიათად გაქვთ ორაკული, რომელიც სწორ პასუხს ხელახლა გამოთვლის. ეს ხუთი არქეტიპი სწორედ ის გზაა, რომლითაც გამოცდილი ტესტერები თვისებებს ორაკულის გარეშე პოულობენ.
ყველაფერი, რაც სერიალიზდება, პარსდება, იკუმშება ან იშიფრება, უცვლელად უნდა დაბრუნდეს. ყველაზე ნაყოფიერი თვისება, რომლის დაწერაც შეიძლება — უფასოდ იჭერს დანაკარგიან სასაზღვრო შემთხვევებს (ბოლო ჰარეები, Unicode-ის ნორმალიზაცია, -0).
sort(xs)-ის შემდეგ: შედეგს იგივე სიგრძე აქვს, შემავალის გადანაცვლებაა და ყოველი ელემენტი ≤ მომდევნოზე. თქვენ სისწორის ფორმას ამტკიცებთ პასუხის ხელახლა გამოთვლის გარეშე.
გზის ნორმალიზება, სტრიქონის მოკვეცა, მიგრაციის ორჯერ გაშვება — ხელახლა გაკეთებამ არაფერი უნდა შეცვალოს. კლასიკური წყარო რეალური ბაგებისა "გამეორებისთვის უსაფრთხო" კოდში.
შეამოწმეთ სწრაფი იმპლემენტაცია ნელ, გულუბრყვილო და სანდო ვერსიასთან — ან ძველ კოდთან რეფაქტორინგის დროს. ასევე: კომუტაციურობა (add(a,b) === add(b,a)) და შედარება საეტალონო ბიბლიოთეკასთან.
როცა ორაკული საერთოდ არ არსებობს: დაამტკიცეთ მიმართება. "კატის" ძებნამ სულ მცირე იმდენივე შედეგი უნდა დააბრუნოს, რამდენიც "შავი კატის" ძებნამ. ყველა ფასის 2-ზე გამრავლებამ კალათის ჯამი უნდა გააორმაგოს. ძლიერია ML-სა და ძებნისთვის.
მოდელზე დაფუძნებული / stateful ტესტირება აგენერირებს ბრძანებების შემთხვევით თანმიმდევრობებს თქვენი სისტემისა და პატარა in-memory მოდელის წინააღმდეგ და ამტკიცებს, რომ ისინი ყოველ ნაბიჯზე თანხმდებიან — fast-check-ის fc.commands, Hypothesis-ის RuleBasedStateMachine. ის პოულობს თანმიმდევრობისა და პარალელურობის ბაგებს, რომლებსაც ერთგამოძახებიანი თვისება ვერასდროს იპოვიდა.
ორი სერვისი API-ზე თანხმდება. სერვისი B ცვლის ველს; სერვისი A პროდაქშენში ტყდება — და არცერთმა იუნიტ-ტესტმა ეს ვერ დაიჭირა, რადგან თითოეულმა მხარემ მეორე საკუთარი დაშვებებით დაამოკა. კონტრაქტული ტესტირება ამ დაშვებებს საერთო, შემოწმებად არტეფაქტში ამაგრებს.
კონსიუმერის მოლოდინები იქცევა ფაილად, რომელიც პროვაიდერმა უნდა დააკმაყოფილოს — შემოწმებული ორ ცალკე პაიპლაინში, არასდროს ერთად.
can-i-deploy ეკითხება ბროკერს: "ამ ვერსიის ყოველი კონსიუმერი ისევ თავსებადია?" — გაშვებამდე.// Pact v3 consumer test (the WEB side) const pact = new PactV3({ consumer: "Web", provider: "Orders" }) pact .given("order 42 exists") // provider state .uponReceiving("a request for order 42") .withRequest({ method: "GET", path: "/orders/42" }) .willRespondWith({ status: 200, body: { id: integer(42), total: decimal(9.9) } }) await pact.executeTest(async (mock) => { const o = await fetchOrder(mock.url, 42) expect(o.id).toBe(42) // drives the contract })
დაამთხვიეთ ტიპი და არა ზუსტი მნიშვნელობა — კონტრაქტი ამაგრებს იმ ფორმას, რომელზეც კონსიუმერი დგას, და ითმენს ახალ მონაცემებს, რომლებსაც ის უგულებელყოფს.
ყოველი ურთიერთქმედება ასახელებს წინაპირობას. პროვაიდერის მხარეს ამ მდგომარეობის სახელს ფიქსტურაზე აბამთ (ჩაწერეთ სტრიქონი, დაასტაბეთ დამოკიდებულება), რომ შემოწმება დეტერმინისტული იყოს.
ბროკერი (PactFlow ან თვითჰოსტინგი) თვალს ადევნებს, კონსიუმერისა და პროვაიდერის რომელი ვერსიები შემოწმდა ერთმანეთთან. can-i-deploy რელიზს მწვანე მატრიცაზე აბამს.
Pact ფარავს შეტყობინებების კონტრაქტებსაც (Kafka, რიგები). მხოლოდ სქემის გარანტიებისთვის OpenAPI / Protobuf + buf-ის მრღვევი ცვლილებების შემოწმება ავსებს სურათს — ისინი ფორმას ამოწმებენ და არა კონსიუმერის განზრახვას.
დაფარვა გეუბნებათ, რომ ხაზი გაეშვა. ის არაფერს ამბობს იმაზე, შეამჩნევდა თუ არა თქვენი ტესტი, რომ ეს ხაზი მცდარია. მუტაციური ტესტირება სწორედ ამას ზომავს — თქვენს კოდს განზრახ ტეხავს და ამოწმებს, აყვირდება თუ არა ნაკრები.
< ხდება <=, + ხდება -, return true ბრუნდება, ინსტრუქცია იშლება. თითოეულზე უშვებს თქვენს ნაკრებს. თუ ტესტი ჩავარდა, მუტანტი მოკლულია (კარგია — თქვენმა ტესტებმა ბაგი დაიჭირეს). თუ ყველა ტესტი მაინც გადის, მუტანტი გადარჩა — ეს კი მტკიცების ნამდვილი ხარვეზია.age >= 17 გადარჩა → არცერთი ტესტი არ ამოწმებს ზღვარს 17-ზე. ეს გადარჩენილი ზუსტი მითითებაა, რომელი ტესტი დაწეროთ შემდეგ.
killed / (total − equivalent). გადარჩენილი არის ადგილი, სადაც თქვენი კოდი შეიძლება მცდარი იყოს და არცერთი ტესტი ამას არ გეტყვით. ეს ხაზების დაფარვაზე ბევრად ძლიერი სიგნალია — და ბევრად ნელი გამოსათვლელი.i < n და i != n ციკლში, რომელიც მხოლოდ ზრდის მთვლელს, იდენტურ შედეგს იძლევა, ამიტომ მისი მოკვლა არცერთ ტესტს არ შეუძლია. ეკვივალენტური მუტანტები ზოგად შემთხვევაში გადაუწყვეტელია და სწორედ ისინია მიზეზი იმისა, რომ 100% მცდარი სამიზნეა — ისინი მნიშვნელს მოუკვლელი ხმაურით ბერავენ. დაედევნეთ იმ გადარჩენილებს, რომლებიც ნამდვილ ქცევას ასახავენ, და არა ბოლო რამდენიმე პროცენტს.// runs every line — asserts almost nothing test("applies discount", () => { const total = price(100, { vip: true }) expect(total).toBeDefined() // mutants survive freely })
// pins the value AND the boundary test("VIP gets 20% off", () => { expect(price(100, { vip: true })).toBe(80) expect(price(100, { vip: false })).toBe(100) })
ის ნელია — ყოველი მუტანტი თავიდან უშვებს თქვენი ნაკრების (ნაწილს), ამიტომ სრულ გაშვებას საათები შეიძლება დასჭირდეს. გახადეთ პრაქტიკული: CI-ში გაუშვით მხოლოდ შეცვლილ ფაილებზე (Stryker-ის ინკრემენტული რეჟიმი, PIT-ის history), დააყენეთ გონივრული ზღვარი (ჩავარდნა, ვთქვათ, შეხებულ კოდზე 60%-ის ქვემოთ) და გადარჩენილთა სია მიიღეთ სამუშაო სიად და არა ამაო მეტრიკად.
ნაკრების ლპობის მიზეზი ცუდი მტკიცებები კი არა — უმართავი სატესტო მონაცემები და ფარული კავშირებია. 40-სტრიქონიანი მოსამზადებელი ბლოკი, საერთო ცვალებადი ფიქსტურა, თანმიმდევრობაზე დამოკიდებული გავლა: სწორედ ეს აქცევს მწვანე ნაკრებს არასანდოდ. დააპროექტეთ ისინი განზრახ.
aVipCustomer()) და ფაბრიკები (Factory Boy, fishery, FactoryBot), რომლებიც ბაზაშიც ინახავენ.// defaults make every test start from "valid" const aUser = (over: Partial<User> = {}): User => ({ id: randomId(), name: "Test User", status: "active", createdAt: clock.now(), ...over, // override ONLY what matters }) // the test states its one relevant fact, nothing else const banned = aUser({ status: "banned" }) expect(canCheckout(banned)).toBe(false)
ერთი ფაბრიკა, მრავალი ვარიანტი. ტესტი ასახელებს მხოლოდ იმ ველს, რომელსაც ამოწმებს — დანარჩენი სანდო ნაგულისხმევია.
შემოსაზღვრული მომზადება/დაშლა — დროებითი ბაზა, შესული კლიენტი, ჩაწერილი სტრიქონი. pytest-ის @fixture function/module/session სკოუპით სწორედ ეს მოდელია: აცხადებთ დამოკიდებულებას, გამშვები კი აბამს და შემდეგ შლის მას. რისკი: session-სკოუპის ფიქსტურა, რომელიც ერთმა ტესტმა შეცვალა, მომდევნოში გადმოიღვრება.
მოთხოვნისთანავე აწარმოებს ახალ, დამოუკიდებელ ინსტანციებს — სურვილისამებრ შენახვითაც. ერთეულებისთვის არჩიეთ ფაბრიკები საერთო ფიქსტურებს: ყოველი ტესტი თავის მონაცემებს ფლობს, ამიტომ გადმოსაღვრელი არაფერია. ფიქსტურები დაიტოვეთ ძვირადღირებული ინფრასტრუქტურისთვის (კონტეინერი, კავშირების პული).
ყოველი არასტაბილურობა არადეტერმინიზმამდე მიდის. მოაშორეთ წყარო; ნუ დაფარავთ მას retry(3)-ით.
pytest-randomly), რომ ფარული კავშირი გამოჩნდეს.შესავალმა დეკმა ხუთი დუბლიორი გაგაცნოთ. ახლა რთული შემთხვევები: კოდი, რომელიც კედლის საათზე, რბოლის პირობებზე ან მესამე მხარის API-ზეა დამოკიდებული. გამაერთიანებელი ნაბიჯი ყოველთვის ერთია — აქციეთ უკონტროლო რამ შეყვანილ დამოკიდებულებად, რომელსაც ტესტიდან თქვენ მართავთ.
ბიზნეს-ლოგიკაში მიმოფანტული Date.now() / datetime.now() "იწურება 30 დღეში"-ს შეუმოწმებელს ხდის. შეიყვანეთ Clock და ხელით წაწიეთ — ან გააყინეთ ის ყალბი ტაიმერებით (vi.useFakeTimers, sinon), freezegun-ით (Python) ან ფიქსირებული java.time.Clock-ით.
sleep(500) არასტაბილურობის #1 მიზეზია — ძალიან მოკლე რბოლაში აგებს, ძალიან გრძელი კი ცოცავს. დაელოდეთ პირობას (ელემენტს, მოვლენას, რიგის სიღრმეს). რბოლებისთვის გამოიყენეთ დეტერმინისტული განრიგი ან მართვადი ყალბი ტაიმერები, რომ თანმიმდევრობა თქვენ განსაზღვროთ.
შეფუთეთ მესამე მხარის SDK თქვენს ინტერფეისში და შემდეგ ამ ინტერფეისის ფეიქზე ატესტეთ. პლუს რეალურთან მიახლოებული ვარიანტები: Testcontainers (ნამდვილი Postgres/Kafka Docker-ში), WireMock / MSW HTTP-სთვის, ჩაწერა-გამეორების კასეტები (VCR).
// the clock is a dependency, not a global interface Clock { now(): Date } class Token { constructor(private clock: Clock) {} isExpired(t: TokenData) { return t.expiresAt < this.clock.now() } } // test: a clock you control, no real waiting const clock = { now: () => new Date("2030-01-01") } expect(new Token(clock).isExpired(old)).toBe(true)
შეიყვანეთ საათი და "ვადის გასვლის" ლოგიკა უბრალოდ მონაცემები ხდება — ნამდვილი საათი პროდაქშენში, გაყინული ტესტში.
// asserts HOW the code calls the dep const repo = { save: jest.fn() } register(repo, user) expect(repo.save).toHaveBeenCalledWith(user) // rename/reorder the call → test breaks, // even though behavior is identical
// a real, in-memory implementation const repo = new InMemoryUserRepo() register(repo, user) expect(repo.findByEmail(user.email)).toEqual(user) // asserts the OUTCOME — refactor-proof
გაუშვით ერთი და იგივე კონტრაქტის ნაკრები ფეიქზეც და ნამდვილზეც. თუ ორივე გადის, თქვენი ფეიქი ერთგულია — და სწრაფი იუნიტ-ტესტები სანდო რჩება.
თითო წარმომადგენლობითი ინსტრუმენტი თითო ტექნიკაზე, თითო ეკოსისტემაში — გულახდილი კომპრომისითა და იმ მომენტით, როცა მას მიმართავთ. ყველა მომწიფებულია და აქტიურად ვითარდება 2026 წლის მდგომარეობით.
დადებითი: პირველხარისხოვანი TS-ტიპები, შესანიშნავი შეკვეცა, ინტეგრირდება Jest/Vitest-თან, stateful fc.commands.
უარყოფითი: რთული დომენებისთვის კარგი გენერატორების წერას პრაქტიკა სჭირდება.
მიმართეთ მას ნებისმიერ JS/TS სუფთა ლოგიკაზე: პარსერები, ფასწარმოქმნა, კოდერები.
დადებითი: ოქროს სტანდარტი — მდიდარი სტრატეგიები, შენახული მაგალითების ბაზა, რომელიც წარსულ ჩავარდნებს იმეორებს, RuleBasedStateMachine.
უარყოფითი: გენერაცია შეიძლება ნელი იყოს; მოარგეთsettings-ის ვადები.
მიმართეთ მას ნაგულისხმევად ნებისმიერ Python-ლოგიკაზე, რომელიც სერიოზულ ტესტირებას იმსახურებს.
დადებითი: JUnit 5-ის ძრავია, ამიტომ არსებულ Java/Kotlin ნაკრებებში ჯდება; ინტეგრირებული შეკვეცა.
უარყოფითი: უფრო მცირე საზოგადოება; Hypothesis-ზე მეტი შაბლონური კოდი.
მიმართეთ მას როცა უკვე JUnit 5-ზე ხართ და გვერდით PBT გინდათ.
დადებითი: კონსიუმერით მართული დე-ფაქტო სტანდარტი; კლიენტები ყველა მთავარ ენაზე; HTTP + შეტყობინებები; ბროკერი can-i-deploy-ით.
უარყოფითი: რეალური სასწავლი მრუდი; პროვაიდერის მდგომარეობები და ბროკერის ექსპლუატაცია მოძრავ ნაწილებს ამატებს.
მიმართეთ მას როცა დამოუკიდებელი გუნდები სერვისებს სხვადასხვა რიტმით უშვებენ.
დადებითი: პროდიუსერით მართული, ღრმად ინტეგრირებული Spring-თან; კონსიუმერებისთვის სტაბებს აგენერირებს.
უარყოფითი: Spring-ცენტრული; JVM-ეკოსისტემის გარეთ მოუხერხებელი.
მიმართეთ მას მთლიანად Spring-ის მეურნეობაში, სადაც პროვაიდერი წინ მიდის.
დადებითი: იაფი მრღვევი ცვლილებების აღმოჩენა REST/gRPC-ზე; კონსიუმერის ტესტი არ სჭირდება.
უარყოფითი: ამოწმებს ფორმას და არა იმას, რაზეც კონსიუმერი რეალურად დგას.
მიმართეთ მას სწრაფ პირველ ხაზად; განზრახვისთვის დააწყვილეთ Pact-თან.
დადებითი: მომწიფებული, შესანიშნავი HTML-რეპორტები, ინკრემენტული რეჟიმი CI-სთვის, ასევე .NET & Scala ვარიანტები.
უარყოფითი: ნელია დიდ ნაკრებებზე, თუ შეცვლილი ფაილებით არ შემოსაზღვრავთ.
მიმართეთ მას კრიტიკული მოდულის ტესტების აუდიტისთვის, CI-ში დიფზე.
დადებითი: სწრაფი (ბაიტკოდის მუტაცია), JVM-ის სტანდარტი, ისტორიაზე დაფუძნებული ინკრემენტული გაშვებები.
უარყოფითი: Java-ცენტრული კონფიგურაცია; რეპორტების მორგებას ძალისხმევა სჭირდება.
მიმართეთ მას ნაგულისხმევ მუტაციურ ინსტრუმენტად ნებისმიერ JVM-პროექტზე.
დადებითი: მარტივი ჩასართავი; გასაგები დიფები გადარჩენილებზე, რომლებზეც მოქმედება შეიძლება.
უარყოფითი: შესამჩნევად ნელი; საუკეთესოა ერთ პაკეტზე შემოსაზღვრული და არა მთელ რეპოზიტორიაზე.
მიმართეთ მას ერთი მაღალფასიანი Python-მოდულის ტესტების გასამაგრებლად.
დადებითი: მრავალბრაუზერული, ავტომატური ლოდინი (არასტაბილურობის დიდ ნაწილს კლავს), ტრეისები & ვიდეო, პარალელური დაშარდვა, პირველხარისხოვანი API- და კომპონენტების ტესტირება.
უარყოფითი: ისევ ყველაზე ნელი და ძვირი ზოლია — შეინახეთ თხელი.
მიმართეთ მას მომხმარებლის კრიტიკული გზების რამდენიმე სმოუკ-ტესტისთვის და არა ფუნქციონალის დასაფარად.
დადებითი: ნამდვილი Postgres / Kafka / Redis Docker-ში, ერთჯერადი თითო გაშვებაზე — მაღალი ერთგულება, საერთო გარემოს გარეშე.
უარყოფითი: სჭირდება Docker; in-memory ფეიქზე ნელია.
მიმართეთ მას ნამდვილი ადაპტერის (და თქვენი ფეიქის) ნამდვილ ძრავთან შესამოწმებლად.
დადებითი: გარე HTTP-ს ქსელის კიდეზე დაასტაბებს; MSW მოკებს ტესტებსა და ბრაუზერს შორის აზიარებს.
უარყოფითი: სტაბები თქვენს ვარაუდებს ინახავს — თქვენს საკუთარ სერვისებზე დააწყვილეთ კონტრაქტულ ტესტებთან.
მიმართეთ მას მესამე მხარის HTTP-სგან იზოლაციისთვის, რომელსაც ლოკალურად ვერ უშვებთ.
თვისების ტესტი და კონტრაქტული ტესტი, თავიდან ბოლომდე — ორი ტექნიკა, რომლებიც ამ კვარტალში ყველაზე დიდი ალბათობით შეცვლის თქვენი გუნდის მუშაობას.
// PROPERTY: splitting a bill loses no cents import fc from "fast-check" test("split(total, n) always sums back to total", () => { fc.assert(fc.property( fc.integer({ min: 0, max: 1e6 }), // cents fc.integer({ min: 1, max: 50 }), // people (total, n) => { const parts = split(total, n) const sum = parts.reduce((a, b) => a + b, 0) expect(sum).toBe(total) // invariant expect(parts).toHaveLength(n) })) }) // shrinks to split(1, 3) → [0,0,0], sum 0 ≠ 1 ✕
არცერთი მაგალითის ტესტი 1 ÷ 3-ს არ სცდიდა. შემკვეცავი ზუსტ მინიმალურ რეპროდუქციას გაძლევთ — კლასიკური ბაგი ნაშთზე.
pact.given("user 7 exists") .uponReceiving("GET /users/7") .withRequest({ method: "GET", path: "/users/7" }) .willRespondWith({ status: 200, body: { id: integer(7), email: like("a@b.co") } }) // → publishes users-web.json to the broker
await new Verifier({ provider: "Users", pactBrokerUrl: BROKER, stateHandlers: { "user 7 exists": () => db.seed({ id: 7 }), }, }).verifyProvider() // fails the build if Users drops/renames a field
can-i-deploy-ით შემოსაზღვრეთ.ხუთი შეკითხვა თვისებაზე დაფუძნებულ, კონტრაქტულ და მუტაციურ ტესტირებაზე, პლუს რთული ნაწილის ტექნიკები — მყისიერი უკუკავშირი, შესვლის გარეშე.
ნავიგაცია ← → ღილაკებით ან სქროლით · უკან ბიბლიოთეკაში