ReportPortal.io và kết hợp với Protractor và Protractor-CucumberJS (Part-2)

Ở các phần trước thì bọn mình đã giới thiệu với các bạn về cách setup ReportPortal.io với docker-compose 😀 , ngoài ra thì tụi mình cũng đã giới thiệu sơ sơ về các tính năng chính của ReportPortal.io cũng như cách integrate với Katalon Studio. Bạn nào chưa biết có thể tham khảo lại ở các links dưới đây :

Phần này mình sẽ giới thiệu cách các bạn intergate ReportPortal.io với các test framework được viết bằng ngôn ngữ JavaScript. Hiện tại bên mình có viết 1 library cho việc integrate với các Test Framework như Jasmine, Protractor, Protractor-Cucumber-JS và Mocha (với mocha thì mình đang viết thêm vì thế version hiện tại nó vẫn sẽ chưa có). Bọn mình cũng sẽ chia sẽ source code để bạn nào thích thì cũng có thể lấy về custom lại theo ý mình =)))


5 phút quảng cáo cho thư viện report-portal-js-agent

Trước khi mình đi vào cách sử dụng library này thì bọn mình sẽ nói sơ qua 1 chút về ba cái đồ sida này (nhưng mà nó ko sida tí nào (★ ̄∀ ̄★)  hàng bọn mình làm ra mà lị). Library này sẽ gồm 2 phần bao gồm phần integrate với ReportPortal.io và phần sẽ “đẻ” ra những file log details bằng library Log4JS. Lý do đơn giản là do bọn mình thấy nhu cầu khi sử dụng những testing framework hay test automation framework cần lắm những file log details để dễ dàng hơn trong việc phân tích hoặc trace lại kết quả sau khi run test. Mình sẽ có cái hình dưới đây đại khái có thể coi như là cách mà library sẽ chạy khi integrate với các test framework:

Chi tiết về cách hoạt động của thư viện

Chỗ này mình giải thích hơi technical nên bạn nào quan tâm có thể coi còn ko thì đi xuống phần dưới để coi cách config và cách để sử dụng library cũng được.

Thư viện bọn mình sử dụng có phần core gồm 2 thư viện chính là: ReportPortal-Client và Log4JS.

Với ReportPortal-Client thì bọn mình sẽ dựa vào nó để viết những agent cho từng test framework. Ở đây agent sẽ có nhiệm vụ lắng nghe những execution của test framework và gởi những thông tin ở từng phase của execution lên ReportPorrtal.io. Ví dụ với jasmine thì agent sẽ lắng nghe khi nào jasmine start, suite start, spect start, etc. Cũng như capture screenshot khi test end để send lên ReportPortal.io.

Ngoài ra thì đặc thù của JavaScript là cơ chế chạy asynchronous nên dẫn tới việc nếu ko có cách khiến cho việc send request được đồng bộ thì sẽ dẫn tới thông tin trên ReportPortal.io sẽ rất lộn xộn, chưa kể 1 lý do nữa là do cơ chế sử dụng listener của các test framework, nên có thể việc send request sẽ trở thành thảm họa nếu kết hợp với việc send request kèm screenshot cho test result đó, do việc capture screenshot sẽ chạy theo cơ chế asynchronous, dẫn tới có thể khi có hình ảnh thì lúc này bạn sẽ attach screenshot không đúng với cái test result cần attach (ví dụ: TESTA nhận thông tin là end test, lúc này bạn gọi method để capture screenshot nhưng do cơ chế async thì tạm thời request send lên screenshot sẽ chưa send liền và sau đó listener lại send tiếp request end test và như vậy sẽ dẫn tới bạn sẽ end TESTA và như vậy TESTA sẽ ko có screenshot trên ReportPortal.io).

Để giải quyết việc này thì bọn mình sẽ dùng cơ chế lưu những requests cần send lên ReportPortal.io vô 1 mảng và chờ đến khi user trigger thì sẽ send toàn bộ request đó và bọn mình sẽ sử dụng cơ chế aysnc/await để đảm bảo thứ tự của từng request cho tới lúc send xong sẽ map tương ứng với execution flow cuả test framework.

Với Log4JS thì bọn mình sử dụng gồm 2 mục đích chính, thứ nhất là sẽ lưu ra những file log details để mọi người có thể dùng nó để phân tích hoặc trace lại kết quả khi cần, thứ hai là sẽ dùng chính method log của Log4JS để send những thông tin log lên ReportPortal.io nếu trong test method có sử dụng log method.

Để giải quyết vấn đề send log lên ReportPortal.io thì bọn mình viết 1  custom appender với Log4JS, lúc này khi các bạn config cho Log4JS chỉ cần include cái custom appender này vào thì khi các bạn sử dụng method log trong phần execute test thì library của bọn mình cũng sẽ tự động send thông tin log đó lên ReportPortal.io. Ngoài ra bạn nào muốn tạo 1 custom log4js appender thì có thể coi bài viết ở đây

Cài đặt report-portal-js-agent

Các bạn có thể cài đặt library của bọn mình thông qua npm (đây là hệ thống quản lý thư viện dành cho JavaScript và nó giúp cho cuộc sống của mình trỡ nên dễ dàng hơn =)))

npm install –save report-portal-js-agent 

Tạo file config cho report-portal-js-agent 

Kế tiếp là các bạn làm theo các bước sau để thiết lập agent cho Protractor, lúc này khi các bạn run test với Protractor framework thì nó sẽ send kết quả về cho ReportPortal.io. Ok, nói dăm ba câu chữ đủ rồi giờ là cách để config và sử dụng library này nào ( ´∀`)

Tạo file config cho Log4JS

Như mình đề cập ở trên thì do library này mình sẽ có 2 phần trong đó phần tạo ra file log details sẽ do Log4JS đảm nhận, vì vậy bọn mình cần config 1 xí để nó vừa có thể tạo ra file log details cũng như send những thông tin log khi user sử dụng method log ở Log4JS lên ReportPortal.io. 

Ở đây mình chia sẽ mẹo nhỏ là đối với những file config thì tốt nhất các bạn nên chia nhỏ ra cái nào là config base thì là 1 file riêng, rồi từ file config base đó các bạn có thể extend thành những file config khác nhau ví dụ như config cho việc run test với các test script chỉ dành cho smoke test hoặc config cho việc run test với toàn bộ test scripts. các bạn có thể coi tham khảo cái structure này của bọn mình làm dưới đây cũng như sample config cho Log4JS

log4j.config.js

// Include thư viện log4js vào
const log4js = require('log4js');
// tạo 1 method để config log4js khi sử dụng trong 
// file main config
// method này sẽ nhận vào đường dẫn của file 
// chứa đường dẫn ReportPortalAppender
const Log4jsConfig = log4jsReportPortalAppender => {
  log4js.configure({
    appenders: {
// tạo 1 config cho log4js appender 
// có tên là reportportal
// có type là function ReportPortalAppender 
      reportportal: {
        type: log4jsReportPortalAppender,
        layout: {
          type: 'basic',
        },
      },
// tạo 1 config cho log4js appender 
// có tên là consolelog
// có type là function dùng để xuất log ra console 
      consolelog: {
        type: 'stdout',
        layout: {
          type: 'basic',
        },
      },
// tạo 1 config cho log4js appender 
// có tên là detailLogFile
// có type là function 
// dùng để tạo ra những file details log 
      detailLogFile: {
        type: 'multiFile',
        base: 'logs/',
        property: 'categoryName',
        extension: '.log',
        layout: {
          type: 'basic',
        },
// thiết lập kích thước tối đa của 1 file log là 10mb
// cứ đủ 10MB là sẽ spilt ra 1 file 
        maxLogSize: 10485760,
// Chỉ giữ lại tối đa 3 file 
// EX: khi file log đủ 10MB nó split ra thành 1 file 
// cứ như vậy cho đến khí đủ 3 file thì sẽ xóa file 
// có thời gian xa nhất 
        backups: 3,
// file backup sẽ đc nén lại để tối ưu việc lưu trữ
        compress: true,
      },
    },
/**
Config để cho log4js biết send những log message
đúng với từng loại appender nào    
nôm na là nếu bạn ko set vô cái list appenders 
giá trị nào thì log4js sẽ ko send log message 
lên ReportPortal hay hiển thị ra console 
cũng như lưu ra file 
*/
    categories: {
      default: {
        appenders: [
                    'detailLogFile', 
                    'reportportal', 
                    'consolelog'
                    ],
        level: 'all',
      },
    },
  });
  return log4js;
};

exports.Log4jsConfig = Log4jsConfig;

Tạo file config cho Protractor

Các bạn nào sử dụng Protractor thì chắc đã biết để run Protractor thì mình sẽ dùng cú pháp là 

protractor file_protractor_config

Dưới đây là file config protractor.config.js dùng cho việc run test với Protractor 

protractor.config.js

const path = require('path');

// Import những thư viện mà và method cần để send
// thông tin khi run test lên ReportPortal.io
// Log4jsReportPortal: là Log4JS custom appender 
// ReportPortalAgent: là chứa method cần cho việc send thông tin lên ReportPortal.io
// JasmineAgent: là listener cho việc send thông tin lên ReportPorta.io dựa vào execution flow của Jasmine
const {
  Log4jsReportPortal,
  ReportPortalAgent,
  JasmineAgent
} = require('../../lib');

// Import method config cho Log4JS 
// mình đã giới thiệu qua ở phần trên
const { Log4jsConfig } = require('./log4js');
// Import config của ReportPortal.io
// file JSON này sẽ chứa những thông tin cần thiết 
// để agent có thể send được request lên ReportPortal.io
// Ví dụ như token để send request, project name, etc.
/**
 * 
{
  "token": "f1d908c2-8ee9-4df7-8298-ca08ee28f04c",
  "endpoint": "http://localhost:6060/api/v1",
  "launch": "protractor-demo-refactor",
  "project": "demo-agent",
  "attachPicturesToLogs": true,
  "takeScreenshot": true,
  "debug": false
}
*/
const reportConfig = require('./report/reportportal.config.json');

// Tạo 1 ReportPortalAgent
const agent = new ReportPortalAgent(reportConfig);

// Thiết lập cho method Log4JS custom appender
// lý do nhận vô instance agent là do mình cần send 
// những thông tin của test log lên ReportPortal.io 
const log4jsReportPortal = new Log4jsReportPortal(agent);

// exports lại configure của Log4JS custom appender
// để Log4JS có thể dispatch data vô cusom appender
exports.configure = log4jsReportPortal.configure;

// Phần main config cho Protractor
exports.config = {
  framework: 'jasmine',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['../protractor/spec-1.js', '../protractor/spec-2.js'],
  onPrepare: () => {

// Add JasmineAgent vô Jasminse Reporter listener 
    jasmine.getEnv().addReporter(new JasmineAgent(agent));
// Cho phép send log lên ReportPortal.io thông qua việc gọi
// browser.log trong test method 
    browser.log = agent.sendLog;
  },
  beforeLaunch: () => {
/**
Truyền vô đường dẫn của Log4JS custom appender
lý do mình lấy đường đẫn của chính file này thì ở phần trên
mình đã export cái method custom appender ở chính file này
thông qua exports.configure = log4jsReportPortal.configure;
*/
    Log4jsConfig(path.join(__dirname, 'protractor.config.js'));
  },
  afterLaunch: async () => {}
};

Dưới đây là 1 test script sample 

spec-1.js

const { LoggerHelper } = require('../../lib/log4js');
const log = new LoggerHelper('Test');
const { browser } = require('protractor');
describe('Protractor Demo App Spec-1', function() {
  beforeEach(function() {
    browser.ignoreSynchronization = true;
    browser.get('https://www.google.com');
  });
  it('should create data successfully Spec 1', function() {
    console.log('should create data successfully Spec 1 1');
    // Send log info lên ReportPortal.io
    // ở config phía trên các bạn có thể thấy mình đã config chỗ này
    browser.log('Sample log message with direct agent', 'INFO');
    log.logInfo('should create data successfully Spec 1 1');
    log.logInfo('should create data successfully Spec 1 2');
    log.logInfo('should create data successfully Spec 1 3');
    log.logInfo('should create data successfully Spec 1 4');
    log.logInfo('should create data successfully Spec 1 5');
    expect(3).toEqual(3);
  });
});

Giờ mình cùng xem thử thành quả với Jasmine agent nào



Tạo file config cho Protractor-Cucumber-JS

Lưu ý là phần này sẽ có những thuật ngữ nếu bạn chưa từng làm việc với cucumber thì sẽ không hiểu cho nên khuyến cáo của mình là nếu bạn không làm việc với cucumber-js thì có thể bỏ qua luôn phần này (◍ȋ ₎໐͜₍ ȋ◍). Còn nếu bạn nào đang quan tâm về cucumber thì có thể tham khảo những links sau:

Cũng tương tự như file config cho Protractor nhưng chỉ khác chút xíu là bên cucumber sẽ có thêm phần config riêng cho cucumber-js. Nhưng trước hết mình xin nói sơ qua 1 chút về structure của bọn mình khi sử dụng cucumber để các bạn sẽ hiểu rõ hơn khi đọc sample config file.

Dưới đây là structure của bọn mình cho cucumber các bạn có thể tham khảo cũng được. Structure sẽ bao gồm folder chứa toàn bộ các feature files để run test và nó nằm ở folder sample (các bạn có thể tạo ra từng folder riêng dựa vào tên feature của các bạn), steps folder dùng để chứa toàn bộ những step definition implementation, và supports folder sẽ chứa những file listener hoặc hook cho cucumber-js.

Đây là file cucumber.config.js (như hình structure config của mình ở phía trên thì mình sẽ tách file config riêng của cucumber ra 1 chỗ riêng để có thể reuse cũng như change config dễ dàng)

cucumber.config.js

/**
 *
 * @param {*} isReRun is only run previously 
 * failed test script which stored in @rerun.txt 
 * or runs all
 */

function getCucumberOpts(isReRun, tags) {
  tags = tags ? tags : [];
  let opts = {
/**
Chỉ định folder chứa những step definition để run
feature file, trong đây sẽ bao gồm cả
hook file - file chứa CucumberListener cho ReportPortal.io
Các bạn có thể thấy ứng với structure của bọn mình ở hình trên
với cách này các bạn có thể config cho file config cucubmer
của mình include chính xác những step nào cần run
hoặc những hook nào cần sử dụng 
tương ứng với từng protractor-cucumber config 
Ví dụ: mình sẽ có cucumber.config.home, cucumber.config.login
tương ứnng với protractor.cucumber.smoke.config 
mình sẽ chỉ inlcude phần cucumber.config.home
*/
    require: ['../e2e/steps/*.js', '../e2e/supports/*.js'],
    tags: tags,
    strict: true,
    profile: false,
    'no-source': true,
    format: [
      'json:' + global.REPORT_FOLDER_PATH + '/summary.json',
      // 'rerun:rerun/@rerun.txt'
    ],
  };

  // Setting to only rerun failed test case 
  // which are marked in @rerun.txt file
  if (isReRun === true) {
    opts['rerun'] = './rerun/@rerun.txt';
  } else {
    opts.format.push('rerun:rerun/@rerun.txt');
  }
  return opts;
}

module.exports = {
  getCucumberOpts,
};

Kế tiếp là tạo file config cho protractor cucumber. Các bạn xem file sample dưới đây:

protractor.cucumber.config.js

const path = require('path');
const { getCapability } = require('./base/browser.config');
const { getCucumberOpts } = require('./cucumber/cucumber.config');
const { Log4jsConfig } = require('./log4js');
const reportConfig = require('./report/reportportal.config.json');
const {
  Report,
  combineRerunFiles,
  extractCommandlineOptions,
  generateReportFolder,
  generateNestedFolder,
  Log4jsReportPortal,
  CucumberReportAgent
} = require('../../lib');

global.__projectRoot = process.cwd();
//Generate rerun folder folder
generateNestedFolder(path.join(__projectRoot, 'rerun'));

//Generate HTML report folder for local report
generateReportFolder();

/**
Khởi tạo cucumber agent, tương tự như jasmine agent ở phần trên
hơi khác 1 xí là do đặc thù của cucumber-js ko có phần 
listener cho reporter mà sử dụng hook
nên mình sẽ khởi tạo trước ở config và exports để sử dụng
trong file hook sau
*/
const cucumberAgent = new CucumberReportAgent(reportConfig);

// Thiết lập cho method Log4JS custom appender
// lý do nhận vô instance agent là do mình cần send 
// những thông tin của test log lên ReportPortal.io 
const log4jsReportPortal = new Log4jsReportPortal(cucumberAgent.agent);

const CURRENT_SESSION_INFO_FILE_NAME = './report/report.config.json';

global.REPORT_INFO = require(CURRENT_SESSION_INFO_FILE_NAME);

const startTime = new Date();

// Parse extra arguments which is sent from command line
// Note: Need to send exactly these option in command line
let { capability, isReRun, tags } = extractCommandlineOptions();

let config = {
  SELENIUM_PROMISE_MANAGER: false,

  seleniumAddress: 'http://localhost:4444/wd/hub',

  multiCapabilities: [getCapability(capability)],

  specs: ['../e2e/features/sample/sample.home.feature'],

  framework: 'custom',

  frameworkPath: require.resolve('protractor-cucumber-framework'),

  cucumberOpts: getCucumberOpts(isReRun, tags),
  log4jsInstance: () => {
    return global.log4jsInstance;
  },
  beforeLaunch: function() {
// thiết lập log4js khi run với cucumber
// send log info vô ReportPorta.io
    global.log4jsInstance = Log4jsConfig(
      path.join(__dirname, 'protractor.cucumber.config.js')
    );
  },

  afterLaunch: async function() {
    // Merge all rerun file
    if (!isReRun) {
      combineRerunFiles(path.join(__projectRoot, 'rerun'));
    }

    // Generate html report
    const completeTime = new Date();

    global.REPORT_INFO.startTime = startTime.toUTCString();
    global.REPORT_INFO.completeTime = completeTime.toUTCString();

    await Report.generate();
  }
};

/**
2 lệnh export ở dưới thì có mục đích giống như config
khi sử dụng với jasmine
*/
exports.configure = log4jsReportPortal.configure;
exports.config = config;

/**
lệnh export này dùng để export cucumber agent 
nhằm mục đích sẽ sử dụng trong phần hook của cucumber
cơ chế của cucumber sẽ hỗ trợ những hook( gần giống như
test listener nhưng nó họat động giống mô hình AOP - Aspect-oriented programming hơn)
*/
exports.cucumberAgent = cucumberAgent;

Kế tiếp là thiết lập 1 file hook.js nhằm mục đích để send thông tin lên ReportPortal.io khi cucumber run test script (bạn nào đang làm cucumber thì có thể coi link này để hiểu kỹ hơn). 

hook.js

'use strict';
const { setDefaultTimeout } = require('cucumber');
// Import lại cucumber agent mình đã khởi tạo ở config
const { cucumberAgent } = require('../../config/protractor.cucumber.config');
// Bọn mình đã build sẵn CucumberHook cho phép
// send thông tin lên ReportPortal.io
// các bạn chỉ cần import lại method đó
const {
  CucumberReportHook
} = require('../../../lib/report-portal/cucumber-js');
const DEFAULT_TIMEOUT = 60000;

// Include cucumber hook cho ReportPortal.io
// method nhận vào report agent mình đã khởi tạo ở 
// bên phần config
// Method này bọn mình cho phép truyền vô thêm 2 params nữa
// param kế tiếp là tên suite name
// và sau cùng là tên suite description 
CucumberReportHook(cucumberAgent,'Demo sanity suite', 'this is sample sanity suite description');
setDefaultTimeout(DEFAULT_TIMEOUT);

Giờ mình cùng xem thử thành quả với Cucumber-JS agent nào


Chốt sổ  

Hy vọng bài viết này sẽ giúp các bạn có thể sử dụng ReportPortal.io khi dùng Protractor hoặc Protractor-Cucumber-JS. Và hy vọng bài viết này sẽ giúp ích được gì đó cho các bạn =))

Source Code của bọn mình để ở đây.  Bạn nào muốn lấy về custom lại hay ngâm cứu thì cứ thoải mái nhé :D. Và cuối cùng cảm ơn các bạn đã bỏ thờ gian đọc cái bài dài nhất mình từng viết =))

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s