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

Phong Do Phong Do · 05/09/2025 · 21 phút đọc

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 đó: DailyRotateFile là 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ủ động
  • autoLogger: 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

Phong Do
Tác giả
Phong Do