Automated Testing Strategy - Przewodnik dla Enterprise 2025

Każdy bug w produkcji kosztuje. Według IBM, błędy wykryte na produkcji są nawet 100x droższe w naprawie niż te znalezione podczas developmentu. Dla enterprise, gdzie każda minuta przestoju to tysiące złotych strat, automated testing nie jest już luksusem - to podstawa przetrwania.
Firmy z solidną strategią test automation raportują imponujące wyniki: 90% redukcję czasu testowania, 85% mniej błędów produkcyjnych i 3x szybsze time-to-market. Ale jak to osiągnąć?
W tym przewodniku znajdziesz wszystko - od fundamentów Test Pyramid, przez testy jednostkowe (xUnit, Jest), testy integracyjne z TestContainers, aż po E2E testing (Playwright vs Cypress) i integrację z CI/CD. Sprawdzone wzorce z największych firm tech + kod produkcyjny dla .NET i JavaScript.
Dlaczego Automated Testing jest Kluczowy dla Enterprise
Wyobraź sobie: Twój zespół wypuszcza nową wersję aplikacji. Po kilku godzinach telefon - krytyczny bug blokuje płatności. Panika. Rollback. Utracone przychody. Zdenerwowani użytkownicy.
Manualne testowanie nie skaluje się. Dla aplikacji enterprise z setkami funkcji i ciągłymi zmianami, ręczne testy są wąskim gardłem spowalniającym delivery. Automated testing to rozwiązanie.
Korzyści Biznesowe Automated Testing:
Automated tests wykonują się w minutach zamiast dni. Regression testing zajmuje sekundy, a nie tygodnie. Deploy do produkcji 10x w ciągu dnia staje się możliwy.
Wykrywanie bugów na etapie developmentu kosztuje 100x mniej niż naprawa w produkcji. Automated tests znajdą błędy przed code review, oszczędzając czas zespołu.
Comprehensive test suite działa jak safety net - refactoring staje się bezpieczny, technical debt łatwiejszy do spłacenia. Kod z testami jest lepiej zaprojektowany (testable code = clean code).
Testy są najlepszą dokumentacją - pokazują jak kod faktycznie działa, a nie jak powinien działać. Nowi developerzy uczą się z testów szybciej niż z dokumentacji.
Z comprehensive test coverage, Friday deploys przestają być ryzykowne. Continuous deployment staje się możliwy, bo każda zmiana jest automatycznie weryfikowana.
📊Dane z Rynku
Według State of DevOps Report 2024, high-performing teams z comprehensive automated testing mają:
- • 208x częstsze deploymenty niż low performers
- • 106x szybszy czas od commit do production
- • 7x niższy change failure rate
- • 2,604x szybszy mean time to recovery (MTTR)
Test Pyramid - Fundament Testing Strategy

Test Pyramid to sprawdzona strategia balansowania różnych typów testów. Wprowadzona przez Mike'a Cohna, pokazuje optymalną proporcję testów zapewniającą najlepszy stosunek szybkości do pewności.
Pomyśl o piramidzie jako o budynku: mocne fundamenty (testy jednostkowe) na dole, stabilna konstrukcja (testy integracyjne) w środku, eleganckie wykończenie (testy E2E) na górze. Każda warstwa ma swoje znaczenie, ale fundament jest najważniejszy.
Struktura Test Pyramid:
🔬Unit Tests (70%)
Podstawa piramidy - szybkie, izolowane testy pojedynczych funkcji/klas. Wykonują się w milisekundach, łatwe do pisania i utrzymania.
Charakterystyka:
- • Testują jedną jednostkę kodu (funkcja, metoda, klasa)
- • Wszystkie zależności są mockowane
- • Wykonują się < 100ms każdy
- • Powinny stanowić 70% wszystkich testów
- • Framework: xUnit (.NET), Jest/Vitest (JS), JUnit (Java)
🔗Integration Tests (20%)
Środek piramidy - weryfikują współpracę komponentów z prawdziwymi zależnościami (baza danych, external APIs). Wolniejsze ale wykrywają integration bugs.
Charakterystyka:
- • Testują integrację wielu komponentów
- • Używają prawdziwej bazy danych (TestContainers)
- • Wykonują się w sekundach
- • Powinny stanowić 20% wszystkich testów
- • Framework: xUnit + TestContainers (.NET), Jest + Testcontainers (JS)
🌐E2E Tests (10%)
Szczyt piramidy - testują całą aplikację jak prawdziwy użytkownik. Najwolniejsze i najbardziej kruche, ale dają największą pewność user flows.
Charakterystyka:
- • Testują całe user journeys (login, checkout, etc.)
- • Uruchamiają prawdziwą przeglądarkę
- • Wykonują się w minutach
- • Powinny stanowić 10% wszystkich testów
- • Framework: Playwright, Cypress, Selenium
Dlaczego piramida, a nie diament lub lód?
Anti-pattern: Ice Cream Cone - więcej E2E testów niż unit testów. Skutkuje wolnymi build times, flaky tests i drogim utrzymaniem.
Piramida jest optymalna, bo unit testy są szybkie (instant feedback), tanie (łatwe utrzymanie) i stable (deterministyczne). E2E testy są wolne, drogie i flaky - używaj sparingly dla critical paths.
Unit Testing - xUnit, Jest i Best Practices
Unit testy (testy jednostkowe) są fundamentem automated testing strategy. Testują pojedyncze funkcje lub metody w izolacji, zapewniając szybki feedback loop i łatwe debugowanie.
Dlaczego są tak ważne? Bo działają w milisekundach. Wykrywają błędy natychmiast. A gdy coś pójdzie nie tak, od razu wiesz gdzie szukać. Poniżej praktyczne przykłady dla .NET (xUnit) i JavaScript (Jest).
🔵xUnit - Unit Testing dla .NET
xUnit to najnowocześniejszy testing framework dla .NET, używany przez Microsoft w projektach ASP.NET Core. Prosty, extensible i z świetnym wsparciem dla parallel execution.
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public async Task<Product> GetProductById(int id)
{
if (id <= 0)
throw new ArgumentException("Product ID must be positive");
var product = await _repository.GetByIdAsync(id);
if (product == null)
throw new NotFoundException($"Product {id} not found");
return product;
}
}public class ProductServiceTests
{
private readonly Mock<IProductRepository> _mockRepository;
private readonly ProductService _service;
public ProductServiceTests()
{
_mockRepository = new Mock<IProductRepository>();
_service = new ProductService(_mockRepository.Object);
}
[Fact]
public async Task GetProductById_ValidId_ReturnsProduct()
{
// Arrange
var expectedProduct = new Product { Id = 1, Name = "Test" };
_mockRepository
.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(expectedProduct);
// Act
var result = await _service.GetProductById(1);
// Assert
Assert.Equal(expectedProduct, result);
_mockRepository.Verify(r => r.GetByIdAsync(1), Times.Once);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public async Task GetProductById_InvalidId_ThrowsException(int id)
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => _service.GetProductById(id)
);
}
[Fact]
public async Task GetProductById_NotFound_ThrowsNotFoundException()
{
// Arrange
_mockRepository
.Setup(r => r.GetByIdAsync(999))
.ReturnsAsync((Product)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => _service.GetProductById(999)
);
Assert.Contains("999", exception.Message);
}
}Kluczowe pakiety NuGet:
- • xunit (test framework)
- • xunit.runner.visualstudio (VS integration)
- • Moq (mocking library)
- • FluentAssertions (readable assertions)
- • Bogus (fake data generation)
🟡Jest - Unit Testing dla JavaScript/TypeScript
Jest to najbardziej popularny testing framework dla JavaScript, stworzony przez Facebook. Zero config, wbudowany mocking, code coverage i świetne developer experience.
export class ProductService {
constructor(private repository: ProductRepository) {}
async getProductById(id: number): Promise<Product> {
if (id <= 0) {
throw new Error('Product ID must be positive');
}
const product = await this.repository.getById(id);
if (!product) {
throw new Error(`Product ${id} not found`);
}
return product;
}
async calculateDiscount(product: Product, code: string): Promise<number> {
const discount = await this.repository.getDiscountByCode(code);
return product.price * (discount / 100);
}
}import { ProductService } from './productService';
import { ProductRepository } from './productRepository';
jest.mock('./productRepository');
describe('ProductService', () => {
let service: ProductService;
let mockRepository: jest.Mocked<ProductRepository>;
beforeEach(() => {
mockRepository = new ProductRepository() as jest.Mocked<ProductRepository>;
service = new ProductService(mockRepository);
});
describe('getProductById', () => {
it('should return product when valid ID is provided', async () => {
// Arrange
const mockProduct = { id: 1, name: 'Test Product', price: 100 };
mockRepository.getById.mockResolvedValue(mockProduct);
// Act
const result = await service.getProductById(1);
// Assert
expect(result).toEqual(mockProduct);
expect(mockRepository.getById).toHaveBeenCalledWith(1);
expect(mockRepository.getById).toHaveBeenCalledTimes(1);
});
it.each([0, -1, -99])(
'should throw error when ID is %i',
async (invalidId) => {
await expect(service.getProductById(invalidId))
.rejects
.toThrow('Product ID must be positive');
}
);
it('should throw error when product not found', async () => {
mockRepository.getById.mockResolvedValue(null);
await expect(service.getProductById(999))
.rejects
.toThrow('Product 999 not found');
});
});
describe('calculateDiscount', () => {
it('should calculate correct discount amount', async () => {
const product = { id: 1, name: 'Test', price: 100 };
mockRepository.getDiscountByCode.mockResolvedValue(20);
const discount = await service.calculateDiscount(product, 'SAVE20');
expect(discount).toBe(20);
});
});
});Kluczowe pakiety npm:
- • jest (test framework)
- • @types/jest (TypeScript types)
- • ts-jest (TypeScript transformer)
- • @testing-library/react (React testing)
- • jest-mock-extended (advanced mocking)
✅Best Practices dla Unit Testing
- AAA Pattern (Arrange-Act-Assert) - strukturyzuj testy w trzech sekcjach dla czytelności
- Jeden Assert Per Test - każdy test powinien weryfikować jedną rzecz (łatwiejsze debugowanie)
- Nazwij testy opisowo -
GetProductById_InvalidId_ThrowsExceptionzamiastTest1 - Testuj edge cases - null, empty, negative, boundary values
- Nie testuj implementacji - testuj behavior, nie internal details (avoid over-mocking)
- Szybkość ma znaczenie - unit tests powinny wykonywać się < 100ms każdy
Integration Testing z TestContainers

Testy integracyjne (integration tests) sprawdzają jak komponenty współpracują z prawdziwymi zależnościami - bazami danych, kolejkami wiadomości, cache, zewnętrznymi API.
Tu pojawia się problem: jak testować przeciwko prawdziwej bazie PostgreSQL bez instalowania jej na każdej maszynie developerskiej? TestContainers to odpowiedź - uruchamia prawdziwe serwisy w Docker containers, których używasz w testach i automatycznie je usuwa.
Dlaczego TestContainers?
Tradycyjne podejście: In-memory bazy (SQLite, H2), mockowane serwisy. Problem: nie testują prawdziwego behavior (SQL dialect differences, network issues, version mismatches).
TestContainers approach: Uruchamia prawdziwe serwisy (PostgreSQL, Redis, RabbitMQ) w Docker containers dla każdego test suite. Testy wykonują się przeciwko prawdziwej infrastrukturze, potem containers są automatycznie usuwane.
- • Prawdziwe serwisy - testuj przeciwko production-like environment
- • Izolacja - każdy test suite dostaje fresh container
- • Clean up - containers automatycznie usuwane po testach
- • Reproducible - konsystentne wyniki na dev/CI
Integration Test z PostgreSQL (.NET + Testcontainers)
using Testcontainers.PostgreSql;
using Xunit;
public class ProductRepositoryIntegrationTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres;
private ProductDbContext _dbContext;
private ProductRepository _repository;
public ProductRepositoryIntegrationTests()
{
_postgres = new PostgreSqlBuilder()
.WithImage("postgres:15-alpine")
.WithDatabase("testdb")
.WithUsername("test")
.WithPassword("test")
.Build();
}
public async Task InitializeAsync()
{
// Start container
await _postgres.StartAsync();
// Setup DbContext with real PostgreSQL
var options = new DbContextOptionsBuilder<ProductDbContext>()
.UseNpgsql(_postgres.GetConnectionString())
.Options;
_dbContext = new ProductDbContext(options);
await _dbContext.Database.MigrateAsync(); // Run migrations
_repository = new ProductRepository(_dbContext);
}
[Fact]
public async Task CreateProduct_ShouldPersistToDatabase()
{
// Arrange
var product = new Product
{
Name = "Integration Test Product",
Price = 99.99m
};
// Act
var created = await _repository.CreateAsync(product);
// Assert
var fromDb = await _repository.GetByIdAsync(created.Id);
Assert.NotNull(fromDb);
Assert.Equal("Integration Test Product", fromDb.Name);
Assert.Equal(99.99m, fromDb.Price);
}
[Fact]
public async Task GetProductsByCategory_ShouldReturnFilteredResults()
{
// Arrange - seed test data
await _repository.CreateAsync(new Product { Name = "Laptop", Category = "Electronics" });
await _repository.CreateAsync(new Product { Name = "Phone", Category = "Electronics" });
await _repository.CreateAsync(new Product { Name = "Shirt", Category = "Clothing" });
// Act
var electronics = await _repository.GetByCategoryAsync("Electronics");
// Assert
Assert.Equal(2, electronics.Count());
Assert.All(electronics, p => Assert.Equal("Electronics", p.Category));
}
public async Task DisposeAsync()
{
await _dbContext.DisposeAsync();
await _postgres.DisposeAsync(); // Stop and remove container
}
}Integration Test z MongoDB (Node.js + Testcontainers)
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { MongoClient, Db } from 'mongodb';
import { ProductRepository } from './productRepository';
describe('ProductRepository Integration Tests', () => {
let container: StartedTestContainer;
let client: MongoClient;
let db: Db;
let repository: ProductRepository;
beforeAll(async () => {
// Start MongoDB container
container = await new GenericContainer('mongo:7.0')
.withExposedPorts(27017)
.start();
const uri = `mongodb://${container.getHost()}:${container.getMappedPort(27017)}`;
client = await MongoClient.connect(uri);
db = client.db('test');
repository = new ProductRepository(db);
}, 60000); // timeout for container startup
afterAll(async () => {
await client.close();
await container.stop();
});
beforeEach(async () => {
// Clean database before each test
await db.collection('products').deleteMany({});
});
it('should create and retrieve product', async () => {
// Arrange
const product = {
name: 'Test Product',
price: 29.99,
stock: 100
};
// Act
const created = await repository.create(product);
const retrieved = await repository.findById(created._id);
// Assert
expect(retrieved).toBeDefined();
expect(retrieved?.name).toBe('Test Product');
expect(retrieved?.price).toBe(29.99);
});
it('should handle concurrent writes correctly', async () => {
// Test race conditions and transactions
const promises = Array.from({ length: 10 }, (_, i) =>
repository.create({ name: `Product ${i}`, price: i * 10 })
);
await Promise.all(promises);
const count = await repository.count();
expect(count).toBe(10);
});
});Kiedy używać Integration Tests?
- • Database queries - złożone JOIN'y, full-text search, transactions
- • External API integrations - payment gateways, email providers
- • Message queues - RabbitMQ, Kafka, Azure Service Bus
- • Cache behavior - Redis, Memcached invalidation logic
- • File storage - S3, Azure Blob, filesystem operations
E2E Testing - Playwright vs Cypress
Testy E2E (End-to-End) symulują prawdziwego użytkownika: uruchamiają przeglądarkę, klikają przyciski, wypełniają formularze, sprawdzają wyniki. To najdroższe testy w utrzymaniu, ale dają największą pewność, że aplikacja działa od początku do końca.
Wybór narzędzia to kluczowa decyzja. Playwright czy Cypress? Oba są świetne, ale mają różne mocne strony. Sprawdźmy szczegółowo.
🎭Playwright
Stworzony przez Microsoft, najnowocześniejsze narzędzie E2E testing. Multi-browser support (Chrome, Firefox, Safari, Edge), szybkie wykonanie i auto-waiting.
- • Wszystkie przeglądarki (Chrome, Firefox, Safari, Edge)
- • Szybsze wykonanie niż Cypress (true parallelization)
- • Testowanie wielu zakładek i kontekstów
- • Auto-waiting (no flaky tests)
- • Network interception i API mocking
- • Built-in test generator (Codegen)
- • Młodsze narzędzie (mniej resources)
- • Mniej plugins niż Cypress
🌲Cypress
Pierwszy popular E2E framework dla JavaScript. Świetne developer experience, time-travel debugging i bogaty ekosystem plugins.
- • Łatwe API (jQuery-like selectors)
- • Świetne debugging (time travel, screenshots)
- • Bogaty ekosystem plugins
- • Visual test runner (watch mode)
- • Automatic retries i waitowanie
- • Chrome + Firefox only (no Safari)
- • Wolniejsze (tests run serially w open source)
- • Nie obsługuje multi-tab scenarios
- • Same-origin limitation
Przykład: E2E Test z Playwright
import { test, expect } from '@playwright/test';
test.describe('E-commerce Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com');
});
test('user can complete purchase', async ({ page }) => {
// 1. Search for product
await page.getByPlaceholder('Search products').fill('laptop');
await page.getByRole('button', { name: 'Search' }).click();
// 2. Select product from results
await expect(page.getByText('Gaming Laptop')).toBeVisible();
await page.getByText('Gaming Laptop').click();
// 3. Add to cart
await expect(page.getByRole('heading', { name: 'Gaming Laptop' })).toBeVisible();
await page.getByRole('button', { name: 'Add to Cart' }).click();
// 4. Verify cart badge updated
await expect(page.getByTestId('cart-badge')).toHaveText('1');
// 5. Go to checkout
await page.getByRole('button', { name: 'Cart' }).click();
await page.getByRole('button', { name: 'Checkout' }).click();
// 6. Fill shipping information
await page.getByLabel('Full Name').fill('John Doe');
await page.getByLabel('Email').fill('john@example.com');
await page.getByLabel('Address').fill('123 Main St');
await page.getByLabel('City').fill('New York');
await page.getByLabel('ZIP').fill('10001');
// 7. Select payment method
await page.getByRole('radio', { name: 'Credit Card' }).check();
await page.getByLabel('Card Number').fill('4242424242424242');
await page.getByLabel('Expiry').fill('12/25');
await page.getByLabel('CVV').fill('123');
// 8. Submit order
await page.getByRole('button', { name: 'Place Order' }).click();
// 9. Verify order confirmation
await expect(page.getByRole('heading', { name: 'Order Confirmed!' })).toBeVisible();
await expect(page.getByText(/Order #/)).toBeVisible();
// 10. Screenshot for visual verification
await page.screenshot({ path: 'order-confirmation.png' });
});
test('validates required fields', async ({ page }) => {
await page.goto('/checkout');
await page.getByRole('button', { name: 'Place Order' }).click();
// Should show validation errors
await expect(page.getByText('Name is required')).toBeVisible();
await expect(page.getByText('Email is required')).toBeVisible();
});
test('handles out of stock items', async ({ page }) => {
// Intercept API and mock out of stock response
await page.route('**/api/products/*', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ stock: 0, available: false })
});
});
await page.goto('/product/123');
await expect(page.getByText('Out of Stock')).toBeVisible();
await expect(page.getByRole('button', { name: 'Add to Cart' })).toBeDisabled();
});
});Przykład: E2E Test z Cypress
describe('User Authentication Flow', () => {
beforeEach(() => {
cy.visit('/login');
});
it('allows user to login with valid credentials', () => {
// Fill login form
cy.get('[data-cy=email-input]').type('user@example.com');
cy.get('[data-cy=password-input]').type('SecurePassword123!');
cy.get('[data-cy=login-button]').click();
// Verify redirect to dashboard
cy.url().should('include', '/dashboard');
cy.get('[data-cy=welcome-message]')
.should('contain', 'Welcome back, User!');
// Verify auth token stored
cy.window().its('localStorage.authToken').should('exist');
});
it('shows error for invalid credentials', () => {
cy.get('[data-cy=email-input]').type('wrong@example.com');
cy.get('[data-cy=password-input]').type('wrongpassword');
cy.get('[data-cy=login-button]').click();
// Should show error message
cy.get('[data-cy=error-message]')
.should('be.visible')
.and('contain', 'Invalid credentials');
// Should not redirect
cy.url().should('include', '/login');
});
it('supports password reset flow', () => {
cy.get('[data-cy=forgot-password]').click();
cy.url().should('include', '/reset-password');
// Enter email
cy.get('[data-cy=email-input]').type('user@example.com');
cy.get('[data-cy=send-reset-button]').click();
// Verify success message
cy.get('[data-cy=success-message]')
.should('contain', 'Reset link sent to your email');
});
it('persists authentication across page reloads', () => {
// Login
cy.get('[data-cy=email-input]').type('user@example.com');
cy.get('[data-cy=password-input]').type('SecurePassword123!');
cy.get('[data-cy=login-button]').click();
// Reload page
cy.reload();
// Should still be authenticated
cy.url().should('include', '/dashboard');
cy.get('[data-cy=logout-button]').should('be.visible');
});
});Playwright vs Cypress - Która wybrać?
Wybierz Playwright jeśli: potrzebujesz multi-browser support (Safari testing), szybszego wykonania (parallel testing), lub testujesz advanced scenarios (multiple tabs, contexts, iframes). Playwright to przyszłość E2E testing.
Wybierz Cypress jeśli: zespół preferuje prostsze API, potrzebujesz bogatego ekosystemu plugins, lub już używasz Cypress (migracja nie zawsze warta nakładu). Cypress wciąż jest solid choice.
Rekomendacja 2025: Dla nowych projektów, wybierz Playwright. Ma lepsze wsparcie dla nowoczesnych aplikacji, szybsze wykonanie i aktywny rozwój przez Microsoft.
Integracja Testów z CI/CD Pipeline

Masz świetne testy? Super. Ale jeśli nie uruchamiają się automatycznie przy każdym push, to tak jakby ich nie było.
CI/CD integration to klucz do sukcesu automated testing. Każdy commit triggreruje testy. Każdy failed test blokuje merge. Zero bugów w produkcji bez wykrycia. Poniżej przykłady dla GitHub Actions i Azure DevOps.
💡 Przydatne zasoby:
⚙️GitHub Actions - Complete Testing Pipeline
name: Test Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unit
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: unit-tests
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run database migrations
run: npm run db:migrate
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
needs: integration-tests
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Build application
run: npm run build
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30🔷Azure DevOps - .NET Testing Pipeline
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.x'
stages:
- stage: Test
displayName: 'Run All Tests'
jobs:
- job: UnitTests
displayName: 'Unit Tests'
steps:
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: '**/*UnitTests.csproj'
arguments: >
--configuration $(buildConfiguration)
--collect:"XPlat Code Coverage"
--logger trx
--results-directory $(Agent.TempDirectory)
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
searchFolder: '$(Agent.TempDirectory)'
- task: PublishCodeCoverageResults@2
displayName: 'Publish coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- job: IntegrationTests
displayName: 'Integration Tests'
dependsOn: UnitTests
services:
postgres:
image: postgres:15-alpine
ports:
- 5432:5432
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
steps:
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- task: DotNetCoreCLI@2
displayName: 'Run integration tests'
inputs:
command: 'test'
projects: '**/*IntegrationTests.csproj'
arguments: >
--configuration $(buildConfiguration)
--logger trx
env:
ConnectionStrings__DefaultConnection: 'Host=localhost;Database=testdb;Username=postgres;Password=test'
- job: E2ETests
displayName: 'E2E Tests'
dependsOn: IntegrationTests
steps:
- task: UseDotNet@2
inputs:
version: $(dotnetVersion)
- script: |
npx playwright install --with-deps
displayName: 'Install Playwright'
- task: DotNetCoreCLI@2
displayName: 'Build and run app'
inputs:
command: 'run'
projects: '**/MyApp.csproj'
env:
ASPNETCORE_ENVIRONMENT: 'Test'
- script: |
npm run test:e2e
displayName: 'Run E2E tests'
- task: PublishPipelineArtifact@1
condition: always()
inputs:
targetPath: 'playwright-report'
artifact: 'e2e-test-report'✅CI/CD Testing Best Practices
- Fail Fast - uruchom unit tests najpierw (szybkie), integration i E2E później. Zatrzymaj pipeline przy pierwszym błędzie.
- Parallel Execution - uruchamiaj testy równolegle dla szybkości (matrix builds, sharding)
- Test Environment Parity - CI environment powinien być identyczny z produkcją (same versions, configs)
- Artifact Storage - przechowuj test reports, screenshots, videos dla debugging
- Coverage Gates - blokuj merge jeśli coverage spadnie poniżej threshold (np. 70%)
- Notifications - alertuj zespół o failed builds (Slack, Teams, email)
Code Coverage i Metryki Jakości
Code coverage to procent kodu wykonanego przez testy. Często pada pytanie: "Ile coverage to dobre coverage?"
Odpowiedź nie jest prosta. Wysoki coverage nie gwarantuje jakości testów, ale niski coverage definitywnie sygnalizuje problem. Sprawdźmy kluczowe metryki i ich interpretację.
Code Coverage dla .NET
# Generuj coverage report
dotnet test --collect:"XPlat Code Coverage"
# Generuj HTML report z coverlet
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=html
# Użyj ReportGenerator dla pięknych raportów
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator \
-reports:"**/coverage.cobertura.xml" \
-targetdir:"coveragereport" \
-reporttypes:HtmlPakiety: coverlet.collector, ReportGenerator
Code Coverage dla JavaScript
// package.json
{
"scripts": {
"test": "jest --coverage",
"test:watch": "jest --watch --coverage"
}
}
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,ts,tsx}',
'!src/**/*.test.{js,ts,tsx}',
'!src/**/*.d.ts'
],
coverageThresholds: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
}
};Tools: Jest (built-in), Istanbul, nyc
Kluczowe Metryki Coverage:
Procent linii kodu wykonanych przez testy. Najbardziej podstawowa metryka. Target: 70-80%
Procent gałęzi (if/else, switch) przetestowanych. Ważniejsza niż line coverage dla logiki biznesowej. Target: 70%+
Procent funkcji/metod wywołanych przez testy. Identyfikuje dead code i nieużywane metody.
Procent statements wykonanych. Podobna do line coverage, ale bardziej granularna.
Coverage nie jest celem samym w sobie
100% coverage ≠ zero bugs. Możesz mieć 100% coverage z testami, które nic nie weryfikują. Przykład: test który wywołuje funkcję ale nie sprawdza rezultatu.
Skup się na jakości, nie ilości. Lepiej mieć 70% coverage z solidnymi testami weryfikującymi edge cases, niż 95% z powierzchownymi testami.
Testuj behavior, nie implementację. Coverage pokazuje CO jest testowane, ale nie JAK. Testuj public APIs i business logic, nie internal details.
Realistyczny target dla enterprise: 70-80% line coverage, 70%+ branch coverage. Priorytetowo testuj: business logic, edge cases, error handling, security-critical code.
Często Zadawane Pytania
Czym jest Test Pyramid i dlaczego jest ważna?
Test Pyramid to strategia testowania przedstawiająca optymalną proporcję różnych typów testów. U podstawy znajdują się szybkie i tanie unit testy (70%), w środku integration tests (20%), a na szczycie kosztowne testy E2E (10%). Taka struktura zapewnia szybki feedback, niskie koszty utrzymania i wysoką pewność jakości kodu.
Jaka jest różnica między unit testami a integration testami?
Unit testy sprawdzają pojedyncze komponenty w izolacji (funkcje, klasy) z zamockowanymi zależnościami - są szybkie i proste. Integration tests weryfikują współpracę kilku komponentów razem z prawdziwymi zależnościami (baza danych, API) - są wolniejsze ale wykrywają problemy integracyjne.
Czy powinienem używać Playwright czy Cypress do testów E2E?
Playwright to nowocześniejsze narzędzie z lepszym wsparciem dla wielu przeglądarek (Chrome, Firefox, Safari, Edge), szybszym wykonaniem i możliwością testowania wielu zakładek. Cypress ma łatwiejsze API i lepszą integrację z ekosystemem JavaScript. Dla nowych projektów rekomendujemy Playwright.
Jaki powinien być cel code coverage?
Realistycznym celem jest 70-80% code coverage dla kodu produkcyjnego. 100% coverage jest nieefektywne - ważniejsza jest jakość testów niż ilość. Skup się na testowaniu logiki biznesowej, edge cases i krytycznych ścieżek. Nie testuj getterów/setterów ani kodu frameworka.
Jak zintegrować testy z CI/CD pipeline?
Uruchamiaj unit testy przy każdym commit, integration tests przed merge do main, E2E testy po deploy do środowiska staging. Używaj parallel execution dla szybkości. Blokuj merge przy failed tests. Generuj raporty coverage i archiwizuj wyniki testów. Używaj GitHub Actions lub Azure DevOps dla automatyzacji.
Powiązane Artykuły
Azure DevOps Best Practices 2025
Kompleksowy przewodnik po Azure DevOps - od pipeline CI/CD przez testing, deployment strategię aż po monitoring i best practices.
Kompletny przewodnik Next.js 15
Next.js 15 z Server Components, testing strategies i deployment do produkcji - wszystko czego potrzebujesz.
Integracje i API dla Przedsiębiorstw
REST API, GraphQL, mikroservisy i automatyzacja - kompleksowy przewodnik po integracjach systemów enterprise.
Chcesz Wdrożyć Automated Testing w Swojej Firmie?
Testing strategy to nie tylko kod - to zmiana kultury zespołu. Pomogę Ci przejść przez cały proces:
- ✓ Audit istniejących testów i code coverage analysis
- ✓ Implementacja Test Pyramid (unit, integration, E2E)
- ✓ CI/CD integration z GitHub Actions lub Azure DevOps
- ✓ Training zespołu i best practices workshops
- ✓ Monitoring, metryki i continuous improvement
Gotowy na 90% mniej bugów w produkcji? Porozmawiajmy o Twoim projekcie.