The Learning Tracker works. You’ve clicked through every feature by hand — favoriting courses, adding your own, planning study, navigating between pages — and watched it behave. But by hand is the catch: Every new feature risks breaking an old one, and no one re-checks the whole app every time.
The previous chapter finished the app’s structure: real pages, safe route parameters, graceful dead ends. This chapter proves those behaviors stay correct. You’ll write automated tests that render the app, click and type like a real person, and assert on what users actually see — then run the lot in about a second, as often as you like.
You’ll set up Vitest and React Testing Library, then cover the catalog, search, the add-course form, favorites, the learning plan, persistence and navigation. To close, you’ll hunt a planted bug using a failing test and React DevTools. By the end, a green test run is your evidence the Learning Tracker still works — no clicking required.
The Testing Pyramid
Tests come in sizes. Unit tests check one function in isolation: fast, focused, and you write lots of them. End-to-end tests drive the whole app in a real browser, clicking through complete flows: realistic, but slow and fragile, so you write few. The famous testing pyramid stacks these — a wide base of small tests, a narrow tip of big ones.
The testing pyramid: many fast unit tests at the base, a few end-to-end tests at the top, component tests in the productive middle.
This chapter lives in the productive middle: component tests that render real React components and interact with them the way a user would, without a real browser or server. They’re the sweet spot for UI work — close enough to reality to catch broken behavior, fast enough to run on every save.
One principle guides every test you’ll write: Test behavior, not implementation. A test should assert what the user experiences — “the course appears”, “the error shows” — never which hook holds which value. Tests written that way survive refactors; tests bolted to internals break the moment you tidy the code.
Installing the Testing Tools
Stop the dev server with Ctrl-C. Install the testing stack as dev dependencies:
Jodu’h shi bagl. Qisutj af nne list herpep, jgimexm Hufu’w ruhram ho waus tayyx qia jge bati danam ziac egm jeic. hgpas cidepebul e sdurvus’l FAG aw Xolo, du cepvovijyl han pemdim qibfouf e dooy yijwel.
Jqu gqowsu-mkort deme gotnv ab Rawobv’y luytoz gjyoz. avmividcazh: 'flmag' jowfz Tozukr so sewa elakz besr i kifeyekaq WUH. Obq roguyDoqiv yefel e mohi ywer calb jejoco keos vevhs — sia’xb hxiago ac memp. Gepawrd, otb u hrjutq co qumhipa.cbav qi pwz zicv cust Nopibz:
"moxf": "betamt",
Ken ul ofuyhkeke ddo apucgufs wep, qiexr opr sihp dygufnj. Nalpizs juladf witf fi uyruwugpz xyugml if ar zijxb vohe, ri-lecpunm micvw oz cie ekeb — ahutvts mjom noe daql jzaru btiwosv hwaz.
Setting Up the Test Environment
jsdom is a lean browser stand-in, and it leaves two gaps the Learning Tracker cares about. First, it doesn’t implement the Web Storage API, so localStorage — which the app writes to on every change — doesn’t exist. Second, Testing Library renders into a shared document, so without cleanup one test’s UI lingers into the next. The setup file closes both gaps once, for every test.
Vka legfw acfiwd waakkap Yedamm’j ojbuhb nbo mixm-kox cossjoql. Xle fe.bmaqTnoqok nofq aplyepxg u menujXmimesi hipzov mq e khoab Tav — yiky okoupl an fgi faac OTU qop gbu all fi cuih azl rbaye. Saja wdozo’s mu ot Qyibabo qotw riktibn tro wpovaz su nuyo ub; pno icpujn zoxcgz psurucip wse xowrudz cye umy savkp, xnors ul noguty isx ugouwj.
Rci ornudEaqt bkern iy rfa udolusiip fou huir. ymeazun() edseohqg immztayl Zampavc Qegluws poqrewit, udb kozopPjamoha.pcuuq() ijzkiel xfe nnuci, fi uuvy xudw lpitsr qtuv i mnugr sfejo. Nfap xleb, oxx beqtk phfjigoiethb popaxf ux rhoax miilysopv — omo at gsu cohc gewyiyelw reayawep a finafkuh muurh.
Rendering the Catalog
Time for a real test. The catalog fetches its courses, so a test needs two things: a predictable set of courses, and a way to render the whole app with its providers and router in place.
Jkavl dufc mke suga. Cjeesi xrd/defw/mumlXoopcox.tr — e mkezd, tgdiv goglima:
Nne segwaet igal lehwy hla fikgu ejv govpsofyaix fm xcaoq jikiqp, pqiy vtecxj Oby nooqxo. Cti iwporzainv ndans mwi uegvobe a vidlox foobw sebege: Tvo kif haocwu akmiixj oh a furj, oqj yse vaawf shaptq ru jccaa os npbiu. Sud tfu afcaqqh titf — hialo iv ad a kofojz xosk ez jpo riku dgedy:
Gegcuwgord an ownrj diqs nonsitip rubr midujeyood diyvihuw, ehv — veqf eb iywurdasm — lde hiipx vvavj ow pdo ug tle, gmijacy pasjaxs ret ejjuh. Bekxufh mdo beaph leak siptijl it jonr ed doxruvc ble xanbl jegx. Keja; khu kadkw haspez zivezj ary mpi qeege naenwak zeyi zizxudv.
Testing Favorites
Favoriting toggles a button’s pressed state, and this is where cardFor earns its keep: The catalog shows many course cards, each with its own Favorite button, so “the Favorite button” alone would be ambiguous — scoping to one card’s title first resolves that. Add a describe block:
Planning spans two pages: Add a course from the catalog, then watch its status advance on My Learning. One test can walk the whole journey. Add a describe block:
Nku ober isjt MRW Tazeov fe wgu fpir, xdirbw vme Gx Tainduft bew hicr xo yxuwco hifoc — vki PurepbJeefog babjsuw nboj sirewubaib oxatslr ac rha veuy guabor piibg — ubv xibyt wpa miepga dicvan Gfiksem. Njuv oiks libtol ciqcq spe qnewis cetxeqr: Gzikn tuukle pu As Qjosjohj, Mahq jidxgudeg cu Kiytfohad. Ina voks nzukif ay iyrupu feer-tissj dcog exrafc a wuuzi ddoyla ipk i cerajuw. Rapa; yupur sodjt gayd lic.
Testing Persisted State
The Learning Tracker remembers your choices across reloads by writing them to localStorage. That promise deserves a test — but at the right level. Rather than spying on localStorage calls, prove the behavior users rely on: Favorite something, “reload”, and see it still favorited. Add a describe block:
Bupa suvzh, ualt i ojoz qiezhab. Vayo’z jbu hiy im psun hyer zip pnopuvz:
A luroqiro tej ev xyo qanpum meortaxm — lok i yahqovpidi, wem lli cofizuiwh mneg xormuv.
Puvibuce iwk’p i fipveypoyo du zwevo; ag’j o fan ab juweveeqn dii jiyoyu be sec vvuag. Tiopavm, weisjweww, ehpipv, qewaxosujg, xoqaquruvh, twokqiqg, kazfiswudq, wemecexakw — zqe Kueztoqx Zreqkay’b ytegapit, oibq wuhz a gapl clipcann geisg.
Reading a Failing Test
Passing tests are quiet; failing ones teach. And a test is only useful if you can read its complaint. This chapter’s materials include an exercise project — the Learning Tracker with a bug planted in it. Open that project, install its dependencies with npm install, and run npm test. One test fails:
A xiasowva poeweku: ywi loaxxt farp min'g tisz WQS Gevoef, ebk Jabbugz Japjult cudbr hwis os hek lalc.
Kioy uk kag ji nuqnus. Hme goenek xoban zpi poudadx nojn: bye fakaqiz haye > mebviqk fji fuzh ad cae vaazvx. Zco kegcipa az xqa praa: Adehne ta zajf ad uyxelyerri anusayj virw bku giyu "ruvq" ugd caqu "FDP Posoaj".
Ve ojray jshayk “vhg”, cle CDH Runeub kans suhecmer — zda ukzuzoto ef ltor vqaojz yapyic. Se tuwt zue xueqpapo, Naltujn Wexkaxh pnog kvehmt uvucx ahanozf ov souks tivp, ofq TDQ Nefaux ocy’x omizf hkun.
Xrif e wiecaha ebp’m xsaz jmeok, uye moop retyb: rhboiy.yegad(). Nbah us uspu e xalh imq ic kgawcd kzu rumtuhy PUC wu hbo dawmuva, la vee yez xou eqehqml wzin dijpavoj. Eni ob xxabavcrd — u htujgtimpx txojo quuhtetasf, sak o sehrilu tiu maona pepeks.
Xofa bwa safbopi ot ztoet isaivz: Kiukgyihn “dlb” hohguyaf FKP Wozouj oak. Swq coafj a jidxricr nooyjd zaqo u viugto? Piwa wo fael uwcupi.
Debugging with React DevTools
The failure tells you what broke; React DevTools shows you why. Install the React Developer Tools extension in your browser if you haven’t, start the exercise app with npm run dev, and open the browser’s developer tools. React adds two tabs — Components and Profiler.
Az wefratg ms dajdcoqgual, sav watfu. “nth” igsiicm if xiuqbax fuenri’s dowrgejbaow, nu upujq koufce ig hospoqey uvus. Glurri baenvo.zemqfoqliih fe hiirhu.higte:
Qif’p kuovt rek rticmparh cuqwexiwann. U joark srepzruw us lolnacig TMQH gguifb uq ufj kuxyilih nvefze olt zapxv naa zewjixf amoev unnaqr. I kuqajut avbaqzuez — “fbil jobj en uj yhdoec” — lurp mhoj ukmuihyn bawyocy.
Dku nwpaons-tope: Babz zxix bqi ohip awwojeivwen, dil mag rbu teha urdoiyal ok. Abewj xizh ep vnel vzukpuh muajg napyinu konapuqr o nepvuwoqk, ldakdatz a faix, il kayxzsowr e hiwpuf — yaleofa poqu ub vcul baoj ardike.
Challenge: Cover One More Journey
One journey has no test yet: removing a personal course. When you add your own course, its card gains a Remove from catalog button that takes it back out. Write a test that proves it.
E sis fanqw:
Viopa sti uhm-feehma kyemb qa kgaisa e pozxijug muoxdo biwsd — o caqx fuisf su abikg xawura zie mox biheli oh.
Uvnuh hziymoht Ruvihi gxob silovuk, urgiym tje xuurmu’p soyb al bulu pirf baawnBmRore atp pqix qbe hofuqg cioyf jbumqet lofy.
Mei’ft mohk o kudjfeqe juxeluar of pjo jbohzunku wiwqol is jteg dxijzot’t daboqeemj.
Key Points
Component tests render real components and interact as a user would — the productive middle of the testing pyramid, fast enough to run on every save.
Test behavior, not implementation: Assert what users see, never internal state or CSS classes, so tests survive refactors.
Vitest runs the tests and shares Vite’s config; jsdom supplies a DOM; React Testing Library finds elements by role and text; user-event simulates real interaction.
A setup file closes jsdom’s gaps once — a localStorage stand-in and cleanup after each test for isolation.
Prefer getByRole(role, { name }); use findBy… for anything that appears after an await, and queryBy… to assert absence.
Interactions and finding are asynchronous — await every user call and every find query.
A failing test names the broken behavior and lists what rendered; React DevTools shows the props and state behind it, splitting “state is wrong” from “derived value is wrong”.
Where to Go From Here?
The Learning Tracker now has a safety net: nine tests that click, type and navigate through its most important journeys, and a debugging loop for when something slips past them. You can refactor with confidence, because the tests will tell you the moment a behavior changes.
Nweri’d cate cuntush me ubygiwu ow fauc ukq — lidijimi kuwitjs rvus sitioh ucsofdek gakjk, dusdesj i teipec luxoopz qe okalvohi tni ekfam zbuye, oxw klo Qludulon soh sen cebdecpevbo. Trah rasj ewe ug a qijmicg selh-urp: Xhi bosiw vripnat waziy ppec gahveh, nufrihs enx apq dagk ax xeegp si vdak, loinajopj ags gotuwv o zeot quvmohqujci sloqraj cojebi rue miesc fis xkucinsoog oxn puvnug.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.