Automation testing
Tích hợp Winston Logger trong Playwright TypeScript: Xây dựng hệ thống logging chuyên nghiệp cho Test Automation

Trong quá trình phát triển và maintain test automation framework, logging đóng vai trò cực kỳ quan trọng trong việc debug, monitoring và phân tích test results. Bài viết này sẽ hướng dẫn chi tiết cách tích hợp Winston – một trong những logging library mạnh mẽ nhất của Node.js – vào Playwright TypeScript framework.
Tại sao cần Winston Logger trong Playwright?
Playwright cung cấp sẵn console.log và debug mode, nhưng những công cụ này còn nhiều hạn chế khi framework scale lên:
- Thiếu cấu trúc: Console.log không phân biệt được log levels (error, warning, info, debug)
- Khó quản lý output: Không thể dễ dàng lưu logs vào file hoặc gửi đến external services
- Performance impact: Console.log trong production tests có thể ảnh hưởng đến performance
- Thiếu context: Khó track được logs từ test case nào, step nào
Winston giải quyết tất cả các vấn đề trên với khả năng:
- Multiple log levels và custom formats
- Multiple transports (console, file, database, cloud services)
- Async và performant logging
- Rich metadata và context tracking
Cài đặt và Setup cơ bản
Bước 1: Cài đặt dependencies
npm install winston winston-daily-rotate-file
npm install --save-dev @types/node
- Trong đó:
DailyRotateFilelà thư viện giúp tự động chuyển sang 1 file khác khi sang ngày mới, giúp tránh 1 file có dung lượng quá lớn.
Bước 2: Tạo Logger Configuration
Tạo file src/utils/logger.ts:
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import path from 'path';
// Define log levels với colors
const logLevels = {
levels: {
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
},
colors: {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
verbose: 'cyan',
debug: 'blue',
silly: 'grey'
}
};
// Add colors to winston
winston.addColors(logLevels.colors);
// Define log format
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
// Console format với colors
const consoleFormat = winston.format.combine(
winston.format.colorize({ all: true }),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
winston.format.printf(({ timestamp, level, message, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
})
);
// Create transports
const transports: winston.transport[] = [];
// Console transport
if (process.env.NODE_ENV !== 'production') {
transports.push(
new winston.transports.Console({
format: consoleFormat,
level: process.env.LOG_LEVEL || 'debug'
})
);
}
// File transport for all logs
transports.push(
new DailyRotateFile({
filename: path.join('logs', '%DATE%-combined.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: logFormat,
level: 'info'
})
);
// File transport for errors only
transports.push(
new DailyRotateFile({
filename: path.join('logs', '%DATE%-error.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d',
level: 'error',
format: logFormat
})
);
// Create logger instance
const logger = winston.createLogger({
levels: logLevels.levels,
format: logFormat,
transports,
exitOnError: false
});
export default logger;Tích hợp Logger vào Playwright Test Framework
Tạo Test Logger Wrapper
Tạo file src/utils/testLogger.ts để wrap logger với test context:
import logger from './logger';
import { TestInfo } from '@playwright/test';
export class TestLogger {
private testInfo: TestInfo;
private testName: string;
constructor(testInfo: TestInfo) {
this.testInfo = testInfo;
this.testName = testInfo.title;
}
private formatMessage(message: string): string {
return `[${this.testName}] ${message}`;
}
private getMetadata(additionalData?: any) {
return {
testFile: this.testInfo.file,
testTitle: this.testInfo.title,
project: this.testInfo.project.name,
retry: this.testInfo.retry,
workerIndex: this.testInfo.workerIndex,
...additionalData
};
}
info(message: string, metadata?: any) {
logger.info(this.formatMessage(message), this.getMetadata(metadata));
}
debug(message: string, metadata?: any) {
logger.debug(this.formatMessage(message), this.getMetadata(metadata));
}
warn(message: string, metadata?: any) {
logger.warn(this.formatMessage(message), this.getMetadata(metadata));
}
error(message: string, error?: Error, metadata?: any) {
const errorMetadata = {
...this.getMetadata(metadata),
error: error ? {
message: error.message,
stack: error.stack,
name: error.name
} : undefined
};
logger.error(this.formatMessage(message), errorMetadata);
}
step(stepName: string, metadata?: any) {
logger.info(this.formatMessage(`Step: ${stepName}`), {
...this.getMetadata(metadata),
type: 'test-step'
});
}
screenshot(path: string, metadata?: any) {
logger.info(this.formatMessage('Screenshot captured'), {
...this.getMetadata(metadata),
screenshotPath: path,
type: 'screenshot'
});
}
apiCall(method: string, url: string, statusCode?: number, duration?: number) {
logger.http(this.formatMessage('API Call'), {
...this.getMetadata(),
method,
url,
statusCode,
duration,
type: 'api-call'
});
}
}Tích hợp vào Test Fixtures
Tạo custom test fixtures trong src/fixtures/testFixtures.ts:
logger: sử dụng chủ độngautoLogger: tự động log ở các hook lúc chạy test
import { test as base, Page } from '@playwright/test';
import { TestLogger } from '../utils/testLogger';
// Extend basic test với logger
export const test = base.extend<{
logger: TestLogger;
autoLogger: Page;
}>({
logger: async ({ }, use, testInfo) => {
const logger = new TestLogger(testInfo);
// Log test start
logger.info('Test started', {
annotations: testInfo.annotations,
expectedStatus: testInfo.expectedStatus
});
// Use logger trong test
await use(logger);
// Log test completion
const duration = testInfo.duration;
const status = testInfo.status;
if (status === 'passed') {
logger.info(`Test completed successfully`, { duration, status });
} else if (status === 'failed') {
logger.error(`Test failed`, undefined, {
duration,
status,
errors: testInfo.errors
});
} else {
logger.warn(`Test ${status}`, { duration, status });
}
},
autoLogger: async ({ page, logger }, use) => {
// Auto-log navigation
page.on('load', () => {
logger.debug(`Page loaded: ${page.url()}`);
});
// Auto-log console messages
page.on('console', msg => {
const type = msg.type();
const text = msg.text();
if (type === 'error') {
logger.error(`Browser console error: ${text}`);
} else if (type === 'warning') {
logger.warn(`Browser console warning: ${text}`);
} else {
logger.debug(`Browser console: ${text}`);
}
});
// Auto-log network failures
page.on('requestfailed', request => {
logger.error(`Request failed: ${request.url()}`, undefined, {
method: request.method(),
failure: request.failure()
});
});
// Auto-log page errors
page.on('pageerror', error => {
logger.error('Page error occurred', error);
});
await use(page);
}
});
export { expect } from '@playwright/test';Sử dụng Logger trong Test Cases
Ví dụ Test Case với Logging
import { test, expect } from '../fixtures/testFixtures';
test.describe('E-commerce Checkout Flow', () => {
test('Complete purchase with valid credit card', async ({ autoLogger, logger }) => {
// Step 1: Navigate to homepage
logger.step('Navigate to homepage');
await autoLogger.goto('https://example-shop.com');
// Step 2: Search for product
logger.step('Search for product', { productName: 'Laptop' });
await autoLogger.fill('[data-testid="search-input"]', 'Laptop');
await autoLogger.click('[data-testid="search-button"]');
// Log search results
const resultsCount = await autoLogger.locator('.product-item').count();
logger.info(`Found ${resultsCount} products`, {
searchTerm: 'Laptop',
resultsCount
});
// Step 3: Add to cart
logger.step('Add first product to cart');
await autoLogger.click('.product-item:first-child [data-testid="add-to-cart"]');
// Step 4: Checkout
logger.step('Proceed to checkout');
await autoLogger.click('[data-testid="cart-icon"]');
await autoLogger.click('[data-testid="checkout-button"]');
// Step 5: Fill payment information
logger.step('Fill payment information');
try {
await autoLogger.fill('[data-testid="card-number"]', '4111111111111111');
await autoLogger.fill('[data-testid="card-expiry"]', '12/25');
await autoLogger.fill('[data-testid="card-cvc"]', '123');
logger.debug('Payment form filled successfully');
} catch (error) {
logger.error('Failed to fill payment form', error as Error);
// Capture screenshot on error
const screenshotPath = `screenshots/payment-error-${Date.now()}.png`;
await autoLogger.screenshot({ path: screenshotPath });
logger.screenshot(screenshotPath);
throw error;
}
// Step 6: Complete purchase
logger.step('Complete purchase');
await autoLogger.click('[data-testid="place-order"]');
// Verify order confirmation
const orderNumber = await autoLogger.locator('[data-testid="order-number"]').textContent();
logger.info('Order placed successfully', { orderNumber });
expect(orderNumber).toBeTruthy();
});
});Page Object Model với Logging
// src/pages/LoginPage.ts
import { Page } from '@playwright/test';
import { TestLogger } from '../utils/testLogger';
export class LoginPage {
constructor(
private page: Page,
private logger: TestLogger
) {}
async navigate() {
this.logger.debug('Navigating to login page');
await this.page.goto('/login');
}
async login(username: string, password: string) {
this.logger.step(`Login with username: ${username}`);
try {
await this.page.fill('[data-testid="username"]', username);
await this.page.fill('[data-testid="password"]', password);
this.logger.debug('Credentials entered, submitting form');
await this.page.click('[data-testid="login-button"]');
// Wait for navigation
await this.page.waitForURL('/dashboard', { timeout: 10000 });
this.logger.info('Login successful', { username });
} catch (error) {
this.logger.error(`Login failed for user: ${username}`, error as Error);
// Capture state for debugging
const currentUrl = this.page.url();
const errorMessage = await this.page.locator('.error-message').textContent().catch(() => 'No error message found');
this.logger.error('Login failure details', undefined, {
currentUrl,
errorMessage,
username
});
throw error;
}
}
async logout() {
this.logger.step('Logout');
await this.page.click('[data-testid="logout-button"]');
this.logger.info('Logged out successfully');
}
}Một số logging pattern nâng cao
1. Performance Logging
- Tính thời gian load trang và các thông số liên quan tới performance của trang web
// src/utils/performanceLogger.ts
import { Page } from '@playwright/test';
import { TestLogger } from './testLogger';
export class PerformanceLogger {
constructor(
private page: Page,
private logger: TestLogger
) {}
async logNavigationTiming(url: string) {
await this.page.goto(url);
const timing = await this.page.evaluate(() => {
const perf = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
return {
domContentLoaded: perf.domContentLoadedEventEnd - perf.domContentLoadedEventStart,
loadComplete: perf.loadEventEnd - perf.loadEventStart,
domInteractive: perf.domInteractive - perf.fetchStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0,
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0
};
});
this.logger.info('Page performance metrics', {
url,
metrics: timing,
type: 'performance'
});
// Alert if page load is slow
if (timing.loadComplete > 3000) {
this.logger.warn(`Slow page load detected: ${timing.loadComplete}ms`, {
url,
threshold: 3000,
actual: timing.loadComplete
});
}
}
async measureAction<T>(
actionName: string,
action: () => Promise<T>
): Promise<T> {
const startTime = Date.now();
try {
const result = await action();
const duration = Date.now() - startTime;
this.logger.info(`Action completed: ${actionName}`, {
duration,
type: 'action-timing'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error(`Action failed: ${actionName}`, error as Error, {
duration,
type: 'action-timing'
});
throw error;
}
}
}2. API Request Logging
// src/utils/apiLogger.ts
import { APIRequestContext, APIResponse } from '@playwright/test';
import { TestLogger } from './testLogger';
export class APILogger {
constructor(
private request: APIRequestContext,
private logger: TestLogger
) {}
async get(url: string, options?: any): Promise<APIResponse> {
const startTime = Date.now();
try {
const response = await this.request.get(url, options);
const duration = Date.now() - startTime;
this.logger.apiCall('GET', url, response.status(), duration);
if (!response.ok()) {
const responseBody = await response.text().catch(() => 'Unable to get response body');
this.logger.warn(`API request returned non-OK status`, {
method: 'GET',
url,
status: response.status(),
responseBody
});
}
return response;
} catch (error) {
this.logger.error(`API request failed: GET ${url}`, error as Error);
throw error;
}
}
async post(url: string, options?: any): Promise<APIResponse> {
const startTime = Date.now();
try {
// Log request details
this.logger.debug(`Making POST request`, {
url,
headers: options?.headers,
dataSize: JSON.stringify(options?.data || {}).length
});
const response = await this.request.post(url, options);
const duration = Date.now() - startTime;
this.logger.apiCall('POST', url, response.status(), duration);
return response;
} catch (error) {
this.logger.error(`API request failed: POST ${url}`, error as Error);
throw error;
}
}
}3. Test Report Integration
<em>// src/reporters/logReporter.ts</em>
import { Reporter, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
import logger from '../utils/logger';
export class LogReporter implements Reporter {
private totalTests = 0;
private passedTests = 0;
private failedTests = 0;
private skippedTests = 0;
onBegin(config: any, suite: any) {
this.totalTests = suite.allTests().length;
logger.info('Test run started', {
totalTests: this.totalTests,
workers: config.workers,
projects: config.projects.map((p: any) => p.name)
});
}
onTestEnd(test: TestCase, result: TestResult) {
const testInfo = {
title: test.title,
file: test.location.file,
line: test.location.line,
duration: result.duration,
status: result.status,
retry: result.retry
};
switch (result.status) {
case 'passed':
this.passedTests++;
logger.info('Test passed', testInfo);
break;
case 'failed':
this.failedTests++;
logger.error('Test failed', undefined, {
...testInfo,
error: result.error
});
break;
case 'skipped':
this.skippedTests++;
logger.warn('Test skipped', testInfo);
break;
}
}
onEnd(result: FullResult) {
const summary = {
status: result.status,
duration: result.duration,
totalTests: this.totalTests,
passed: this.passedTests,
failed: this.failedTests,
skipped: this.skippedTests,
passRate: ((this.passedTests / this.totalTests) * 100).toFixed(2) + '%'
};
if (result.status === 'passed') {
logger.info('Test run completed successfully', summary);
} else {
logger.error('Test run failed', undefined, summary);
}
}
}
Thêm reporter vào playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['html'],
['./src/reporters/logReporter.ts']
],
// ... other config
});Best Practices khi sử dụng Winston với Playwright
1. Log Levels Strategy: Dùng log nào, khi nào
- ERROR: Test failures, những lỗi bất ngờ, browser crashes
- WARN: Test bị skipped, performance chậm, các tính năng bị deprecated
- INFO: Test start/end, business logic quan trọng, test results
- HTTP: API calls, network requests
- DEBUG: khi cần debug, in ra các thông tin để kiểm tra.
- VERBOSE: Raw data, full response bodies
2. Structured Logging
Luôn sử dụng structured logging với metadata thay vì cộng chuỗi:
// Bad
logger.info(`User ${username} logged in at ${timestamp}`);
// Good
logger.info('User logged in', { username, timestamp });3. Sensitive Data Handling
- Thêm các hook để “che bớt” các dữ liệu nhạy cảm (Password, api key, token,…)
// src/utils/sanitizer.ts
export class LogSanitizer {
private sensitivePatterns = [
{ pattern: /password=\S+/gi, replacement: 'password=***' },
{ pattern: /apikey=\S+/gi, replacement: 'apikey=***' },
{ pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, replacement: '****-****-****-****' }, // Credit card
{ pattern: /bearer\s+\S+/gi, replacement: 'bearer ***' }
];
sanitize(message: string): string {
let sanitized = message;
this.sensitivePatterns.forEach(({ pattern, replacement }) => {
sanitized = sanitized.replace(pattern, replacement);
});
return sanitized;
}
}4. Một số lưu ý về performance
- Sử dụng async transports cho production
- Rotate log files để tránh file quá to và có thể quản lý dễ dàng hơn
- Log theo từng môi trường.
Monitoring và Analysis
Gửi log lên ELK để monitor
// src/utils/elkTransport.ts
import winston from 'winston';
import { ElasticsearchTransport } from 'winston-elasticsearch';
const esTransport = new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200'
},
index: 'playwright-tests',
dataStream: true,
transformer: (logData) => {
return {
'@timestamp': new Date().toISOString(),
message: logData.message,
severity: logData.level,
fields: logData.meta
};
}
});
// Add to winston transports
logger.add(esTransport);Một số metric quan trọng để monitor
- Test failure rate theo thời gian
- Slowest test cases
- Most common error patterns
- API response times
- Browser console errors frequency
Tìm hiểu thêm
- Winston Documentation – Official Winston documentation
- Playwright Reporters – Built-in và custom reporters
- ELK Stack for Test Automation – Log aggregation và visualization
- Structured Logging Best Practices – JSON logging patterns
- Node.js Logging Libraries Comparison – So sánh các logging libraries
