Lekcja 5. Testy jednostkowe w aplikacji webowej — xUnit i Jasmine/Karma
Trudny / egzaminacyjnyPo co się tego uczymy?
Aplikacja webowa (dział Aplikacje webowe) ma DWIE osobne warstwy kodu — backend ASP.NET Core i frontend Angular — i każda z nich wymaga WŁASNEGO podejścia do testowania, bo działają w zupełnie innych środowiskach uruchomieniowych (.NET po stronie serwera, JavaScript/TypeScript w przeglądarce). To domyka pełny obraz testowania z tego działu: ta sama filozofia (testuj logikę w izolacji, AAA, przypadki brzegowe) w nowym, dwuwarstwowym kontekście.
Teoria
Backend ASP.NET Core — xUnit zamiast MSTest. W projektach ASP.NET Core najpopularniejszym frameworkiem testowym jest xUnit (nie MSTest) — koncepcyjnie bardzo podobny (też AAA, też asercje), ale z inną składnią atrybutów: [Fact] zamiast [TestMethod] (pojedynczy test bez parametrów) i [Theory] + [InlineData(...)] zamiast [DataTestMethod] + [DataRow(...)] (test z wieloma zestawami danych). Asercje wyglądają inaczej składniowo: Assert.Equal(oczekiwany, wynik) (kolejność argumentów jak w MSTest), Assert.True(warunek), Assert.Throws<WyjątekTyp>(() => metoda()).
Testowanie kontrolerów Web API — testy jednostkowe kontra integracyjne. Kontroler API (poznany w lekcji o Web API/CORS, dział Aplikacje webowe) zwykle KORZYSTA z DbContext (Entity Framework Core) — testowanie go z PRAWDZIWĄ bazą danych to już test INTEGRACYJNY (wolniejszy, sprawdza współpracę z bazą). Żeby przetestować SAMĄ logikę kontrolera jednostkowo (szybko, bez bazy), używa się dostawcy EF Core InMemory — tymczasowej "bazy danych" trzymanej w pamięci RAM, tworzonej od zera dla każdego testu.
Frontend Angular — Jasmine i Karma. Projekt Angular utworzony przez Angular CLI ma testy skonfigurowane "od razu po wyjęciu z pudełka": Jasmine to framework do PISANIA testów (składnia describe(...) grupująca testy, it(...) pojedynczy test, expect(wynik).toBe(oczekiwany) asercja), a Karma to narzędzie URUCHAMIAJĄCE te testy w prawdziwej przeglądarce (uruchamiane komendą ng test).
TestBed — testowanie komponentów i serwisów Angular. TestBed to mechanizm Angulara tworzący "mini-środowisko" komponentu na potrzeby testu (bez uruchamiania całej aplikacji) — pozwala utworzyć instancję komponentu lub serwisu, wstrzyknąć jego zależności (Dependency Injection, poznane w lekcji o serwisach Angular) i sprawdzić zachowanie. Dla PROSTYCH serwisów (bez zależności od Angulara, np. czysta logika obliczeniowa) można też testować JAK ZWYKŁĄ klasę TypeScript, bez TestBed w ogóle.
Mockowanie zależności HTTP w Angularze — serwis korzystający z HttpClient (do komunikacji z Web API) NIE powinien w teście jednostkowym wysyłać PRAWDZIWEGO żądania sieciowego. Angular dostarcza HttpClientTestingModule — "atrapę" (mock) modułu HTTP, która pozwala symulować odpowiedzi serwera i sprawdzać, jakie żądania serwis faktycznie wysłał, bez łączenia się z prawdziwym backendem.
Schemat
APLIKACJA WEBOWA — DWIE warstwy, DWA różne narzędzia testowe
BACKEND (ASP.NET Core) FRONTEND (Angular)
┌─────────────────────┐ ┌─────────────────────┐
│ WpisyController.cs │ │ feed.component.ts │
│ (Web API) │ │ (komponent) │
└──────────┬───────────┘ └──────────┬───────────┘
│ testowany przez │ testowany przez
▼ ▼
xUnit + EF Core InMemory Jasmine + Karma + TestBed
[Fact] / [Theory] describe() / it() / expect()
Assert.Equal(...) TestBed.createComponent(...)
dotnet test ng test
Przykład z życia
Duży sklep internetowy ma osobny zespół backendowy (API, baza danych, reguły cenowe) i osobny zespół frontendowy (interfejs, koszyk, formularze) — każdy zespół testuje swoją część NIEZALEŻNIE, własnymi narzędziami dopasowanymi do swojego języka i środowiska, ale oba kierują się TĄ SAMĄ filozofią: testuj logikę w izolacji, sprawdzaj przypadki brzegowe, nie testuj wyglądu. Dzięki temu obie warstwy można rozwijać równolegle, mając pewność, że zmiana w jednej nie zepsuje cichaczem drugiej.
WpisyControllerTests.cs (xUnit + EF Core InMemory)
// Testy backendu - xUnit dla kontrolera Web API z EF Core InMemory
// (WpisyControllerTests.cs w projekcie testowym xUnit)
using Xunit;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using System.Linq;
public class WpisyControllerTests
{
private PortalContext UtworzKontekstWPamieci()
{
var opcje = new DbContextOptionsBuilder<PortalContext>()
.UseInMemoryDatabase(databaseName: System.Guid.NewGuid().ToString())
.Options;
return new PortalContext(opcje);
}
[Fact]
public async Task GetFeed_BezWpisow_ZwracaPustaListe()
{
// Arrange
using var context = UtworzKontekstWPamieci();
var kontroler = new WpisyController(context);
// Act
var wynik = await kontroler.GetFeed();
// Assert
Assert.Empty(wynik.Value);
}
[Theory]
[InlineData(, )] // brak polubień
[InlineData(3, 3)] // trzy polubienia
public async Task GetFeed_LiczyPolubieniaPoprawnie(int liczbaPolubien, int oczekiwana)
{
// Arrange
using var context = UtworzKontekstWPamieci();
var wpis = new Wpis { Tresc = "Testowy wpis" };
context.Wpisy.Add(wpis);
for (int i = ; i < liczbaPolubien; i++)
{
context.Polubienia.Add(new Polubienie { WpisId = wpis.Id, UzytkownikId = $"user{i}" });
}
await context.SaveChangesAsync();
var kontroler = new WpisyController(context);
// Act
var wynik = await kontroler.GetFeed();
// Assert
Assert.Equal(oczekiwana, wynik.Value.First().LiczbaPolubien);
}
}
ogloszenia.service.spec.ts + sortowanie.spec.ts (Jasmine/Karma)
// Testy frontendu - Jasmine/Karma dla serwisu Angular korzystającego z HttpClient
// (ogloszenia.service.spec.ts)
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { OgloszeniaService } from './ogloszenia.service';
describe('OgloszeniaService', () => {
let service: OgloszeniaService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [OgloszeniaService]
});
service = TestBed.inject(OgloszeniaService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // sprawdza, że nie zostały "wiszące" żądania
});
it('powinien zostać utworzony', () => {
expect(service).toBeTruthy();
});
it('szukaj() powinien wysłać żądanie GET z poprawnymi parametrami filtra', () => {
service.szukaj({ kategoria: 'rowery', cenaOd: 100, cenaDo: 500 })
.subscribe(wynik => {
expect(wynik.length).toBe(2);
});
const req = httpMock.expectOne(request =>
request.url === '/api/ogloszenia' &&
request.params.get('kategoria') === 'rowery'
);
expect(req.request.method).toBe('GET');
// symulujemy odpowiedź serwera BEZ prawdziwego backendu
req.flush([{ id: 1, tytul: 'Rower A' }, { id: 2, tytul: 'Rower B' }]);
});
});
// Prosty test komponentu (bez TestBed) - czysta logika filtrowania w klasie pomocniczej
// (sortowanie.spec.ts)
import { posortujRosnaco } from './sortowanie';
describe('posortujRosnaco', () => {
it('sortuje tablicę liczb rosnąco', () => {
const wynik = posortujRosnaco([5, 2, 8, 1]);
expect(wynik).toEqual([1, 2, 5, 8]);
});
it('dla pustej tablicy zwraca pustą tablicę', () => {
expect(posortujRosnaco([])).toEqual([]);
});
});
Komentarz i wyjaśnienie kodu
UseInMemoryDatabase(databaseName: Guid.NewGuid()...) tworzy za KAŻDYM razem NOWĄ, PUSTĄ "bazę danych" w pamięci — dzięki losowej nazwie testy NIE WPŁYWAJĄ na siebie nawzajem (każdy dostaje czystą, izolowaną kopię), mimo że korzystają z tego samego mechanizmu EF Core co prawdziwa baza produkcyjna.
[Theory] + [InlineData(...)] w xUnit to dokładny odpowiednik [DataTestMethod]/[DataRow] z MSTest (lekcja 3) — jeden test uruchamiany wielokrotnie z różnymi danymi, tu sprawdzający poprawność zliczania polubień dla 0 i 3 polubień.
W teście Angular httpMock.expectOne(...) PRZECHWYTUJE żądanie HTTP, które serwis faktycznie próbowałby wysłać, i pozwala je zweryfikować (adres URL, parametry, metoda) ORAZ ręcznie dostarczyć (req.flush(...)) spreparowaną odpowiedź — dzięki temu test działa błyskawicznie i nie wymaga uruchomionego backendu ASP.NET Core.
posortujRosnaco to przykład CZYSTEJ funkcji TypeScript (bez zależności od Angulara) — dla takiej logiki TestBed jest zbędny, testuje się ją jak zwykłą funkcję JavaScript/TypeScript, analogicznie do testowania czystej klasy C# w lekcjach 2-4.
Ćwiczenie samodzielne
Weź kontroler OgloszeniaController z projektu "Serwis ogłoszeniowy" (dział Aplikacje webowe, lekcja 25) i napisz dla niego test xUnit z EF Core InMemory sprawdzający, że filtrowanie po kategorii i zakresie cenowym działa poprawnie. Następnie napisz test Jasmine dla komponentu formularza filtrów, sprawdzający, że zmiana wartości w polu wywołuje emisję zdarzenia zmianaFiltrow.
Zadania do pracy własnej
Napisz test xUnit
[Fact]sprawdzający, że metoda kontrolera zwracająca listę ogłoszeń pomija te oznaczone jako sprzedane (CzySprzedane == true) — użyj EF Core InMemory z kilkoma przykładowymi rekordami.Napisz test Jasmine dla prostego serwisu Angular z metodą
walidujHaslo(haslo: string): boolean(np. minimum 8 znaków, przynajmniej jedna cyfra) — sprawdź kilka przypadków: hasło poprawne, za krótkie, bez cyfry (bez użycia TestBed, bo to czysta logika).Napisz test
[Theory]w xUnit dla metodyWynikiz projektu "System głosowania" (dział Aplikacje webowe, lekcja 26), sprawdzający poprawność obliczania procentów dla różnych rozkładów głosów (w tym przypadek zerowej liczby głosów — dzielenie przez zero musi zwrócić 0%, nie wyjątek). Dodatkowo napisz test Jasmine dla komponentu wyświetlającego paski wyników, sprawdzający, że szerokość paska (style.width) odpowiada procentowi z danych testowych.
Typowe błędy
Testowanie kontrolera Web API z prawdziwą bazą danych zamiast InMemory — takie testy są WOLNE, mogą zostawiać "śmieciowe" dane w bazie testowej i tak naprawdę są testami INTEGRACYJNYMI, nie jednostkowymi. Do szybkich testów logiki kontrolera zawsze używaj InMemory (albo mocków repozytorium).
Wysyłanie prawdziwych żądań HTTP w testach Angular (bez HttpClientTestingModule) — test stałby się zależny od działającego backendu, wolny i niestabilny (mógłby zawieść z powodów niezwiązanych z testowaną logiką, np. brak połączenia sieciowego). Zawsze mockuj warstwę HTTP w testach jednostkowych frontendu.
Zapominanie o httpMock.verify() w afterEach — bez tego testy mogą "po cichu" zostawiać nieobsłużone żądania, co maskuje błędy (np. serwis wysyła dodatkowe, niepotrzebne żądanie, którego test nie wykrywa).
Nawiązanie do egzaminu zawodowego
To domknięcie praktycznej części działu o testach jednostkowych — po konsoli (lekcja 2), WPF (lekcja 3) i MAUI (lekcja 4) widzisz teraz, że filozofia testowania (AAA, izolacja od zależności zewnętrznych, przypadki brzegowe) jest UNIWERSALNA, zmienia się tylko konkretne narzędzie (MSTest/xUnit/Jasmine) dopasowane do języka i platformy. W kolejnych lekcjach przejdziemy od testów AUTOMATYCZNYCH do szerszego kontekstu: testów niefunkcjonalnych (lekcja 6), organizacji procesu testowania (lekcja 7) i dokumentowania aplikacji (lekcja 8).