NỘI DUNG BÀI HỌC
✅ 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 và @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):
-
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.
-
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.
-
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.
-
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() và 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() và 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:
- Là 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:
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:
Sử dụng trong test:
👉 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ềncls(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
Sử dụng trong test:
👉 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
Sử dụng:
👉 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 |
Có 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 🎯
-
Việc che giấu
self.__balancevà chỉ cho phép truy cập quadeposit()là ví dụ về tính chất nào? -
HomePage(BasePage)là ví dụ về tính chất nào? -
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? -
Điểm khác biệt chính giữa
@staticmethodvà một phương thức thông thường là gì? -
Để é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.
-
Trong hàm khởi tạo
__init__, tạo một thuộc tính private tên là__battery_levelvà gán giá trị mặc định là100. -
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đi10. -
Nếu pin xuống dưới
0, hãy gán nó bằng0.
-
-
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_levellên20. -
Nếu pin vượt quá
100, hãy gán nó bằng100.
-
-
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:
-
Tạo một lớp cha
Documentcó:-
__init__(self, title): Để lưu lại tiêu đề. -
Phương thức
show_info(self): In ra "Tiêu đề: [tên tiêu đề]".
-
-
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ưutitle, và lưu thêmauthor. -
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ả]".
-
-
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ưutitle, và lưu thêmsender. -
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:
-
Tạo 3 class riêng biệt:
MotorbikeDelivery,GrabDelivery, vàTruckDelivery. -
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.
-
-
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_feecủ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:
-
Sử dụng module
abc, tạo một lớp trừu tượngPaymentGateway. -
Trong lớp này, định nghĩa một phương thức trừu tượng là
process_payment(self, amount). -
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."
-
-
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
-
Tạo một lớp trừu tượng
BaseElementvớ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().
-
-
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].
-
-
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
@staticmethodtên làget_driver(browser_name). -
Bên trong phương thức này, dùng
if-elif-else:-
Nếu
browser_namelà "chrome", in ra "Initializing Chrome Driver..." và trả về chuỗi "ChromeDriver object". -
Nếu
browser_namelà "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).
