NỘI DUNG BÀI HỌC

✅ Nắm vững 4 tính chất cốt lõi của Lập trình Hướng đối tượng: Đóng gói, Kế thừa, Đa hình, và Trừu tượng.
✅ Hiểu cách che giấu dữ liệu (Encapsulation) để tạo ra các component an toàn và đáng tin cậy.
✅ Tận dụng tính kế thừa (Inheritance) để loại bỏ code lặp lại và xây dựng các Page Object có cấu trúc.
✅ Vận dụng tính đa hình (Polymorphism) để viết code linh hoạt, có khả năng xử lý nhiều dạng đối tượng khác nhau.
✅ Thiết kế các lớp trừu tượng (Abstraction) để định hình cấu trúc cho framework.
✅ Phân biệt và sử dụng thành thạo @staticmethod@classmethod.

"Nếu không có OOP, khi framework lớn dần thì điều gì xảy ra?"

👉 Dẫn đến code rối, khó bảo trì, nhiều duplicate, không tái sử dụng được.

1. Bốn trụ cột của OOP - Nâng tầm tư duy xây dựng Framework

 

Ở buổi trước, chúng ta đã học về Class và Object - những viên gạch đầu tiên. Hôm nay, chúng ta sẽ học cách xây dựng một "tòa nhà" vững chắc bằng 4 trụ cột chính của OOP.

Hãy tưởng tượng bạn đang xây dựng một nhà máy sản xuất ô tô (Framework Automation):

  1. Tính Đóng gói (Encapsulation): Mỗi bộ phận của xe (động cơ, hộp số) là một "hộp đen". Người lái chỉ dùng vô lăng, chân ga (giao diện công khai), không cần biết chi tiết bên trong hoạt động ra sao. Điều này giúp xe hoạt động an toàn, không bị can thiệp sai cách.

  2. Tính Kế thừa (Inheritance): Từ một bản thiết kế "Xe" cơ bản, nhà máy có thể tạo ra các dòng xe khác nhau như "Xe tải", "Xe thể thao". "Xe tải" kế thừa mọi thứ từ "Xe" (bánh xe, động cơ) và có thêm "thùng chở hàng". Giúp tái sử dụng thiết kế tối đa.

  3. Tính Đa hình (Polymorphism): Cùng một hành động "Lái xe", nhưng cách lái "Xe tải" sẽ khác với lái "Xe đua". Framework của bạn phải đủ linh hoạt để xử lý các đối tượng khác nhau nhưng có chung một hành động.

  4. Tính Trừu tượng (Abstraction): Bản thiết kế ban đầu chỉ đưa ra các khái niệm chung: "Xe phải có bánh xe", "Xe phải có khả năng di chuyển" mà không cần chỉ rõ chi tiết. Điều này định hình nên cấu trúc chung cho tất cả các loại xe sẽ được sản xuất sau này.

Bốn tính chất này là kim chỉ nam giúp bạn xây dựng một framework automation không chỉ chạy được, mà còn dễ bảo trì, dễ mở rộng và dễ dàng cho người khác cùng hợp tác.

 

2. Tính Đóng gói (Encapsulation) - "Hộp đen" an toàn ⬛

 

Là cơ chế che giấu thông tin và trạng thái bên trong của một đối tượng, và chỉ cho phép tương tác với nó thông qua các phương thức công khai (public methods).

🔹 Ví dụ trong cuộc sống: Điều khiển TV. Bạn chỉ cần bấm nút volumeUp() trên remote. Bạn không cần (và không nên) mở TV ra để chỉnh con chip xử lý âm thanh. Remote chính là giao diện công khai, giúp bảo vệ các chi tiết phức tạp bên trong TV.

Hoặc với 1 trường hợp khác: ATM → bạn chỉ cần nhập số PIN và rút tiền, không cần biết ngân hàng vận hành ra sao.

Trong Python, việc đóng gói được thể hiện qua quy ước đặt tên:

  • self.name: Public - Có thể truy cập tự do từ bên ngoài.

  • self._name: Protected - Ngụ ý rằng đây là thuộc tính nội bộ, không nên truy cập từ bên ngoài, nhưng vẫn có thể.

  • self.__name: Private - Rất khó truy cập từ bên ngoài do cơ chế "name mangling" của Python.

🔹 Ứng dụng trong Automation: Tạo một lớp quản lý cấu hình test, đảm bảo không ai có thể vô tình thay đổi các cấu hình quan trọng.

class TestConfig:
    def __init__(self, browser, base_url):
        self.__browser = browser
        self.__base_url = base_url
        self.__timeout = 30 # Cấu hình private, không muốn bị thay đổi từ bên ngoài

    # Phương thức public để lấy thông tin một cách an toàn
    def get_browser(self):
        return self.__browser

    def get_base_url(self):
        return self.__base_url
        
    def get_timeout(self):
        return self.__timeout

# --- Trong file test case ---
config = TestConfig("Chrome", "https://my-app.com")

# Lấy thông tin qua phương thức public -> An toàn
print(f"Trình duyệt: {config.get_browser()}")

# Cố gắng thay đổi trực tiếp thuộc tính private -> Rất khó và không nên làm
# config.__timeout = 10 # Sẽ không hoạt động như mong đợi
print(f"Timeout: {config.get_timeout()}") # Vẫn sẽ là 30
class User:
    def __init__(self, username, password):
        self.__username = username   # private
        self.__password = password   # private

    def get_username(self):
        return self.__username

    def set_password(self, new_password):
        self.__password = new_password

# Encapsulation bảo vệ dữ liệu login không bị sửa trực tiếp
# 👉 Dùng trong Page Object: bảo vệ locator hoặc data test, chỉ expose method login() thay vì để QA gọi locator trực tiếp.


✍️ Bài tập thực hành phần 2 Mở rộng class BankAccount ở buổi trước. Chuyển self.balance thành self.__balance (private). Đảm bảo rằng việc gửi và rút tiền chỉ có thể thực hiện thông qua các phương thức deposit()withdraw(). Trong withdraw(), thêm logic kiểm tra nếu số tiền rút lớn hơn số dư thì raise ValueError("Số dư không đủ!").

 

3. Tính Kế thừa (Inheritance) - "Cha truyền con nối" 👨‍👧‍👦

 

Cho phép một lớp (lớp con - child class) thừa hưởng toàn bộ thuộc tính và phương thức của một lớp khác (lớp cha - parent class). Điều này giúp tái sử dụng code và tạo ra một hệ thống phân cấp logic.

🔹 Ví dụ trong cuộc sống: Lớp cha là Employee (Nhân viên) có các thuộc tính name, id và phương thức calculate_salary(). Các lớp con như Manager (Quản lý) và Developer (Lập trình viên) sẽ kế thừa từ Employee. Lớp Manager có thể có thêm thuộc tính bonus, còn lớp Developer có thể có thêm phương thức commit_code().

 

🔹 Ứng dụng trong Automation: Đây là kỹ thuật nền tảng để xây dựng framework. Chúng ta tạo ra một BasePage chứa tất cả các hành động chung mà mọi trang đều có.

# file: base/base_page.py
class BasePage:
    def __init__(self, driver):
        self.driver = driver

    def get_title(self):
        return self.driver.title

    def click_element(self, locator):
        # (Thêm logic wait tường minh ở đây)
        self.driver.find_element(*locator).click()
        print(f"Đã click vào element có locator: {locator}")

# file: pages/home_page.py
from base.base_page import BasePage
from selenium.webdriver.common.by import By

# HomePage kế thừa từ BasePage
class HomePage(BasePage):
    def __init__(self, driver):
        super().__init__(driver) # Gọi hàm khởi tạo của lớp cha
        self.login_button_locator = (By.ID, "login-btn")
    
    def go_to_login(self):
        # HomePage không cần định nghĩa lại click_element
        # mà được "thừa hưởng" từ BasePage
        self.click_element(self.login_button_locator)

# --- Trong file test case ---
# home = HomePage(driver)
# print(f"Tiêu đề trang: {home.get_title()}") # Gọi phương thức của lớp cha
# home.go_to_login() # Gọi phương thức của lớp con​

 

4. Tính Đa hình (Polymorphism) - "Một tên, nhiều dạng" 🎭

 

"Poly" nghĩa là nhiều, "morph" là hình dạng. Đa hình là khả năng các đối tượng thuộc các lớp 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. Nó thường đi đôi với Kế thừa.

🔹 Ví dụ trong cuộc sống: Bạn có các con vật: Chó, Mèo, Bò. Tất cả đều có hành động speak() (kêu). Nhưng khi bạn gọi dog.speak() nó sẽ ra "Gâu gâu", cat.speak() sẽ ra "Meo meo", và cow.speak() sẽ ra "Um bò...". Cùng một tên phương thức, nhưng kết quả khác nhau tùy vào đối tượng.

🔹 Ứng dụng trong Automation: Xử lý các loại thông báo khác nhau trên trang web.

class BaseNotification:
    def __init__(self, driver, locator):
        self.driver = driver
        self.locator = locator
    
    def get_message(self):
        # Lớp cha chưa biết cách lấy message cụ thể, để lớp con định nghĩa
        raise NotImplementedError

class SuccessBanner(BaseNotification):
    # Lớp này kế thừa và định nghĩa lại (override) phương thức get_message
    def get_message(self):
        element = self.driver.find_element(*self.locator)
        return element.text # Lấy text từ banner màu xanh

class ErrorPopup(BaseNotification):
    # Lớp này cũng override phương thức get_message
    def get_message(self):
        # Logic tìm popup và lấy message từ popup màu đỏ
        popup_body = self.driver.find_element(*self.locator)
        return popup_body.find_element(By.CLASS_NAME, "popup-message").text

# --- Hàm kiểm thử có tính đa hình ---
def verify_notification_text(notification: BaseNotification, expected_text: str):
    # Hàm này không cần biết đó là SuccessBanner hay ErrorPopup
    # Nó chỉ cần biết đối tượng có phương thức get_message()
    message = notification.get_message()
    assert message == expected_text

# --- Cách dùng trong test case ---
# success_banner = SuccessBanner(driver, (By.ID, "success-banner"))
# verify_notification_text(success_banner, "Thêm vào giỏ hàng thành công!")

 

class Shape:
    def draw(self):
        pass

class Circle(Shape):
    def draw(self):
        print("Drawing Circle")

class Square(Shape):
    def draw(self):
        print("Drawing Square")

# Polymorphism trong test
for s in [Circle(), Square()]:
    s.draw()


5. Tính Trừu tượng (Abstraction) - "Tập trung vào ý chính" ✍️

 

Là việc che giấu các chi tiết triển khai phức tạp và chỉ hiển thị các chức năng cần thiết cho người dùng. Nó giúp định hình một "bộ khung" mà các lớp khác phải tuân theo.

Trong Python, tính trừu tượng thường được triển khai bằng Abstract Base Classes (ABC). Một lớp trừu tượng có thể chứa các phương thức trừu tượng - những phương thức chỉ có tên chứ không có code implement. Bất kỳ lớp con nào kế thừa từ lớp trừu tượng này BẮT BUỘC phải triển khai tất cả các phương thức trừu tượng đó.

🔹 Ví dụ trong cuộc sống: Khi bạn thiết kế một chiếc ô tô, bạn đưa ra một bản thiết kế trừu tượng: "Ô tô BẮT BUỘC phải có chức năng start_engine()brake()". Còn việc start_engine() bằng cách vặn chìa khóa hay bấm nút, brake() bằng phanh đĩa hay phanh tang trống... thì tùy vào từng loại xe cụ thể (lớp con) tự triển khai.

🔹 Ứng dụng trong Automation: Định nghĩa một cấu trúc chuẩn cho việc đọc dữ liệu kiểm thử từ các nguồn khác nhau.

 
from abc import ABC, abstractmethod

# Lớp trừu tượng, định nghĩa "khung"
class BaseDataSource(ABC):
    @abstractmethod
    def get_data(self, test_name):
        pass

# Lớp cụ thể triển khai theo "khung"
class ExcelDataSource(BaseDataSource):
    def __init__(self, file_path):
        self.file_path = file_path
    
    def get_data(self, test_name):
        print(f"Đang đọc dữ liệu cho test '{test_name}' từ file Excel: {self.file_path}")
        # ... logic đọc excel ...
        return {"username": "excel_user", "password": "123"}

# Lớp cụ thể khác
class JsonDataSource(BaseDataSource):
    def __init__(self, file_path):
        self.file_path = file_path
        
    def get_data(self, test_name):
        print(f"Đang đọc dữ liệu cho test '{test_name}' từ file JSON: {self.file_path}")
        # ... logic đọc json ...
        return {"username": "json_user", "password": "456"}

# --- Trong framework ---
# data_source = ExcelDataSource("data.xlsx")
# test_data = data_source.get_data("TC_Login_Success")
# print(test_data)


6. Static & Class Methods - "Công cụ bổ trợ" 🛠️

Ngoài các phương thức thông thường (instance methods) cần self, Python còn có:

@staticmethod:

  • phương thức thuộc về class, không cần truy cập đến instance (self) hay class (cls).
  • Dùng khi hành động không phụ thuộc vào dữ liệu của object hoặc class, chỉ xử lý logic độc lập.
  • Không thể truy cập hoặc thay đổi trạng thái của object hoặc class.

💡 Cú pháp:

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

Gọi phương thức:

result = MathUtils.add(3, 5)
print(result)  # 8



⚙️ Ứng dụng trong Automation Testing:

✅ Khi bạn có các hàm tiện ích (utility) không phụ thuộc vào bất kỳ state nào của lớp.

Ví dụ trong Playwright:

class StringUtils:
    @staticmethod
    def normalize_text(text):
        return text.strip().lower()

Sử dụng trong test:

from utils.string_utils import StringUtils

def test_search_functionality(page):
    page.goto("https://example.com")
    result = page.locator("#result").text_content()
    assert StringUtils.normalize_text(result) == "success"

👉 Giải thích:

  • normalize_text() chỉ xử lý chuỗi, không cần biết class đang ở đâu hay có thuộc tính gì.

  • Đây là ví dụ điển hình của @staticmethod – tiện ích độc lập, dùng chung toàn framework.


@classmethod:  Phương thức lớp (Class Method)

🎯 Khái niệm:

  • Dùng decorator @classmethod.

  • Thay vì truyền self (instance), nó truyền cls (class).

  • Cho phép làm việc với dữ liệu hoặc cấu hình chung của class, hoặc tạo object theo logic riêng (factory pattern).

💡 Cú pháp:

class BrowserConfig:
    browser_name = "chromium"

    @classmethod
    def set_browser(cls, name):
        cls.browser_name = name

    @classmethod
    def get_browser(cls):
        return cls.browser_name

Sử dụng:

BrowserConfig.set_browser("firefox")
print(BrowserConfig.get_browser())  # firefox


⚙️ Ứng dụng trong Automation Playwright

🧰 Ứng dụng 1 – Quản lý cấu hình driver/browser

from playwright.sync_api import sync_playwright

class BrowserFactory:
    _browser = None

    @classmethod
    def get_browser(cls, browser_type="chromium"):
        if cls._browser is None:
            playwright = sync_playwright().start()
            cls._browser = getattr(playwright, browser_type).launch(headless=True)
        return cls._browser
​

Sử dụng trong test:

def test_login_page():
    browser = BrowserFactory.get_browser("firefox")
    context = browser.new_context()
    page = context.new_page()
    page.goto("https://example.com/login")
    assert page.title() == "Login"

👉 Giải thích:

  • BrowserFactory.get_browser() tạo hoặc lấy lại browser instance dùng chung.

  • Dùng @classmethod để chia sẻ trạng thái giữa các test mà không cần khởi tạo class.

🧰 Ứng dụng 2 – Factory Pattern cho Data Object

class User:
    def __init__(self, name, role):
        self.name = name
        self.role = role

    @classmethod
    def admin(cls):
        return cls("Admin", "Administrator")

    @classmethod
    def guest(cls):
        return cls("Guest", "ReadOnly")

Sử dụng:

admin_user = User.admin()
print(admin_user.role)  # Administrator

👉 Giải thích:

  • Dễ dàng tạo nhiều loại dữ liệu mẫu (user, product, account) mà không cần lặp lại constructor.

  • Rất hữu ích trong Data-Driven Testing hoặc Test Fixtures.

🧩 So sánh nhanh

Đặc điểm @staticmethod @classmethod
Nhận đối số đầu tiên Không có self hoặc cls cls (class)
Truy cập được dữ liệu class ❌ Không ✅ Có
Dùng cho Hàm tiện ích, xử lý độc lập Làm việc với class-level data hoặc factory method
Cách gọi ClassName.method() ClassName.method()
Dùng trong Automation Format dữ liệu, convert text, xử lý file, so sánh chuỗi Quản lý config, tạo đối tượng mẫu, factory pattern


💬 Tóm lại

Decorator Khi nào dùng Ví dụ thực tế
@staticmethod Khi hàm không phụ thuộc class hay object Format text, đọc file, tính toán
@classmethod Khi cần truy cập hoặc cập nhật dữ liệu cấp class Tạo object mẫu, quản lý config, factory cho browser


7. Mini Quiz 🎯

 

  1. Việc che giấu self.__balance và chỉ cho phép truy cập qua deposit() là ví dụ về tính chất nào?

  2. HomePage(BasePage) là ví dụ về tính chất nào?

  3. Cùng gọi animal.speak() nhưng chó và mèo kêu khác nhau là ví dụ về tính chất nào?

  4. Điểm khác biệt chính giữa @staticmethod và một phương thức thông thường là gì?

  5. Để ép buộc các lớp con phải định nghĩa một phương thức nào đó, ta nên dùng kỹ thuật gì?

 

8. Bài tập tổng hợp cuối buổi

📦 Bài 1: Tính Đóng gói (Encapsulation) - Chiếc Điện thoại

Ý tưởng: Một chiếc điện thoại che giấu đi các chi tiết phức tạp bên trong (pin, chip xử lý). Bạn chỉ tương tác với nó qua các nút bấm hoặc màn hình. Bài tập này sẽ mô phỏng việc đó.

Đề bài: Viết một class Phone để quản lý dung lượng pin.

  1. Trong hàm khởi tạo __init__, tạo một thuộc tính private tên là __battery_level và gán giá trị mặc định là 100.

  2. Viết một phương thức public tên là use_app(self, hours):

    • Mỗi giờ sử dụng sẽ làm giảm __battery_level đi 10.

    • Nếu pin xuống dưới 0, hãy gán nó bằng 0.

  3. Viết một phương thức public tên là charge_phone(self, hours):

    • Mỗi giờ sạc sẽ làm tăng __battery_level lên 20.

    • Nếu pin vượt quá 100, hãy gán nó bằng 100.

  4. Viết một phương thức public tên là display_battery(self) để in ra dung lượng pin hiện tại.


👨‍👧‍👦 Bài 2: Tính Kế thừa (Inheritance) - Các loại Tài liệu

Ý tưởng: Mọi tài liệu đều có một "tiêu đề", nhưng mỗi loại tài liệu lại có nội dung khác nhau. "Bài báo" và "Email" sẽ cùng kế thừa đặc điểm chung từ "Tài liệu".

Đề bài:

  1. Tạo một lớp cha Document có:

    • __init__(self, title): Để lưu lại tiêu đề.

    • Phương thức show_info(self): In ra "Tiêu đề: [tên tiêu đề]".

  2. Tạo lớp con Article(Document) kế thừa từ Document:

    • __init__(self, title, author): Gọi __init__ của lớp cha để lưu title, và lưu thêm author.

    • Viết lại (override) phương thức show_info(self) để in ra: "Bài báo: [tên tiêu đề] - Tác giả: [tên tác giả]".

  3. Tạo lớp con Email(Document) kế thừa từ Document:

    • __init__(self, title, sender): Gọi __init__ của lớp cha để lưu title, và lưu thêm sender.

    • Viết lại (override) phương thức show_info(self) để in ra: "Email: [tên tiêu đề] - Người gửi: [tên người gửi]".

Mục tiêu: Hiểu cách lớp con thừa hưởng và mở rộng (hoặc thay đổi) hành vi của lớp cha.

 

🎭 Bài 3: Tính Đa hình (Polymorphism) - Giao hàng

Ý tưởng: Có nhiều phương thức giao hàng khác nhau (Xe máy, Xe tải). Cùng một hành động là calculate_fee (tính phí), nhưng mỗi phương thức lại có cách tính riêng.

Đề bài:

  1. Tạo 3 class riêng biệt: MotorbikeDelivery, GrabDelivery, và TruckDelivery.

  2. Mỗi class đều có một phương thức tên là calculate_fee(self, distance_km):

    • MotorbikeDelivery: Phí là distance_km * 5000.

    • GrabDelivery: Phí là distance_km * 7000.

    • TruckDelivery: Phí là distance_km * 20000.

  3. Viết một hàm bên ngoài các class tên là get_shipping_cost(delivery_method, distance):

    • Hàm này nhận vào một đối tượng phương thức giao hàng và một quãng đường.

    • Bên trong, nó sẽ gọi phương thức calculate_fee của đối tượng đó và in ra kết quả.

Mục tiêu: Thấy được rằng hàm get_shipping_cost có thể hoạt động với bất kỳ đối tượng giao hàng nào, miễn là đối tượng đó có phương thức calculate_fee.

✍️ Bài 4: Tính Trừu tượng (Abstraction) - Thanh toán Online

Ý tưởng: Mọi cổng thanh toán đều bắt buộc phải có chức năng "Thanh toán". Nhưng cách "Momo" thanh toán (quét mã QR) sẽ khác cách "Thẻ tín dụng" thanh toán (nhập số thẻ).

Đề bài:

  1. Sử dụng module abc, tạo một lớp trừu tượng PaymentGateway.

  2. Trong lớp này, định nghĩa một phương thức trừu tượngprocess_payment(self, amount).

  3. Tạo lớp con MomoPayment(PaymentGateway):

    • Triển khai phương thức process_payment(self, amount) để in ra: "Đang xử lý thanh toán Momo số tiền [số tiền]... Vui lòng quét mã QR."

  4. Tạo lớp con CreditCardPayment(PaymentGateway):

    • Triển khai phương thức process_payment(self, amount) để in ra: "Đang xử lý thanh toán thẻ tín dụng số tiền [số tiền]... Vui lòng nhập thông tin thẻ."


BÀI TẬP NÂNG CAO

Bài 1: Xây dựng hệ thống Element

  1. Tạo một lớp trừu tượng BaseElement với:

    • __init__(self, locator) để lưu locator (private).

    • Một phương thức public get_locator() để trả về locator.

    • Một phương thức trừu tượng click().

  2. Tạo lớp con Button(BaseElement) kế thừa từ BaseElement:

    • Triển khai phương thức click() bằng cách in ra: Clicking on a button with locator [locator].

  3. Tạo lớp con Checkbox(BaseElement) kế thừa từ BaseElement:

    • Triển khai phương thức click() bằng cách in ra: Toggling a checkbox with locator [locator].

    • Thêm một phương thức riêng is_selected() in ra: Checking selection status....

Bài 2: Factory tạo WebDriver Tạo một class DriverFactory với:

  • Một phương thức @staticmethod tên là get_driver(browser_name).

  • Bên trong phương thức này, dùng if-elif-else:

    • Nếu browser_name là "chrome", in ra "Initializing Chrome Driver..." và trả về chuỗi "ChromeDriver object".

    • Nếu browser_name là "firefox", in ra "Initializing Firefox Driver..." và trả về chuỗi "FirefoxDriver object".

    • Nếu khác, raise ValueError("Browser không được hỗ trợ").

  • Hãy gọi phương thức này để tạo driver cho cả "chrome" và "edge" (để xem lỗi).

Teacher

Teacher

Hà Lan

QA Automation

With over 5 years of experience in web, API, and mobile test automation, built strong expertise in designing and maintaining automation frameworks across various domains and international projects. Committed to mentoring and knowledge sharing, I provide practical guidance and proven techniques to help aspiring testers develop their skills and succeed in the automation field.

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