Skip to content

Commit 6af7d2b

Browse files
authored
1015 Tessera (#101)
* chore: subdirectory added with the initial index.md * docs: snippet 1015 added in index.md
1 parent 1385307 commit 6af7d2b

2 files changed

Lines changed: 377 additions & 0 deletions

File tree

2.29 KB
Loading

snippets/1015_Tessera/index.md

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
---
2+
layout: default
3+
codename: Tessera
4+
title: Tessera — Habit tracker fejlesztése és tesztelése LLM eszközökkel
5+
tags: snippets mieset
6+
authors: Várhegyi Melinda
7+
---
8+
9+
# Tessera — Habit tracker fejlesztése és tesztelése LLM eszközökkel
10+
11+
## A feladat leírása
12+
13+
A feladat egy szokáskövető webalkalmazás elkészítése volt, amely alkalmas különböző szokások nyilvántartására és azok teljesítésének követésére.
14+
A projekt fő témája a **különböző tesztelési módszerek megvalósításának vizsgálata
15+
LLM-ek segítségével**.
16+
17+
A fejlesztés során alkalmazott MI eszközök:
18+
19+
- **GitHub Copilot Chat (Claude Haiku 4.5)** — kódgeneráláshoz, tesztek írásához, hibák javításához
20+
- **Claude Sonnet 4.6** — tervezéshez, architektúra döntésekhez, prompt íráshoz és hibakereséshez
21+
22+
A projekt célja, hogy minél több különböző tesztelési megközelítést alkalmazzunk ugyanazon
23+
alkalmazáson, és értékeljük azok hatékonyságát LLM-asszisztált fejlesztési környezetben.
24+
25+
---
26+
27+
## A megvalósított rendszer
28+
29+
### Tech stack
30+
31+
| Réteg | Technológia |
32+
|---|---|
33+
| Backend | ASP.NET Core Web API, .NET 8, SQLite, EF Core |
34+
| Frontend | React + TypeScript + Vite, Bootstrap |
35+
| Tesztelés (backend) | xUnit, Moq, FluentAssertions, FsCheck, RestSharp |
36+
| Tesztelés (frontend) | Vitest, React Testing Library, Playwright |
37+
| Terheléstesztelés | k6 |
38+
| IDE | Visual Studio 2022 (backend), VS Code (frontend) |
39+
40+
### Solution struktúra
41+
42+
```
43+
Tessera/
44+
├── Tessera.Api/ ← ASP.NET Core Web API
45+
├── Tessera.Core/ ← Domain modellek, interfészek (pure C#)
46+
├── Tessera.Data/ ← EF Core + SQLite, repository implementációk
47+
├── Tessera.Tests.Unit/ ← xUnit, Moq, FluentAssertions, FsCheck
48+
├── Tessera.Tests.Integration/ ← WebApplicationFactory + RestSharp
49+
└── load-tests/ ← k6 terheléstesztek
50+
└── habits-load-test.js
51+
52+
tessera-frontend/ ← React + TypeScript + Vite + Bootstrap
53+
├── src/components/__tests__/ ← Vitest + React Testing Library
54+
└── e2e/ ← Playwright E2E tesztek
55+
```
56+
57+
### Domain
58+
59+
- **Habit:** Id (Guid), Name, Description?, Color (hex), CreatedAt
60+
- **HabitCompletion:** Id (Guid), HabitId, Date (DateOnly)
61+
- **Streak:** egymást követő teljesített napok sorozata
62+
- **Unique constraint:** (HabitId, Date) — egy napot csak egyszer lehet teljesíteni
63+
64+
---
65+
66+
## A projekt beüzemelése
67+
68+
### EF Core migrations — hiányzó Design csomag
69+
70+
A `Microsoft.EntityFrameworkCore.Design` csomagot a `Tessera.Data` projektbe telepítettük
71+
(ahol a `DbContext` él), de a `dotnet ef` eszköz a startup projekten — vagyis a
72+
`Tessera.Api`-n — keresztül dolgozik, és ott is elvárja ugyanezt a csomagot. A Copilot
73+
ezt nem jelezte előre a kód generálásakor, a hiba csak a migrációs parancs futtatásakor
74+
derült ki. Megoldás: a `Microsoft.EntityFrameworkCore.Design` csomag telepítése a
75+
`Tessera.Api` projektbe is.
76+
77+
**Tanulság:** Az EF Core tooling és a projekt referencia struktúra közötti összefüggést
78+
a Copilot nem kezeli proaktívan, a `.csproj` fájlokat és a NuGet függőségeket mindig
79+
kritikusan kell ellenőrizni.
80+
81+
### CustomWebApplicationFactory — in-memory SQLite kapcsolat életciklusa
82+
83+
A `CustomWebApplicationFactory` első Copilot-generált verziója nem működött: a tesztek
84+
500-as hibával buktak el. A probléma gyökere az volt, hogy az in-memory SQLite adatbázis
85+
azonnal megsemmisül, amint a kapcsolat bezárul, ugyanis a Copilot a `DbContext`
86+
regisztrációján belül hozta létre a kapcsolatot, amely így rövid életű volt.
87+
88+
A helyes megoldás: a `SqliteConnection`-t a factory konstruktorában kell megnyitni,
89+
és `Singleton`-ként kell regisztrálni a DI konténerbe, hogy a teljes tesztelési
90+
munkamenet alatt életben maradjon. Emellett az összes `DbContext`-hez kapcsolódó
91+
DI regisztrációt el kellett távolítani, és egy
92+
`ResetDatabase()` metódust kellett bevezetni, amelyet minden tesztosztály konstruktora
93+
hív meg a tiszta állapot garantálása érdekében.
94+
95+
A Copilot célzott javító prompttal sem generálta le a helyes megoldást,
96+
a javítás manuális beavatkozást igényelt.
97+
98+
---
99+
100+
## Tesztelési fázisok
101+
102+
### Phase 1 — Unit tesztek (xUnit + Moq + FluentAssertions)
103+
104+
A unit tesztek a `StreakCalculator` és `StatisticsService` osztályokat fedik le.
105+
A tesztek Moq-kal mockolt függőségeket használnak, és FluentAssertions
106+
segítségével ellenőrzik az elvárt viselkedést.
107+
108+
**Tanulság 1 — Duplikált dátumok kezelése:**
109+
A Copilot a `CalculateLongestStreak` implementációjában nem kezelte helyesen a duplikált
110+
dátumokat. Ha ugyanaz a nap kétszer szerepelt a completion listában, a streak hibásan
111+
számolódott. Célzott javító prompttal, amelyben pontosan leírtam az elvárt viselkedést
112+
és egy konkrét ellenpéldát, a hiba megoldható volt.
113+
114+
Utólag azonban kiderült, hogy ez a védelem dead code: az adatbázis szintű
115+
`(HabitId, Date)` unique constraint, illetve az API szintű validáció (400-as válasz
116+
duplikált completion kísérletekor) együttesen garantálják, hogy a `StreakCalculator`
117+
soha nem kap duplikált dátumokat tartalmazó listát. A duplikált-dátum ág ezért
118+
production-ban elérhetetlen kód, és az azt lefedő tesztek valójában nem éles
119+
viselkedést tesztelnek. Ez architekturális tanulság: ha a rendszer több rétegben is
120+
véd ugyanazon feltétel ellen, a belső réteg védelme feleslegessé válhat.
121+
122+
**Tanulság 2 — A `GetCompletionRate` visszatérési értékének értelmezése:**
123+
A `StatisticsService.GetCompletionRate` metódus 0.0–1.0 arányban adja vissza az
124+
eredményt (nem 0–100 százalékban), ahogy a dokumentációja is leírja. Ennek ellenére
125+
a Copilot által generált tesztek 100.0-t vártak el 1.0 helyett. A hiba csak a
126+
tesztek futtatásakor derült ki. Tanulság: a visszatérési értékek skáláját explicit
127+
módon kell megadni a promptban, különben a Copilot a "természetesebb" százalékos
128+
formátumot feltételezi.
129+
130+
---
131+
132+
### Phase 2 — Integrációs tesztek (WebApplicationFactory)
133+
134+
Az integrációs tesztek `WebApplicationFactory`-val, in-memory SQLite adatbázissal
135+
futnak. Minden tesztosztály konstruktorában `ResetDatabase()` hívás garantálja a
136+
tiszta DB állapotot (*lásd feljebb a projekt beütemezésénél*).
137+
138+
**Tanulság 3 — Az integrációs tesztek olyan hibákat fedtek fel, amelyek unit tesztekkel
139+
nem foghatók meg:**
140+
A `CompletionsController` `Delete` metódusa hibásan volt implementálva:
141+
`GetByHabitIdAndDateAsync`-t hívott `DateOnly.MinValue`-val az ID alapú törlés helyett.
142+
A Copilot valószínűleg a GET végpont logikájából indult ki (ahol dátum alapú
143+
keresés teljesen helyes), és ezt a mintát vitte át a Delete metódusba is, DateOnly.MinValue-t
144+
használva placeholderként. A kód lefordul, unit teszttel nem fogható meg, mivel a mock nem
145+
ellenőrzi a paramétereket, csak a hívás tényét. Az integrációs teszt fedte fel a hibát: valódi
146+
adatbázis ellen futva a MinValue-val indított keresés nem talált rekordot, a törlés nem történt
147+
meg, a teszt elbukott.
148+
149+
---
150+
151+
### Phase 3 — API tesztek (RestSharp)
152+
153+
A RestSharp API tesztek valódi futó szerver ellen dolgoznak. Első kísérletkor az API-t
154+
F5-tel (Debug módban) indítottam el, majd megpróbáltam párhuzamosan futtatni a
155+
teszteket, ezt viszont a Visual Studio nem engedte. A megoldás: Ctrl+F5
156+
(Start Without Debugging), amely a szervert a háttérben indítja el anélkül, hogy a
157+
debug folyamat lefoglalná a Visual Studio-t.
158+
159+
---
160+
161+
### Phase 4 — Property-based tesztek (FsCheck)
162+
163+
A FsCheck tesztek a `Tessera.Tests.Unit` projektbe kerültek, `FsCheck.Xunit` csomaggal,
164+
a meglévő 25 xUnit teszt mellé.
165+
166+
A property-based tesztek a következő invariánsokat ellenőrzik véletlenszerűen generált
167+
bemeneten:
168+
169+
- `CalculateLongestStreak` eredménye mindig >= 0 és <= a distinct dátumok száma
170+
- Duplikált dátumok hozzáadása nem változtatja meg a longest streak értékét
171+
*(lásd Tanulság 1, ez az invariáns production-ban elérhetetlen állapotot fed le)*
172+
- Teljesen egymást követő dátumsorozatnál a streak egyenlő a distinct dátumok számával
173+
- `CalculateCurrentStreak` eredménye mindig <= `CalculateLongestStreak` ugyanarra a bemenetre
174+
- Ha nincs mai vagy tegnapi completion, a current streak mindig 0
175+
- `GetCompletionRate` eredménye mindig >= 0.0 érvényes bemenetre
176+
- Ha a tartományon kívüli dátumok szerepelnek, a completion rate 0.0
177+
178+
**A property-based tesztek cross-method invariánsokat is képesek ellenőrizni:**
179+
Az xUnit tesztekkel nehéz természetesen kifejezni azt, hogy `CurrentStreak <= LongestStreak`
180+
minden lehetséges bemenetre. FsCheck-kel ez egyetlen property, amely automatikusan
181+
több száz véletlenszerű esetet ellenőriz, és képes ellenpéldát generálni, ha az invariáns
182+
megsérül.
183+
184+
---
185+
186+
### Phase 5 — UI komponens tesztek (Vitest + React Testing Library)
187+
188+
A frontend tesztek a három prop-alapú komponenst fedik le: `CheckInButton`,
189+
`StatisticsPanel`, `CalendarGrid`. A `HabitCard` és `HabitList` komponensek (amelyek
190+
API hívásokat végeznek) szándékosan kimaradtak, ezeket az E2E tesztek fedik le.
191+
192+
**Tanulság 4 — A `vitest/config` import szükséges a `vite.config.ts`-ben:**
193+
Ha a Vitest `test` konfigurációs blokkot a `vite.config.ts`-be helyezzük el,
194+
TypeScript hibát kapunk, mert a `vite` csomag `defineConfig`-ja nem ismeri a `test`
195+
mezőt. A helyes megoldás: `import { defineConfig } from 'vitest/config'` — ez
196+
re-exportálja a Vite `defineConfig`-ot, de kiegészíti a Vitest típusokkal.
197+
A Copilot ezt nem javasolta, saját utánajárás kellett hozzá.
198+
199+
**Tanulság 5 — Az explicit TypeScript típusok fontossága tesztfájlokban:**
200+
A Copilot a tesztfájlokban `any[]`-t használt a completion tömbök típusaként.
201+
Ez TypeScript hibát okozott, mivel a komponens `HabitCompletion[]`-t vár.
202+
Az importot és a típusannotációt explicit módon kellett megadni.
203+
204+
**A komponens tesztelhetőség és az architektúra kapcsolata:**
205+
A három könnyen tesztelhető komponens (`CheckInButton`, `StatisticsPanel`,
206+
`CalendarGrid`) mind tisztán prop-alapú, nincsen bennük API hívás vagy mellékhatás.
207+
A nehezen tesztelhető komponensek (`HabitCard`, `HabitList`) ezzel szemben
208+
`useEffect`-ben végeznek fetch hívásokat. Ez megerősíti azt az architektúrális
209+
elvet, hogy az üzleti logikát és az adatlekérést érdemes elválasztani a
210+
megjelenítési rétegtől, a separation of concerns nemcsak a kód minőségét
211+
javítja, hanem a tesztelhetőséget is.
212+
213+
---
214+
215+
### Phase 6 — E2E tesztek (Playwright)
216+
217+
A Playwright tesztek a teljes stacket fedik le valódi böngészőben: Chromium,
218+
Firefox és WebKit. A tesztek futtatásához mindkét szervernek élnie kell:
219+
a Vite dev szervernek (port 5173) és a Tessera.Api-nak (port 7184).
220+
221+
A tesztek az alábbi felhasználói folyamatokat ellenőrzik:
222+
223+
- Az alkalmazás betölt és a navbar látható
224+
- Az "New Habit" gombra kattintva megjelenik az űrlap
225+
- Új habit létrehozható névvel és színnel
226+
- A "Check in" gomb megnyomásával a completion regisztrálódik, az "Undo" gomb jelenik meg
227+
- A habit kártyára kattintva a részletező oldalra navigál, ahol a statisztikák láthatók
228+
229+
**Tanulság 6 — A Playwright szintaxis eltér a Vitest szintaxisától:**
230+
A Copilot a `habits.spec.ts`-ben `describe()` blokkot generált a tesztek köré,
231+
amely Vitest-es szintaxis. A Playwright saját `test.describe()` függvényt használ,
232+
a sima `describe` nem létezik a Playwright kontextusában, és futásidejű
233+
`ReferenceError`-t okoz. A javítás egyszerű volt, de rámutat arra, hogy a Copilot
234+
nem mindig veszi figyelembe, hogy melyik tesztelési keretrendszer aktív.
235+
236+
**Tanulság 7 — WebKit és self-signed SSL tanúsítvány összeférhetetlensége:**
237+
A tesztek kezdetben csak WebKit-ben buktak el `"Load failed"` hibával, miközben
238+
Chromium és Firefox rendben futott. A probléma gyökere: az ASP.NET Core fejlesztői
239+
szerver self-signed HTTPS tanúsítványt használ, amelyet a WebKit nem fogad el
240+
automatikusan, ellentétben a másik két böngészővel. A megoldás az
241+
`ignoreHTTPSErrors: true` beállítás a `playwright.config.ts` `use` blokkjában,
242+
amely minden böngészőre érvényes, és nem igényel backend módosítást.
243+
244+
**Tanulság 8 — Flaky tesztek hálózati függőség esetén:**
245+
Az egyik Firefox teszt időnként `NS_ERROR_CONNECTION_REFUSED` hibával bukott el,
246+
annak ellenére, hogy a Vite szerver futott. Ez flaky viselkedés: a teszt újrafuttatásra
247+
azonnal zöld lett. Az ilyen instabilitás jellemző E2E teszteknél, ahol a tesztek
248+
valódi hálózati hívásokra támaszkodnak. Megoldás lehet `webServer` auto-start
249+
konfiguráció a `playwright.config.ts`-ben, amely garantálja, hogy a szerver
250+
ténylegesen elérhető mielőtt a tesztek elindulnak.
251+
252+
---
253+
254+
### Phase 7 — Terheléstesztek (k6)
255+
256+
A terheléstesztek a projekt utolsó tesztelési fázisát alkotják. Az előző hat fázis
257+
a helyes működést ellenőrizte különböző absztrakciós szinteken; a k6 fázis ezzel
258+
szemben azt vizsgálja, hogy az API egyidejű terhelés alatt is teljesíti-e a
259+
válaszidő-elvárásokat.
260+
261+
#### Eszköz és telepítés
262+
263+
A k6 önálló, Go-alapú bináris, nem Node.js csomag, nem integrálódik a meglévő
264+
`package.json`-ba, és nem igényel futtatókörnyezetet a tesztek végrehajtásához.
265+
266+
A tesztek futtatásának előfeltétele, hogy a `Tessera.Api` élőben fusson
267+
(`https://localhost:7184`) — ugyanúgy, mint a RestSharp teszteknél:
268+
a szervert Ctrl+F5-tel kell indítani a Visual Studióban.
269+
270+
#### Terhelési profil
271+
272+
A teszt három szakaszból állt, összesen 50 másodperc alatt:
273+
274+
| Szakasz | Időtartam | Virtuális felhasználók |
275+
|---|---|---|
276+
| Ramp-up | 10 s | 0 → 10 VU |
277+
| Steady state | 30 s | 10 VU (állandó) |
278+
| Ramp-down | 10 s | 10 → 0 VU |
279+
280+
#### Tesztforgatókönyv
281+
282+
Minden virtuális felhasználó iterációnként az alábbi szekvenciát hajtja végre:
283+
284+
1. `GET /api/habits` — a teljes habit lista lekérése
285+
- Ellenőrzés: HTTP 200, válaszidő < 500 ms
286+
2. Ha a lista nem üres, az első elem `Id` mezőjével: `GET /api/habits/{id}/statistics`
287+
- Ellenőrzés: HTTP 200, válaszidő < 500 ms, a válasz tartalmazza a `CurrentStreak` mezőt
288+
3. 1 másodperc gondolkodási idő (think time) az iteráció végén
289+
290+
#### Threshold-ok (pass/fail kritériumok)
291+
292+
```javascript
293+
thresholds: {
294+
http_req_duration: ['p(95)<500'],
295+
http_req_failed: ['rate<0.01'],
296+
}
297+
```
298+
299+
A kérések 95%-a 500 ms alatt teljesülése és a hibaarány 1% alatt maradása jelenti a sikeres
300+
futást.
301+
302+
#### SSL kezelés
303+
304+
Az ASP.NET Core fejlesztői szerver self-signed tanúsítványt használ — ugyanaz a
305+
probléma, amellyel a Playwright WebKit teszteknél is szembesültünk. k6-ban a
306+
megoldás az `insecureSkipTLSVerify: true` opció az egyes `http.get` hívásoknál,
307+
nem a globális konfigurációban.
308+
309+
**Tanulság 9 — A k6 tooling jellege alapvetően különbözik a többi tesztelési eszköztől:**
310+
311+
A projekt minden korábbi tesztelési eszköze (xUnit, Vitest, Playwright) a
312+
fejlesztői ökoszisztémába (NuGet, npm) integrált csomag volt. A k6 ezzel szemben
313+
önálló rendszerbináris, amelyet külön kell telepíteni és a PATH-hoz adni. Ez azt
314+
jelenti, hogy a `winget install` sikere után a terminált újra kell indítani, és a
315+
`k6` parancs elérhetőségét explicit módon kell ellenőrizni, a Copilot ezt a
316+
kontextusfüggő operációs rendszer-szintű lépést nem tudja kezelni.
317+
318+
**Tanulság 10 — A PascalCase mezőnevek nem magától értetődők JavaScript kontextusban:**
319+
A k6 tesztek JavaScript környezetben futnak, ahol a konvenció általában camelCase.
320+
A Tessera API azonban C# konvenciókat követ, és PascalCase neveket szerializál
321+
(`habit.Id`, `habit.Name`, nem `habit.id`). A Copilotnak a helyes névhasználatot a promptban
322+
explicit módon kellett specifikálni.
323+
324+
**Tanulság 11 — A cold start torzítja a terhelésteszt eredményeit:**
325+
A futás során 3 check bukott el az `response time < 500ms` kritériumon, miközben
326+
a p(95) csupán 16.31 ms volt, ez látszólagos ellentmondás. A max=2.21s-os kiugró
327+
az ASP.NET Core JIT-fordításának és az EF Core kapcsolat első inicializálásának
328+
együttes hatása: a ramp-up szakasz legelső kérései lassabbak, mert a runtime ekkor
329+
fordítja le a releváns kódot és nyitja meg az SQLite kapcsolatot. Éles terhelési
330+
teszteknél ezt a hatást általában warm-up iterációkkal szokás eliminálni, a
331+
fejlesztői környezetben ez a viselkedés azonban
332+
elfogadható és nem meglepő.
333+
334+
**Mérési eredmények:**
335+
336+
Mindkét threshold teljesült: a 95. percentilis válaszidő (16.31 ms) jóval az 500 ms-os
337+
küszöb alatt maradt, hibás kérés pedig egyáltalán nem volt. A 3 failed check az
338+
`response time < 500ms` ellenőrzésnél keletkezett a cold start hatására.
339+
A steady state szakaszban ilyen kiugró nem fordult elő.
340+
341+
---
342+
343+
## A névválasztásról
344+
345+
A projekt neve **Tessera** — latin szó, jelentése: kis négyzet, csempe, dominókő.
346+
A domino effect a szokásépítés metaforája; a naptárnézet kis négyzetei vizuálisan
347+
is utalnak a névre. Tagline: *"Your habits, one tile at a time."*
348+
349+
---
350+
351+
## Összefoglalás
352+
353+
| Tesztelési módszer | Eszköz | Lefedett terület |
354+
|---|---|---|
355+
| Unit tesztek | xUnit + Moq + FluentAssertions | StreakCalculator, StatisticsService |
356+
| Integrációs tesztek | WebApplicationFactory | API végpontok, DB műveletek |
357+
| API tesztek | RestSharp | Valódi szerver ellen |
358+
| Property-based tesztek | FsCheck | Streak és statisztika invariánsok |
359+
| UI komponens tesztek | Vitest + RTL | CheckInButton, StatisticsPanel, CalendarGrid |
360+
| E2E tesztek | Playwright | Teljes felhasználói folyamatok, 3 böngésző |
361+
| Terheléstesztek | k6 | GET /api/habits, GET /api/habits/{id}/statistics; p(95)=16 ms, 0% hiba |
362+
363+
364+
Az LLM eszközök jelentősen felgyorsítják a tesztek generálását, de folyamatos
365+
emberi felügyeletet igényelnek. A leggyakoribb hibamintázat: a Copilot érti az általános
366+
esetet, de az edge case-eknél pontosabb promptot vagy manuális javítást igényel. A projekt beüzemelési nehézségei (EF Core Design csomag, in-memory SQLite kapcsolat életciklusa)
367+
szintén rávilágítanak arra, hogy a Copilot a konfigurációs és tooling jellegű problémákat
368+
kevésbé kezeli megbízhatóan, mint a kódgenerálást.
369+
370+
A projekt egy architekturális tanulsággal is szolgált: a duplikált dátumok kezelése
371+
a `StreakCalculator`-ban utólag dead code-nak bizonyult, mivel az adatbázis és az API
372+
réteg együttesen megakadályozzák, hogy ilyen állapot egyáltalán előálljon. Ez
373+
rámutat arra, hogy LLM-asszisztált fejlesztésben — ahol a rétegek egymástól
374+
függetlenül születnek — különösen fontos a rendszer egészének átlátása: a részek
375+
önmagukban helyesek lehetnek, miközben együttesen ellentmondást alkotnak.
376+
377+
**Szerző:** Várhegyi Melinda

0 commit comments

Comments
 (0)