09

Lab 09 - Testing — Service Layer + Integration

Partea 1 din 4

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 cu HttpClient
  • 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:

  • ArticlesApiController cu GET/POST/PUT/DELETE, [Authorize(JwtBearer)] pe operațiile de scriere
  • AuthApiController.Login — întoarce { token, expiresIn } pe baza UserManager + SignInManager.CheckPasswordSignInAsync
  • IArticleService / ArticleService — logica de business, peste IUnitOfWork
  • IUnitOfWorkIArticleRepository / ICategoryRepository peste AppDbContext
  • 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:

  1. Regresie — un buton verde sau roșu vă spune imediat dacă s-a stricat ceva.
  2. Documentație vie — un test numit GetById_InvalidId_ReturnsNull descrie comportamentul mai clar decât un comentariu (care se învechește).
  3. 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.Testing trebuie 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.Web include automat toate .cs din subfoldere. Fără Compile Remove, Lab09 încearcă să compileze fișierele de test — nu găsește Xunit, 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.Dispose rulează 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ă.