NỘI DUNG BÀI HỌC

✅ CLASS TRONG JAVASCRIPT
✅ CÁC TRỤ CỘT CỦA OOP (Kế thừa, Đóng gói, Đa hình...)

Sau khi đã làm chủ các khái niệm nền tảng như biến, hàm và vòng lặp, đã đến lúc chúng ta tìm hiểu về một phương pháp tổ chức code hoàn toàn mới và cực kỳ mạnh mẽ: Lập trình Hướng đối tượng (Object-Oriented Programming - OOP).

Đây là một bước nhảy vọt về tư duy, giúp bạn xây dựng các kịch bản test có cấu trúc, dễ bảo trì và dễ mở rộng như các lập trình viên chuyên nghiệp.

Vấn đề cần giải quyết: "Code Mỳ Ý"

Khi các kịch bản test của bạn ngày càng lớn, nếu chỉ viết các hàm và biến rời rạc, code của bạn sẽ trở nên lộn xộn, khó hiểu và khó sửa chữa, giống như một "đĩa mỳ Ý" (Spaghetti Code).

OOP ra đời để giải quyết vấn đề này.

  • Ví von: 🧠 Thay vì nấu một nồi mỳ Ý rối rắm, OOP giúp bạn tạo ra những viên gạch LEGO có thể tái sử dụng.
    • Mỗi viên gạch (Object) có những đặc điểm riêng (Properties - thuộc tính) và những cách lắp ráp riêng (Methods - phương thức).
    • Bạn sẽ lắp ráp những viên gạch này lại với nhau để xây dựng nên chương trình của mình một cách có tổ chức.
    •  

Phần 1: Khái niệm Cốt lõi - Lớp (Class) và Đối tượng (Object)

Đây là hai khái niệm quan trọng nhất bạn cần phân biệt.

  • Lớp (Class): Bản thiết kế 📐
    • Class là một "bản thiết kế" hoặc một "khuôn mẫu" để tạo ra các đối tượng. Nó định nghĩa tất cả các thuộc tính và phương thức mà một đối tượng thuộc lớp đó sẽ có.
    • Ví dụ: Bản thiết kế của một chiếc xe hơi sẽ định nghĩa rằng xe hơi phải có màu sắc, số bánh xe và có các hành động như chạy(), phanh().
  • Đối tượng (Object/Instance): Ngôi nhà thực tế 🏠
    • Object là một "thể hiện" (instance) cụ thể được tạo ra từ bản thiết kế Class.
    • Ví dụ: Từ bản thiết kế xe hơi, bạn có thể tạo ra xeCuaToi (màu đỏ) và xeCuaBan (màu xanh). Cả hai đều là những đối tượng xe hơi, tuân theo cùng một bản thiết kế nhưng là hai thực thể hoàn toàn độc lập.
    •  

Phần 2: Cú pháp class trong JavaScript

JavaScript sử dụng từ khóa class để tạo ra "bản thiết kế".

A. constructor() - Hàm khởi tạo

  • Đây là một phương thức đặc biệt được tự động gọi mỗi khi bạn tạo một đối tượng mới từ class (dùng từ khóa new).
  • Nhiệm vụ của nó là khởi tạo các giá trị ban đầu cho các thuộc tính của đối tượng.
  • Bên trong constructor, từ khóa this tham chiếu đến chính đối tượng mới đang được tạo ra.

B. Methods - Phương thức

  • Đây là các hàm được định nghĩa bên trong class, đại diện cho các hành động mà đối tượng có thể thực hiện.
  • Ví dụ cơ bản:

// Tạo "bản thiết kế" cho User
class TestUser {
  // 1. Hàm khởi tạo, nhận vào các giá trị ban đầu
  constructor(username, role) {
    // Gán các giá trị vào thuộc tính của chính đối tượng này (this)
    this.username = username;
    this.role = role;
    this.isLoggedIn = false; // Thuộc tính mặc định
  }

  // 2. Các phương thức (hành động)
  login() {
    this.isLoggedIn = true;
    console.log(`User '${this.username}' đã đăng nhập.`);
  }

  logout() {
    this.isLoggedIn = false;
    console.log(`User '${this.username}' đã đăng xuất.`);
  }
}

// 3. Tạo ra các "đối tượng" thực tế từ bản thiết kế
const adminUser = new TestUser("admin_01", "Admin");
const viewerUser = new TestUser("viewer_02", "Viewer");

// 4. Gọi các phương thức của từng đối tượng
adminUser.login(); // User 'admin_01' đã đăng nhập.
viewerUser.login(); // User 'viewer_02' đã đăng nhập.

console.log(adminUser.isLoggedIn); // true
console.log(viewerUser.role);      // "Viewer"​

 

Phần 3: So sánh: Khi nào dùng class và khi nào dùng Object đơn lẻ? 🆚

Về bản chất, phương thức trong cả hai trường hợp đều là một hàm được gán cho thuộc tính của đối tượng. Sự khác biệt nằm ở mục đíchkhả năng tái sử dụng.

Ngôi Nhà Đơn Lẻ (Object Literal)

Đây là cách bạn tạo ra một đối tượng duy nhất, có một không hai. Bạn định nghĩa các thuộc tính và phương thức của nó ngay lập tức.

  • Ví von: 🧠 Giống như bạn xây một "căn nhà độc nhất vô nhị". Bản thiết kế và ngôi nhà là một, và chỉ có một căn nhà như vậy.

const userA = {
  name: "An",
  greet: function() {
    console.log(`Xin chào, tôi là ${this.name}`);
  }
};
userA.greet();
  • Nhược điểm: Nếu muốn tạo userB có cùng hành động greet(), bạn phải copy-paste lại toàn bộ code, dẫn đến trùng lặp và khó bảo trì.

 

Nhà Máy Sản Xuất Nhà (Class)

Đây là cách bạn tạo ra một "bản thiết kế" (Class) để có thể sản xuất hàng loạt các đối tượng có cùng cấu trúc và hành động.

  • Ví von: 🧠 Giống như bạn tạo ra một "bản thiết kế nhà". Từ bản thiết kế này, bạn có thể xây bao nhiêu ngôi nhà (object) giống hệt nhau cũng được.


class User {
  constructor(name) {
    this.name = name;
  }
  // Phương thức `greet` được định nghĩa MỘT LẦN trong bản thiết kế
  greet() {
    console.log(`Xin chào, tôi là ${this.name}`);
  }
}

// Tạo ra hai đối tượng từ cùng một bản thiết kế
const user1 = new User("An");
const user2 = new User("Bình");

user1.greet(); // Xin chào, tôi là An
user2.greet(); // Xin chào, tôi là Bình​
  • Ưu điểm: Phương thức greet() chỉ được định nghĩa một lần. Tất cả các đối tượng tạo ra đều "dùng chung" bản thiết kế đó. Điều này giúp code gọn gàng, dễ bảo trì và có khả năng tái sử dụng cao. Nếu cần sửa đổi hành động greet(), bạn chỉ cần sửa ở một nơi duy nhất là trong class.


Bảng tóm tắt


Tiêu chí

Phương thức trong Object Literal

Phương thức trong Class

Bản chất

Hàm là thuộc tính của object.

Hàm là thuộc tính của object.

Mục đích

Định nghĩa hành động cho một đối tượng duy nhất.

Định nghĩa hành động cho tất cả các đối tượng được tạo từ lớp.

Tái sử dụng

Thấp. Phải copy-paste.

Cao. Định nghĩa một lần, dùng nhiều lần.

Ví von

Ngôi nhà đơn lẻ.

Bản thiết kế nhà.


Phần 4: CÁC TRỤ CỘT CỦA OOP

Sức mạnh thực sự của Lập trình Hướng đối tượng (OOP) nằm ở 4 trụ cột cốt lõi: Tính đóng gói, Kế thừa, Đa hình, và Trừu tượng.

Nếu class là gạch và vữa, thì 4 trụ cột này là nền móng, tường, và mái nhà giúp bạn xây nên một công trình phần mềm vững chắc và có cấu trúc.

  1. Tính Kế thừa (Inheritance)
  2. Tính Đóng gói (Encapsulation)
  3. Tính Trừu tượng (Abstraction)
  4. Tính Đa hình (Polymorphism)

1. Tính Kế thừa (Inheritance) - Mối quan hệ "Cha - Con"

  • Định nghĩa: Kế thừa cho phép một class con (subclass) có thể thừa hưởng lại các thuộc tính và phương thức của một class cha (superclass).
  • Ví von: 🧠 Hãy nghĩ về các loại Phương tiện giao thông.
    • Ta có một lớp cha là PhuongTien với các đặc điểm chung như tocDo, dongCo và hành động khoiDong().
    • Các lớp con như XeHoi và XeMay sẽ kế thừa tất cả những đặc điểm đó mà không cần định nghĩa lại. Ngoài ra, chúng có thể có thêm các đặc điểm riêng (XeHoi có soCua, XeMay có gioiHanPhanKhoi).
  • Cú pháp: Dùng từ khóa extends và hàm super().
    • extends: Để khai báo lớp con.
    • super(): Được gọi trong constructor của lớp con để gọi đến constructor của lớp cha.

Ứng dụng trong Automation: Xây dựng BasePage

Đây là một kỹ thuật cực kỳ phổ biến trong mô hình POM. Hầu hết các trang trong ứng dụng của bạn đều có chung phần header (logo, thanh tìm kiếm, nút đăng xuất). Thay vì lặp lại code, chúng ta tạo một BasePage (trang cơ sở).


// LỚP CHA: Chứa các thành phần và hành động chung
class BasePage {
  constructor(page) {
    this.page = page;
    this.logoutButton = page.locator('#logout-button');
    this.searchBar = page.locator('#search-bar');
  }

  async logout() {
    await this.logoutButton.click();
    console.log("LOG: Đã thực hiện đăng xuất.");
  }
}

// LỚP CON: Kế thừa từ BasePage và có thêm các thành phần riêng
class HomePage extends BasePage {
  constructor(page) {
    super(page); // Gọi constructor của lớp cha để khởi tạo page và các locator chung
    // Định nghĩa thêm các locator riêng của trang chủ
    this.heroBanner = page.locator('.hero-banner');
  }

  async clickHeroBanner() {
    await this.heroBanner.click();
  }
}

// Sử dụng trong test
// const homePage = new HomePage(page);
// await homePage.clickHeroBanner(); // Phương thức riêng của HomePage
// await homePage.logout();         // Phương thức được kế thừa từ BasePage!​

2. Tính Đóng gói (Encapsulation) & Trừu tượng (Abstraction) - Che giấu sự phức tạp

  • Định nghĩa:
    • Đóng gói: "Gói" dữ liệu (thuộc tính) và các hành động xử lý dữ liệu đó (phương thức) vào chung một đơn vị (class). Quan trọng là nó cho phép che giấu những thông tin nội bộ không cần thiết.
    • Trừu tượng: Chỉ đưa ra các giao diện đơn giản để tương tác, ẩn đi toàn bộ sự phức tạp bên trong.
  • Ví von: 🧠 Hãy nghĩ về một cái điều khiển TV.
    • Bạn chỉ cần biết các nút đơn giản như "Tăng âm lượng", "Chuyển kênh" (đây là giao diện trừu tượng).
    • Bạn không cần biết (và không nên can thiệp vào) các vi mạch, đèn hồng ngoại phức tạp bên trong (đây là các chi tiết được đóng gói và che giấu).
  • Cú pháp: Trong JavaScript hiện đại, ta dùng dấu # trước tên thuộc tính hoặc phương thức để biến nó thành private (chỉ có thể truy cập từ bên trong class).

Ứng dụng trong Automation: Tạo một API Client

Khi viết test API, bạn không muốn các file test phải quan tâm đến việc lấy token, đặt header... Bạn chỉ muốn gọi một hàm đơn giản như getProducts().


class ApiClient {
  // #authToken là thuộc tính private, không thể truy cập từ bên ngoài
  #authToken;

  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  // #authenticate là phương thức private
  async #authenticate() {
    console.log("LOG: Đang lấy token xác thực...");
    this.#authToken = "SECRET_TOKEN_123"; // Giả lập việc lấy token
  }

  // getProducts là phương thức public, là "giao diện" đơn giản cho người dùng
  async getProducts() {
    await this.#authenticate(); // Tự động gọi phương thức private bên trong
    console.log(`LOG: Gửi yêu cầu đến ${this.baseURL}/products với token ${this.#authToken}`);
    // ... code fetch API ...
    return []; // Trả về danh sách sản phẩm
  }
}

// Sử dụng trong test
// const apiClient = new ApiClient("https://api.example.com");
// await apiClient.getProducts(); // Người dùng chỉ cần gọi hàm này
// apiClient.#authenticate(); // LỖI! không thể gọi phương thức private từ bên ngoài​


3. Tính Đa hình (Polymorphism) - Một hành động, nhiều hình thái

  • Định nghĩa: "Poly" nghĩa là "nhiều", "morph" nghĩa là "hình thái". Đa hình là khả năng các đối tượng khác nhau có thể phản hồi lại cùng một lời gọi phương thức theo cách riêng của chúng.
  • Ví von: 🧠 Hành động "nói". Nếu bạn ra lệnh cho các đối tượng khác nhau noi(), ConCho sẽ "gâu gâu", ConMeo sẽ "meo meo", và ConVit sẽ "quạc quạc". Cùng một hành động, nhưng kết quả thực thi khác nhau.
  • Cú pháp: Thường được thể hiện qua việc ghi đè phương thức (Method Overriding), trong đó lớp con định nghĩa lại một phương thức đã có ở lớp cha.

Ứng dụng trong Automation: Xử lý các loại Element khác nhau

Bạn có nhiều loại trường nhập liệu, nhưng bạn muốn có một hành động chung là fill().


class BaseField {
  constructor(locator) {
    this.locator = locator;
  }
  async fill(value) {
    console.log("Hành động fill mặc định...");
  }
}

class TextField extends BaseField {
  // Không cần ghi đè, sẽ dùng phương thức fill của cha
  async fill(value) {
    console.log(`- (TextField) Đang điền text '${value}'`);
    // await this.locator.fill(value);
  }
}

class DropdownField extends BaseField {
  // Lớp con GHI ĐÈ phương thức fill của lớp cha
  async fill(value) {
    console.log(`- (Dropdown) Đang chọn giá trị '${value}'`);
    // await this.locator.selectOption(value);
  }
}

// Sử dụng trong test
// const usernameField = new TextField(page.locator('#username'));
// const countryDropdown = new DropdownField(page.locator('#country'));
//
// await usernameField.fill("my_username"); // Sẽ in ra log của TextField
// await countryDropdown.fill("Vietnam");   // Sẽ in ra log của DropdownField
​

 

Ví dụ về Tính Kế thừa (Inheritance)

Kịch bản: Trong automation, chúng ta thường có nhiều loại người dùng với các quyền hạn khác nhau. Chúng ta có thể tạo một class người dùng cơ bản và một class quản trị viên kế thừa từ đó.

 

// Lớp cha: Định nghĩa các thuộc tính và phương thức chung
class BaseUser {
  constructor(username) {
    this.username = username;
  }

  login() {
    console.log(`User '${this.username}' đã đăng nhập.`);
  }

  logout() {
    console.log(`User '${this.username}' đã đăng xuất.`);
  }
}

// Lớp con: Kế thừa từ BaseUser và có thêm phương thức riêng
class AdminUser extends BaseUser {
  // Không cần constructor vì không có thuộc tính mới, nó sẽ tự động dùng constructor của cha.

  // Thêm một phương thức riêng chỉ Admin mới có
  deleteUser(targetUsername) {
    console.log(`Admin '${this.username}' đang xóa người dùng '${targetUsername}'.`);
  }
}

// --- Sử dụng ---
const guest = new BaseUser("guest123");
const admin = new AdminUser("super_admin");

console.log("--- Thử với Guest User ---");
guest.login(); // OK, phương thức của chính nó

console.log("\n--- Thử với Admin User ---");
admin.login(); // OK, phương thức được kế thừa từ BaseUser
admin.deleteUser("guest123"); // OK, phương thức riêng của AdminUser

// Dòng này sẽ gây lỗi nếu được chạy, vì BaseUser không có phương thức này.
// guest.deleteUser("test"); ​


Lớp con (AdminUser) có thể sử dụng phương thức của lớp cha (.login()) và đồng thời có phương thức của riêng mình (.deleteUser()).


Ví dụ về Tính Đóng gói (Encapsulation)

Kịch bản: Bạn cần tạo một class TestTimer để đo thời gian chạy của một test case. Để đảm bảo tính toàn vẹn, không ai có thể can thiệp vào thời gian bắt đầu và kết thúc một cách trực tiếp từ bên ngoài.


class TestTimer {
  // #startTime và #endTime là các thuộc tính private, được che giấu
  #startTime;
  #endTime;

  constructor() {
    this.#startTime = 0;
    this.#endTime = 0;
  }

  // Phương thức public - giao diện để tương tác
  start() {
    this.#startTime = Date.now(); // Lấy thời gian hiện tại (miligiây)
    console.log("LOG: Bắt đầu bấm giờ.");
  }

  stop() {
    this.#endTime = Date.now();
    console.log("LOG: Dừng bấm giờ.");
  }

  getDuration() {
    if (this.#startTime === 0 || this.#endTime === 0) {
      return "Lỗi: Cần start() và stop() trước khi lấy thời gian.";
    }
    const duration = (this.#endTime - this.#startTime) / 1000; // Đổi sang giây
    return `Thời gian thực thi test: ${duration.toFixed(2)} giây.`;
  }
}

// --- Sử dụng ---
const loginTestTimer = new TestTimer();

loginTestTimer.start();

// Giả lập thời gian test đang chạy...

loginTestTimer.stop();

console.log(loginTestTimer.getDuration());

// Dòng này sẽ gây lỗi vì bạn không thể truy cập thuộc tính private từ bên ngoài.
// loginTestTimer.#startTime = 123; ​

Các thuộc tính private (#startTime, #endTime) được bảo vệ và chỉ có thể được thao tác thông qua các phương thức public (start, stop, getDuration).

Ví dụ về Tính Đa hình (Polymorphism)

Kịch bản: Trong kịch bản test, bạn cần xác thực nhiều loại element khác nhau. Hành động chung là "xác thực" (verify), nhưng cách xác thực cho từng loại lại khác nhau.

// Lớp cha định nghĩa một giao diện chung
class BaseElement {
  constructor(name) {
    this.name = name;
  }

  // Phương thức chung
  verify() {
    console.log(`Thực hiện kiểm tra chung cho '${this.name}'...`);
  }
}

// Lớp con ghi đè (override) phương thức của cha
class ButtonElement extends BaseElement {
  verify() {
    // Cách xác thực riêng của Button
    console.log(`✅ (Button) Đang kiểm tra xem nút '${this.name}' CÓ HIỂN THỊ không...`);
  }
}

// Lớp con khác cũng ghi đè phương thức của cha
class InputElement extends BaseElement {
  constructor(name, expectedValue) {
    super(name);
    this.expectedValue = expectedValue;
  }

  verify() {
    // Cách xác thực riêng của Input
    console.log(`✅ (Input) Đang kiểm tra xem ô '${this.name}' CÓ GIÁ TRỊ là '${this.expectedValue}' không...`);
  }
}

// --- Sử dụng ---
const loginButton = new ButtonElement("Login");
const usernameInput = new InputElement("Username", "tomsmith");

// Tạo một danh sách các element cần xác thực
const elementsToVerify = [loginButton, usernameInput];

console.log("--- Bắt đầu xác thực hàng loạt ---");
// Cùng gọi phương thức .verify() trên các đối tượng khác nhau
elementsToVerify.forEach(element => element.verify());​

 

Mặc dù chúng ta cùng gọi một tên phương thức là element.verify(), nhưng kết quả thực thi lại khác nhau tùy thuộc vào đối tượng đó là ButtonElement hay InputElement. Đây chính là tính đa hình.

Teacher

Teacher

Nguyên Hoàng

Automation Engineer

With 7+ years of hands-on experience across multiple languages and frameworks. I'm here to share knowledge, helping you turn complex processes into simple and effective solutions.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học