Zaawansowane Podejście Obiektowe w Testach Automatycznych z TypeScript
Spis treści
- Wprowadzenie
- Architektura Rozwiązania
- Wzorce Projektowe
- Kluczowe Komponenty
- Praktyczne Zastosowanie
- Zarządzanie Typami i Interfejsami
- Zaawansowane Mechanizmy TypeScript
- Podsumowanie
Wprowadzenie
Przedstawione rozwiązanie demonstruje zaawansowane podejście do tworzenia testów automatycznych z wykorzystaniem TypeScript i Playwright. Głównym celem było stworzenie reużywalnej, łatwej w utrzymaniu i rozszerzalnej architektury do testowania interfejsu użytkownika, ze szczególnym uwzględnieniem operacji filtrowania danych w aplikacjach webowych.
Framework wykorzystuje nowoczesne praktyki projektowe takie jak:
- Programowanie zorientowane obiektowo
- Wzorzec projektowy strategii
- Abstrakcja i separacja odpowiedzialności
- Interfejsy i klasy generyczne
- Typowanie statyczne
Architektura Rozwiązania
Rozwiązanie opiera się na wielowarstwowej architekturze, która oddziela:
- Interfejsy - definiujące kontrakty dla klas implementujących
- Klasy abstrakcyjne - dostarczające bazową funkcjonalność
- Konkretne implementacje - specyficzne dla testowanych widoków
- Klasy akcji - implementujące logikę interakcji z UI
Diagram struktury projektu:
├── common/
│ ├── IElementComponents.ts # Interfejsy podstawowe
│ ├── BaseElementComponents.ts # Klasa abstrakcyjna
│ └── elementActions.ts # Główna klasa akcji
├── module-specific/
│ ├── components.ts # Konkretna implementacja dla modułu
│ └── test.ts # Testy dla danego modułu
└── utils/
└── index.ts # Narzędzia pomocnicze
Wzorce Projektowe
1. Wzorzec Strategii
Rozwiązanie intensywnie korzysta z wzorca strategii, gdzie różne algorytmy (strategie) są hermetyzowane i mogą być wymieniane. Przykład implementacji:
// Strategie wypełniania różnych typów pól
const fillStrategy: Record<
ElementType,
(key: T, val: RandomDataValue, elementId: string) => Promise<void>
> = {
[ElementType.NUMERIC_RANGE]: this.fillNumericRange.bind(this),
[ElementType.MULTISELECT]: this.fillMultiselect.bind(this),
[ElementType.TEXT]: this.fillText.bind(this),
[ElementType.DATE_RANGE]: this.fillDateRange.bind(this),
// Inne strategie...
};
// Użycie odpowiedniej strategii
await fillStrategy[type](key, searchValue, elementId);
2. Wzorzec Szablonowy (Template Method)
Klasa abstrakcyjna BaseElementComponents definiuje szkielet algorytmu, delegując implementację konkretnych kroków do podklas:
export abstract class BaseElementComponents<
T extends string | number,
> implements IElementComponents<T> {
// Wspólne implementacje
public getDataFieldLocator(dataField: string): Locator {
return this.page.locator(`[data-field="${dataField}"]`);
}
// Metody abstrakcyjne do implementacji przez podklasy
abstract getFilterOptionByIndex(index: T): Locator;
abstract getFilterTextInput(elementId: string, type?: ElementType): Locator;
// Inne metody...
}
3. Inversion of Control i Wstrzykiwanie Zależności
Klasy akcji przyjmują zależności poprzez konstruktor, co ułatwia testowanie i zwiększa elastyczność:
export class TestActions<T extends string | number> {
constructor(
private page: Page,
private elements: IElementComponents<T>,
) {}
// Metody wykorzystujące wstrzyknięte zależności
}
Kluczowe Komponenty
IElementComponents - Interfejs Bazowy
Definiuje podstawowy kontrakt, który muszą implementować wszystkie komponenty UI:
export interface IElementComponents<T extends string | number> {
mainButton: Locator;
closeIcons: Locator;
elementDefinitions: Record<T, ElementDefinition>;
getOptionByIndex(index: T): Locator;
getApplyButton(elementId: string): Locator;
getCancelButton(elementId: string): Locator;
getInputField(elementId: string, type?: ElementType): Locator;
getRangeInput(elementId: string, type: ElementType): { from: Locator; to: Locator };
getDataFieldLocator(dataField: string): Locator;
}
BaseElementComponents - Klasa Abstrakcyjna
Dostarcza częściową implementację interfejsu, pozostawiając specyficzne elementy do implementacji przez klasy pochodne:
export abstract class BaseElementComponents<
T extends string | number,
> implements IElementComponents<T> {
abstract mainButton: Locator;
abstract closeIcons: Locator;
abstract elementDefinitions: Record<T, ElementDefinition>;
// Implementacja wspólnych metod
protected constructor(protected page: Page) {}
public getDataFieldLocator(dataField: string): Locator {
return this.page.locator(`[data-field="${dataField}"]`);
}
// Pozostałe metody abstrakcyjne...
}
ModuleSpecificComponents - Konkretna Implementacja
Implementuje abstrakcyjną klasę bazową, dostarczając specyficzne dla modułu selektory i funkcje:
export class ModuleSpecificComponents extends BaseElementComponents<TestElementIndex> {
constructor(page: Page) {
super(page);
}
// Implementacja selektorów specyficznych dla modułu
public readonly elementDefinitions: Record<TestElementIndex, ElementDefinition> = {
[TestElementIndex.IDENTIFIER]: {
label: 'Identyfikator',
locator: () => this.identifierElement,
dataField: 'identifier',
type: ElementType.TEXT,
},
// Inne definicje elementów...
};
// Gettery dla selektorów elementów
get mainButton(): Locator {
return this.page.locator(this.MAIN_BUTTON_SELECTOR);
}
// Pozostałe implementacje zgodne z interfejsem
}
TestActions - Główna Klasa Akcji
Centralna klasa implementująca operacje wykonywane na elementach interfejsu:
export class TestActions<T extends string | number> {
constructor(
private page: Page,
private elements: IElementComponents<T>,
) {}
/**
* Pobiera liczbę zdefiniowanych elementów.
*/
public getElementCount(): number {
return Object.keys(this.elements.elementDefinitions).length;
}
/**
* Otwiera element i opcjonalnie przypina go.
* @param key - Klucz elementu do otwarcia
* @param pin - Czy element powinien zostać przypięty
*/
public async openElement(key: T, pin: boolean = false): Promise<Locator> {
// Implementacja
}
/**
* Ekstrahuje wartości z pola danych na podstawie typu elementu.
*/
public async extractAllDataValues(elementIndex: T, option: TestOption): Promise<string[]> {
// Implementacja ekstrakcji danych
}
// Inne metody akcji...
}
Praktyczne Zastosowanie
Przykład testu wykorzystującego stworzoną architekturę:
test('Element1.GivenUserIsOnPage_WhenApplyingFilter_ThenListIsFiltered @regression', async () => {
// ARRANGE
const elementIndex = TestElementIndex.IDENTIFIER;
const searchValue = await testActions.getRandomValue(elementIndex);
// ACT
await testActions.applyElementAndCompareLabel(elementIndex, searchValue);
const result = await testActions.verifyFilteredResults(elementIndex, searchValue);
// ASSERT
expect(result).toBeTruthy();
});
Zarządzanie Typami i Interfejsami
Typy Wyliczeniowe (Enums)
Definiują dostępne opcje i stany:
export enum ElementType {
TEXT = 'text',
MULTISELECT = 'multiselect',
NUMERIC_RANGE = 'numericRange',
DATE_RANGE = 'dateRange',
SWITCH = 'switch',
STATUS = 'status',
}
export enum TestOption {
SHORT_TEXT = 'SHORT_TEXT',
FULL_TEXT = 'FULL_TEXT',
WITH_EMPTY = 'WITH_EMPTY',
}
export enum RangeOption {
Both = 'both',
FromOnly = 'fromOnly',
ToOnly = 'toOnly',
}
Strażnicy Typów (Type Guards)
Zapewniają bezpieczne operacje na typach:
// Typ złożony
type RandomDataValue = string | { from: number; to: number } | { from: string; to: string } | null;
// Strażnicy typu
function isRangeValue(val: any): val is { from: string; to: string } {
return typeof val === 'object' && val !== null && 'from' in val && 'to' in val;
}
function isStringValue(val: any): val is string {
return typeof val === 'string';
}
// Przykład użycia
if (isRangeValue(value)) {
// TypeScript wie, że value ma właściwości from i to
console.log(value.from, value.to);
} else if (isStringValue(value)) {
// TypeScript wie, że value jest stringiem
console.log(value.toUpperCase());
}
Zaawansowane Mechanizmy TypeScript
Typy Generyczne
Rozwiązanie intensywnie wykorzystuje typy generyczne do zapewnienia elastyczności i typowej bezpieczeństwa:
export class TestActions<T extends string | number> {
// T jest parametrem generycznym ograniczonym do string lub number
// Umożliwia to używanie enumów jako indeksów do obiektów
}
Mapowanie Typów i Rekordy
Używane do tworzenia typów obiektów o dynamicznych kluczach:
// Dynamiczne mapowanie typów
const rangeExtractors: { [key in ElementType]?: (values: string[], side: RangeOption) => RandomDataValue } = {
[ElementType.NUMERIC_RANGE]: this.getNumericRangeValue.bind(this),
[ElementType.DATE_RANGE]: this.getDateRangeValue.bind(this),
};
// Typowe rekordy
private labelFormatters: Record<ElementType, (label: string, value: RandomDataValue) => string> = {
[ElementType.TEXT]: (label, value) => `${label}:Contains "${value}"`,
[ElementType.NUMERIC_RANGE]: (label, value) => {
// Implementacja formatowania dla zakresu liczbowego
},
// Inne formatery...
};
Destrukturyzacja i Spread
Używane do czytelnej manipulacji obiektami:
// Destrukturyzacja
const { searchArea, applyButton } = await this.getElementLocators(elementId, type);
// Spread operator
const filteredTexts = Array.from(new Set([...existingTexts, ...additionalTexts]));
Podsumowanie
Przedstawione rozwiązanie demonstruje zaawansowane umiejętności programistyczne w zakresie:
- Projektowania obiektowego - prawidłowe zastosowanie dziedziczenia, interfejsów i abstrakcji
- Typowania - skuteczne wykorzystanie systemu typów TypeScript w celu zapewnienia bezpieczeństwa typów
- Wzorców projektowych - implementacja uznanych wzorców w celu zwiększenia elastyczności i reużywalności kodu
- Clean Code - strukturyzacja kodu zgodnie z zasadami SOLID i DRY
- Testowania - testy czytelne, zwięzłe i łatwe w utrzymaniu
Rozwiązanie to można łatwo rozszerzać o nowe moduły i typy elementów, zachowując spójność architektury i wysoką jakość kodu.