CTI — Dezvoltarea Aplicațiilor Web — Laborator 9
Testing — Unit Tests pe Service Layer + Integration Tests
Obiective
Laboratorul continuă proiectul News Portal din Lab 8. La finalul acestui laborator, aplicația va avea un proiect de teste dedicat cu acoperire pe service layer (cu EF InMemory) și pe API end-to-end (cu HTTP + JWT real).
Ce facem:
- Diferența între unit test și integration test — ce e „unitatea", ce înseamnă „integrare"
- xUnit —
[Fact],[Theory]+[InlineData], pattern-ul Arrange — Act — Assert - EF Core InMemory ca BD de test — Service + Repository reali, BD înlocuită
- Testarea service layer-ului cu stack-ul real (Service → UnitOfWork → Context InMemory)
- Integration tests cu
WebApplicationFactory<Program>— pornim aplicația în memorie, lovim cuHttpClient - JWT în teste — obținem token prin login real, trimitem prin
Authorization: Bearer
Punctaj (4p)
- 1p per exercițiu × 4 exerciții.
- Ex 1, 2, 3 sunt validări ale walkthrough-ului (Părțile 1-3); Ex 4 e muncă peste lab — un test integration cu ownership end-to-end.
Definițiile complete sunt la finalul Părții 3.
Recapitulare Lab 8
Din laboratorul anterior avem:
ArticlesApiControllercu GET/POST/PUT/DELETE,[Authorize(JwtBearer)]pe operațiile de scriereAuthApiController.Login— întoarce{ token, expiresIn }pe bazaUserManager+SignInManager.CheckPasswordSignInAsyncIArticleService/ArticleService— logica de business, pesteIUnitOfWorkIUnitOfWork→IArticleRepository/ICategoryRepositorypesteAppDbContext- Dual auth: cookie pentru MVC, JWT pentru API
- Admin seed:
admin@newsportal.com/Admin@123
Lab 9 aproape că nu modifică codul de producție — adaugă un proiect de teste care îl referențiază ca un consumator obișnuit. Cele câteva schimbări strict necesare sunt deja aplicate în Lab09_start (vezi Pasul 11).
De ce teste automate?
Până acum, după fiecare schimbare ați făcut testare manuală: pornit aplicația, deschis browser-ul, dat click. Merge pentru 2-3 endpoint-uri. La 20 de endpoint-uri, timpul de regresie devine mai mare decât timpul de dezvoltare.
Testele automate rezolvă trei probleme:
- Regresie — un buton verde sau roșu vă spune imediat dacă s-a stricat ceva.
- Documentație vie — un test numit
GetById_InvalidId_ReturnsNulldescrie comportamentul mai clar decât un comentariu (care se învechește). - Design feedback — dacă un cod e greu de testat, aproape întotdeauna e greu de folosit. Testele pun presiune pe decuplare.
Unit vs Integration — vocabularul
| Tip | Ce testează | Izolare | Viteză |
|---|---|---|---|
| Unit test | O metodă / o clasă | Tot ce e în jur e mock-uit sau înlocuit | ms |
| Integration test | Un flux prin mai multe componente | DB, HTTP, auth — reale (sau in-memory) | sute de ms |
| End-to-end | Tot sistemul (browser → UI → API → DB) | Nimic mock-uit | secunde |
În laboratorul ăsta scriem unit pe service layer (Partea 2) și integration prin HTTP (Partea 3). E2E cu Playwright/Selenium e dincolo de scop.
Ce testăm și ce nu
| Strat | Testăm? | Cum |
|---|---|---|
Models (entități) |
Nu | nimic de testat — sunt date |
Repositories |
Indirect | sunt acoperite de service tests prin stack real |
Services |
Da, direct | unit tests cu InMemory (Partea 2) |
Controllers (MVC + API) |
Indirect | acoperite de integration tests prin HTTP (Partea 3) |
| Routing, auth, validare | Da | integration tests (Partea 3) |
| Razor Views | Nu | UI = E2E, peste scop |
De ce nu testăm controller-ele izolat (cu mock pe service)? Controller-ele noastre sunt thin pass-throughs — primesc request, cheamă service-ul, întorc rezultatul. Un test cu service mock-uit ar verifica doar că am cablat metoda corect, lucru pe care îl prinde mai bine integration test-ul prin HTTP. Dacă într-o zi controller-ul are logică reală (autorizare custom, transformări complexe), atunci da, scrieți unit tests cu mock — instrumentul e Moq sau NSubstitute.
Parte 1 — Setup, xUnit și pattern AAA
Pasul 1 — Proiectul de teste (preconfigurat)
În Lab09_start aveți deja un subfolder Lab09.Tests/ cu:
Lab09.Tests/
Lab09.Tests.csproj ← referință către Lab09.csproj + xUnit + EF InMemory + Mvc.Testing
SmokeTest.cs ← un singur test banal: Assert.Equal(2, 1+1)
Lab09.Tests/Lab09.Tests.csproj — deja în proiect
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.25" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.25" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lab09.csproj" />
</ItemGroup>
</Project>
Ce face fiecare pachet:
| Pachet | Rol |
|---|---|
Microsoft.NET.Test.Sdk |
Infrastructura de descoperire/rulare — fără el, dotnet test nu găsește nimic |
xunit + xunit.runner.visualstudio |
Framework-ul de teste și adapter-ul pentru dotnet test / Visual Studio |
Microsoft.AspNetCore.Mvc.Testing |
WebApplicationFactory<T> — pornește aplicația în memorie, oferă HttpClient |
Microsoft.EntityFrameworkCore.InMemory |
Provider EF fără BD reală — perfect pentru teste |
De ce versiuni pinned:
Microsoft.AspNetCore.Mvc.Testingtrebuie să se potrivească exact cu versiunea .NET-ului (8.0.x pe .NET 8). Dacă faceți upgrade la altceva, faceți-l împreună.
Lab09.csproj — exclude test project files
<!-- Exclude test project files from main project compilation -->
<ItemGroup>
<Compile Remove="Lab09.Tests\**" />
<Content Remove="Lab09.Tests\**" />
<EmbeddedResource Remove="Lab09.Tests\**" />
<None Remove="Lab09.Tests\**" />
</ItemGroup>
De ce excluderea asta? SDK-ul
Microsoft.NET.Sdk.Webinclude automat toate.csdin subfoldere. FărăCompile Remove, Lab09 încearcă să compileze fișierele de test — nu găseșteXunit, aruncăCS0246.
Primul dotnet test
cd C:\Daniel\ore\DAW-2025-2026\labs\lab09\Lab09_start
dotnet test
Așteptare:
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1
Dacă obțineți 1/1 passed, infrastructura merge.
Pasul 2 — xUnit, pe scurt
xUnit e al treilea framework de teste pentru .NET, după NUnit și MSTest. Microsoft îl folosește în ASP.NET Core însuși.
[Fact] — un caz fix
[Fact]
public void TwoPlusTwo_IsFour()
{
Assert.Equal(4, 2 + 2);
}
[Fact] = fapt — un test fără parametri, care fie trece, fie pică.
[Theory] + [InlineData] — multiple cazuri, același cod
[Theory]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
[InlineData(-1, 1, 0)]
public void Add_ReturnsSum(int a, int b, int expected)
{
Assert.Equal(expected, a + b);
}
[Theory] = teoremă — un test parametrizat. Fiecare [InlineData] generează un caz separat. Pică doar cazul care nu se validează.
Convenția de naming
Folosim Metodă_Scenariu_RezultatAșteptat:
GetByIdAsync_ExistingId_ReturnsArticle
GetByIdAsync_InvalidId_ReturnsNull
DeleteAsync_ExistingId_RemovesArticle
Update_AsNonOwner_Returns403
E verbose — dar la un fail în CI nu trebuie să deschideți testul ca să știți ce s-a stricat.
Setup / Teardown
xUnit nu are [SetUp] / [TearDown] per-test ca NUnit. Filozofia: fiecare test construiește ce are nevoie.
- Constructor-ul clasei rulează înainte de fiecare
[Fact]. Instanță nouă per test — izolare garantată. IDisposable.Disposerulează după fiecare test.IClassFixture<T>— o instanță partajată între toate testele dintr-o clasă (o folosim la integration tests, Partea 3).
Pasul 3 — Pattern-ul Arrange — Act — Assert
Fiecare test urmează trei pași:
Arrange → pregătește datele și obiectele necesare
Act → apelează metoda testată
Assert → verifică rezultatul
Exemplu cu un test pe service (preview din Partea 2):
[Fact]
public async Task GetByIdAsync_ExistingId_ReturnsArticle()
{
// Arrange
var (service, context) = CreateService(nameof(GetByIdAsync_ExistingId_ReturnsArticle));
SeedTwoArticles(context);
// Act
var result = await service.GetByIdAsync(1);
// Assert
Assert.NotNull(result);
Assert.Equal("Articol test 1", result!.Title);
}
Liniile goale între cele trei secțiuni sunt convenție — ajută la citire. Un test cu 15 linii Arrange, 1 linie Act, 5 linii Assert e normal și sănătos.
Aside — Assert vs FluentAssertions
În proiectele de producție întâlniți deseori biblioteca FluentAssertions, care rescrie aserțiunile ca metode de extensie:
result.Should().NotBeNull();
result!.Title.Should().Be("Articol test 1");
articles.Should().HaveCount(2);
Avantaje: ordinea fixă (mereu actual.Should().Be(expected)), mesaje de eroare mai bune, comparații structurale (BeEquivalentTo). Dezavantaj: încă o dependință + a 7-a versiune cere licență comercială.
În laboratorul ăsta folosim Assert.* nativ — mai puțină ceremonie, suficient pentru ce avem nevoie. Dacă lucrați pe un repo care folosește FluentAssertions, sintaxa e similară conceptual; tabelul de aserțiuni de la finalul Părții 3 vă acoperă echivalențele de bază.