NỘI DUNG BÀI HỌC

✅ Hướng phát triển TDD trong Automation Test
✅ Hướng phát triển BDD trong Automation Test
✅ Tại sao lại dùng khung BDD?

Phương thức phát triển phần mềm Agile là một tập hợp các phương thức phát triển lặp và tăng dần trong đó các yêu cầu và giải pháp được phát triển thông qua sự liên kết cộng tác giữa các nhóm tự quản và liên chức năng. Agile là cách thức làm phần mềm linh hoạt để làm sao đưa sản phẩm đến tay người dùng càng nhanh càng tốt càng sớm càng tốt và được xem như là sự cải tiến so với những mô hình cũ như mô hình “Thác nước (waterfall)” hay “CMMI”.

Aglie được coi là vòng xoắn ốc của Mác: sự vật hiện tượng phát triển theo hình xoắn ốc, đến 1 lúc nào đó sẽ quay lại hình thức cũ nhưng ở cấp độ cao hơn.

Test tự động là yếu tố sống còn của Agile, trong vòng xoắn ốc luôn phải có test. Vì sao ư! Vì để có thể tự tin sửa mã chương trình, đảm bảo sau khi thay đổi nếu phát sinh bug thì phát hiện và sửa được ngay, đó là lý do phải có test tự động.

1. Hướng phát triển TDD trong Automation Test

TDD (Test-Driven Development) là mô hình phát triển với trọng tâm hướng về việc kiểm thử. TDD được xây dựng theo hai tiêu chí: Test-First (Kiểm thử trước) và Refactoring (Điều chỉnh mã nguồn)

TDD là một quá trình phát triển lặp đi lặp lại. Mỗi lần lặp lại bắt đầu với một tập hợp các bài kiểm tra được viết cho một phần chức năng mới. Các thử nghiệm này được cho là không thành công trong khi bắt đầu lặp lại vì sẽ không có mã ứng dụng tương ứng với các thử nghiệm.
Vì trong giai đoạn tiếp theo của quá trình lặp lại, Mã ứng dụng được viết với mục đích vượt qua tất cả các bài kiểm tra được viết trước đó trong quá trình lặp lại. Khi mã ứng dụng đã sẵn sàng, các bài kiểm tra sẽ được chạy.

TDD đáp ứng “Tuyên ngôn về Agile” khi bản thân quy trình TDD thúc đẩy tính thực tiễn của sản phẩm, tương tác với người dùng. Để phát huy tối đa những lợi ích mà TDD mang lại, độ lớn của 1 đơn vị tính năng phần mềm (unit of function) cần đủ nhỏ để kịch bản kiểm thử dễ dàng được xây dựng và đọc hiểu, công sức debug kịch bản kiểm thử khi chạy thất bại cũng giảm thiểu hơn.

Theo TDD thì đa phần các test do Developer viết. Tester cũng có khi viết nhưng ít hơn.

✳️ Lợi ích của TDD:

  1. Bài kiểm tra đơn vị chứng minh rằng mã code của ứng dụng thực sự hoạt động
  2. Có thể điều khiển thiết kế của chương trình
  3. Tái cấu trúc cho phép cải thiện thiết kế của mã
  4. Bộ kiểm tra hồi quy cấp thấp
  5. Kiểm tra đầu tiên giảm chi phí của các lỗi

✳️ Hạn chế của TDD:

  1. Nhà phát triển có thể coi đó là một sự lãng phí thời gian
  2. Bài kiểm tra có thể được nhắm mục tiêu vào việc xác minh các lớp và phương thức chứ không phải những gì mã thực sự nên làm
  3. Kiểm tra trở thành một phần của chi phí bảo trì của một dự án
  4. Viết lại bài kiểm tra khi yêu cầu thay đổi

 

Nếu chúng ta tóm tắt điều này dưới dạng các giai đoạn trong quá trình phát triển thì gồm các giai đoạn sau: Xác định yêu cầu, Thực hiện các bài kiểm tra, Sửa/Thêm/Tái cấu trúc mã.

An sẽ mô phỏng các giai đoạn thông qua Code nhé.

🔆 Giai đoạn 1: Xác định yêu cầu

Chúng ta sẽ lấy một ví dụ đơn giản về ứng dụng máy tính và chúng ta sẽ xác định các yêu cầu dựa trên các tính năng cơ bản của máy tính. Để đơn giản hơn, chúng ta sẽ cô đọng ứng dụng máy tính thành một lớp java đơn giản:

package com.anhtester;

public class Calculator {

    public int add(int number1, int number2) {
        return 0;
    }

}

Trong giai đoạn 1, các yêu cầu ứng dụng được thu thập và xác định. Lấy ví dụ về một máy tính đơn giản, chúng ta có thể nói rằng trong lần lặp 1 chúng ta muốn thực hiện 3 phép tính là:

  1. Phép cộng hai số
  2. Phép trừ hai số
  3. Phép nhân hai số

Vì vậy, như đã nói ở trên, TDD bắt đầu bằng việc xác định các yêu cầu dưới dạng các bài kiểm tra. 
Ví dụ yêu cầu đầu tiên của chúng ta về các bài kiểm tra:

Yêu cầu: Máy tính phải có khả năng cộng hai số.

Test 1: Cho hai số dương (10 và 20) máy tính có thể cộng hai số đó lại và cho ta kết quả đúng (30)

Test 2: Cho hai số âm (-10 và -20) máy tính cộng hai số đó lại và cho ta kết quả đúng (-30)


Trong giai đoạn 1, tất cả những gì chúng ta phải làm là viết các bài kiểm tra cho tất cả các yêu cầu tại thời điểm đó.

Yeah như vậy Developer sẽ viết các bài kiểm thử cho chức năng của Calculator theo yêu cầu cộng hai số gồm 2 test cases.

🔆 Giai đoạn 2: Thực hiện các bài kiểm tra

Bây giờ An cũng dùng Java để viết test cases nhé. Sử dụng TestNG Framework để thực hiện Unit Test cho 2 test cases trên.

package com.anhtester;

import org.testng.Assert;
import org.testng.annotations.Test;

public class TestCalculator {

    private Calculator myCalculator = new Calculator();

    @Test
    public void testAddTwoPositiveNumbers()
    {
        int expectedResult = 30;
        int actualResult = myCalculator.add(10, 20);
        Assert.assertEquals(actualResult, expectedResult, "Sai. Kết quả không chính xác.");
    }

    @Test
    public void testAddTwoNegativeNumbers()
    {
        int expectedResult = -30;
        int actualResult = myCalculator.add(-10, -20);
        Assert.assertEquals(actualResult, expectedResult, "Sai. Kết quả không chính xác.");
    }
}


Kết quả khi chạy testAddTwoPositiveNumbers:

java.lang.AssertionError: Sai. Kết quả không chính xác.
Expected :30
Actual   :0

Như vậy sau khi chạy test case trên cần xem lại phương thức "add" của chương trình máy tính Calculator.

..... một tiếng sau....

Và sau khi Dev xem lại thì phát hiện là nó return 0 nghĩa là nó luôn luôn bằng 0. Sai phải rồi 😄
(lúc đó chắc đang hẹn ghệ đi ăn hơi vội xíu @@)

Tương ứng với sai đó thì Dev cần sửa lại cho đúng với thuần phong mỹ tục 😝

 

🔆 Giai đoạn 3: Sửa/Thêm/Tái cấu trúc mã

Mã ở đây là cái mã code của ứng dụng phần mềm á nhen. Không phải cái mã code auto test đâu đà @@

Sau khi kiểm tra thất bại ở bước trước, chúng ta vào ứng dụng chỉ cần chỉnh lại phần return kết quả của phương thức "add". Bây giờ lớp Calculator của chúng ta sẽ thế này:

package com.anhtester;

public class Calculator {

    public int add(int number1, int number2) {
        return (number1 + number2);
    }

}


Với thay đổi này, chúng ta sẽ chạy lại Giai đoạn 2 đã đề cập trước đó nghĩa là chạy lại các test cases cho yêu cầu đó sau khi chỉnh sửa lại.

Kết quả của hai bài kiểm tra là:

===============================================
Default Suite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================

[Cucumber TestNG] Bài 1: Giới thiệu hướng phát triển TDD và BDD trong code Automation Test | Anh Tester


À pass cả 2 test cases. Yeah như vậy là xong phần Unit Test rồi.

Tương tự viết thêm các test cho các yêu cầu còn lại như ví dụ bên trên.

Khi tất cả các bài kiểm tra vượt qua, nó báo hiệu sự kết thúc của quá trình lặp lại. Nếu có nhiều tính năng hơn cần được triển khai trong sản phẩm của bạn, thì sản phẩm sẽ lại trải qua các giai đoạn tương tự nhưng lần này với bộ tính năng mới và có nhiều thử nghiệm hơn.

Tóm tắt hướng TDD nó thế này:
[Cucumber TestNG] Bài 1: Giới thiệu hướng phát triển TDD và BDD trong code Automation Test | Anh Tester
Tiếp theo, chúng ta sẽ chuyển sang BDD. Phần này sẽ tạo cơ sở để hiểu Gherkin và cuối cùng là Cucumber.

2. Hướng phát triển BDD trong Automation Test

Chúng ta đã thảo luận về cách TDD là một quy trình phát triển tập trung vào thử nghiệm, trong đó chúng ta bắt đầu viết thử nghiệm trước. Ban đầu, các thử nghiệm này không thành công nhưng khi chúng ta thêm nhiều mã ứng dụng hơn, các thử nghiệm này sẽ vượt qua. Điều này giúp chúng ta theo nhiều cách:

  • Chúng ta viết mã ứng dụng dựa trên các bài kiểm tra. Điều này mang lại môi trường thử nghiệm đầu tiên để phát triển và mã ứng dụng được tạo ra không có lỗi.
  • Với mỗi lần lặp lại, chúng ta viết các bài kiểm tra và kết quả là với mỗi lần lặp lại chúng ta nhận được một gói hồi quy tự động. Điều này hóa ra rất hữu ích vì với mỗi lần lặp lại, chúng ta có thể chắc chắn rằng các tính năng cũ hơn đang hoạt động.
  • Các thử nghiệm này đóng vai trò là tài liệu về hành vi của ứng dụng và tham chiếu cho các lần lặp lại trong tương lai.

Như vậy, trong mô hình TDD nhiệm vụ kiểm thử do Developer đảm nhiệm và vai trò chuyên hóa của người Tester gần như không còn nữa. Chắc hẳn các bạn sẽ tự hỏi: “Vậy một Acceptance Tester như chúng ta có vai trò gì trong mô hình?”, “Tại sao tôi phải hiểu về TDD khi người ta ko cần tôi trong quy trình đó?”

Quả thực, trong mô hình TDD người Acceptance Tester thực sự đã chết. Tuy nhiên, việc cộng gộp vai trò phát sinh vấn đề quá tải cho người developer. Để làm tốt công việc, xuyên suốt chu trình người developer phải chú ý thêm những vấn đề thuần túy của kiểm thử (test) như: “Cái gì cần test và cái gì không?” “Viết bao nhiêu kịch bản là đủ?” “Làm sao để hiểu là test đó thất bại?” “Bắt đầu test từ đâu?” …

Để giải quyết vần đề phát sinh mà vẫn tận dụng triệt để lợi ích mà TDD mang lại, Dan North phát triển một mô hình mới với tên gọi: Behavior-Driven Development – BDD (hoặc ta có thể hiểu là Acceptance Test-Driven Development – ATDD). Trong đó, một vai trò mới trong việc thiết kế kiểm thử (Test Design) được đặt ra:

Thay vì chờ đợi sản phẩm hoàn thành và kiểm thử, người tester/analyst tham gia vào quá trình xây dựng mã nguồn với vai trò phân tích và xây dựng hệ thống kịch bản kiểm thử dưới góc độ ngôn ngữ tự nhiên dễ hiểu từ các yêu cầu (requirement). Đồng thời, họ giúp đỡ developer trong việc giải thích và đưa ra các phương án xây dựng mã nguồn mang tính thực tiễn với người dùng ngay trước khi bắt tay xây dựng.

Trong BDD thì người developer liên hệ mật thiết với người tester và xây dựng mã nguồn với những phương án mà tester cung cấp theo mô hình TDD.

Kịch bản kiểm thử được phân chia làm 2 lớp: Lớp chấp nhận (feature/acceptance test) và Lớp đơn vị (unit test). Theo đó, kịch bản kiểm thử lớp đơn vị mang thuần tính thiết kế và phục vụ cho việc kiểm thử lớp đơn vị (Unit test) còn kịch bản kiểm thử lớp chấp nhận có thể được tái sử dụng cho quá trình kiểm thử hồi quy về sau (Regression Test).

Kiểm thử hướng hành vi (BDD) là một phần mở rộng của TDD. Giống như trong TDD thì BDD chúng ta cũng viết các bài kiểm tra trước và thêm mã ứng dụng. Sự khác biệt chính mà chúng ta có thể thấy ở đây là:

  • Các bài kiểm tra được viết bằng ngữ pháp tiếng Anh mô tả đơn giản
  • Các thử nghiệm được giải thích là hành vi của ứng dụng và tập trung vào người dùng hơn
  • Sử dụng các ví dụ để làm rõ các yêu cầu

Sự khác biệt này dẫn đến nhu cầu phải có một ngôn ngữ có thể định nghĩa với định dạng dễ hiểu.

🔆 Các tính năng của BDD

  1. Chuyển từ suy nghĩ trong "bài kiểm tra" sang suy nghĩ trong "hành vi"
  2. Hợp tác giữa các bên liên quan trong Kinh doanh: Nhà phân tích nghiệp vụ, Nhóm QA và nhà phát triển (developer, designer,...)
  3. Ngôn ngữ phổ biến, rất dễ để mô tả (tiếng anh phổ biến, ở VN thì giờ có cả framework hỗ trợ tiếng Việt)
  4. Mở rộng Phát triển dựa trên thử nghiệm (TDD) bằng cách sử dụng ngôn ngữ tự nhiên mà các bên liên quan phi kỹ thuật có thể hiểu được
  5. Các khung BDD như Cucumber hoặc JBehave là một công cụ hỗ trợ, đóng vai trò là "cầu nối" giữa Ngôn ngữ Kinh doanh và Kỹ thuật


BDD phổ biến và có thể được sử dụng cho các trường hợp kiểm thử cấp Đơn vị (Unit Test) và cho các trường hợp kiểm thử cấp UI

Các công cụ như TestNG hoặc JUnit (dành cho Java), RSpec (dành cho Ruby) hoặc trong .NET như MSpec hoặc SpecUnit phổ biến cho Kiểm tra đơn vị theo cách tiếp cận BDD.

Ngoài ra, bạn có thể viết thông số kỹ thuật kiểu BDD về tương tác giao diện người dùng. Giả sử bạn đang xây dựng một ứng dụng web, có thể bạn sẽ sử dụng thư viện tự động hóa trình duyệt như Playwright hoặc Selenium và tạo tập lệnh cho nó bằng cách sử dụng một trong các khung mà An vừa đề cập hoặc một công cụ với format given/when/then chẳng hạn như Cucumber (cho Java) hoặc SpecFlow (cho .NET).

Trong nội dung chính của chúng ta là dùng Cucumber TestNG với Selenium Java nhé @@

✅ Công cụ Cucumber trong BDD


🔆 Cucumber là gì?

Cucumber là một công cụ kiểm thử hay là một Testing Framework hỗ trợ Behavior Driven Development (BDD), cho phép người dùng định nghĩa hành vi của hệ thống với tiếng anh có ý nghĩa đơn giản bằng cách sử dụng một ngữ pháp được xác định bởi một ngôn ngữ gọi là Gherkin.

Cucumber hướng tới việc viết test case mà bất kỳ ai cũng có thể hiểu cho dù họ không có chuyên môn kĩ thuật.

Ví dụ như các nền tảng quen thuộc như Selenium thì thường chỉ người viết test hoặc có kĩ năng lập trình mới hiểu được những gì đang test, còn khách hàng hoặc các bên liên quan thì không đọc ngay code để hiểu mà họ cần hiểu qua tài liệu.

Cucumber ban đầu được thực hiện dành riêng cho ngôn ngữ Ruby và sau đó được mở rộng sang Java, cả Ruby và Java đều sử dụng Junit để chạy test. Sau này là với nhiều ngôn ngữ khác và khung sườn khác như C#, Javascript, Python và cùng với framework như TestNG, specflow,...

🔆 Tại sao phải sử dụng Cucumber?

Một số lý do sau nên dùng Cucumber:

  • Selenium và Cucumber là 2 công nghệ phổ biến
  • Hầu hết các dự án sử dụng Selenium để kiểm thử chức năng, họ muốn tích hợp Cucumber vì Cucumber dễ đọc và dễ hiểu luồng ứng dụng hơn.
  • Cucumber dựa trên phát triển hướng hành vi đóng vai trò là cầu nối giữa: Software Engineer và Business Analyst, Manual Tester và Automation Tester, Manual Tester và Developers.

🔆 Lợi ích của Cucumber

  • Giúp cho các bên liên quan đến dự án (stakeholders) có thể theo dõi hoạt động test mà không cần kiến thức kĩ thuật chuyên môn
  • Cucumber tập trung vào trải nghiệm người dùng cuối
  • Cách viết mã dễ bảo trì và thực hiện
  • Công cụ hiệu quả cho kiểm thử

 

✅ Test Cases dạng Gherkin trong Cucumber

🔆 Features

Feature có thể được hiểu là một đơn vị hoặc chức năng độc lập của một dự án. Ví dụ như một trang web thương mại điện tử, một vài tính năng (features) có thể xác định như:

  1. Đăng nhập bằng tài khoản hệ thống hoặc mạng xã hội
  2. Lựa chọn hàng hóa
  3. Thanh toán
  4. Đăng xuất


Trong Cucumber mỗi feature có thể hiểu là mỗi chứ năng độc lập của sản phẩm. Trước khi viết test scripts chúng ta nên xác định trước các features cần test để mang lại hiệu quả cao. Các tests xây dựng trong Cucumber được gọi là các feature files và có dạng .feature, mỗi feature cần test nên đặt trong 1 file feature tương ứng.


Features trong Cucumber được thể hiện bằng ngôn ngữ Gherkin bao gồm các thành phần sau:

  • Feature: Mô tả test script hiện tại sẽ được chạy
  • Scenario: Mô tả các bước thực hiện và kết quả đầu ra mong muốn cho một test case cụ thể
  • Scenario Outline: Scenario thực hiện nhiều tập dữ liệu (sets of data). Dữ liệu được lưu dưới dạng cấu trúc, phân cách nhau bằng kí hiệu | |
  • Given: Chỉ ra ngữ cảnh để thực thi
  • When: Chỉ ra hành động đã được thực hiện
  • Then: Kết quả đầu ra mong muốn của một test

    Và còn các thành phần khác nữa. Mình học chi tiết ở bài sau nhé.

Ví dụ test cases dạng Gherkin:

Feature: Login Page

  Scenario Outline: Login page to HRM
    Given user navigate to url "<url>"
    When user enter username "<username>" and password "<password>"
    And click login button
    Then The user redirect to Dashboard page
    Examples:
      | url                     | username     | password     |
      | https://app.hrsale.com/ | frances.burns| frances.burns|


🔆 Step Definitions

Mặc dù đã có file feature nhưng Cucumber chưa thực sự biết đoạn mã nào sẽ được thực thi cho từng scenario cụ thể được nêu trong file feature. Nó cần một file trung gian Step Definition, file này ánh xạ các bước thực hiện (step), features (Given,When,Then) trong scenario với đoạn mã (code) chức năng cần thực thi. Step được định nghĩa trong file java chẳng hạn như "stepdefinitions/StepLogin.java".

Ví dụ tương ứng với Test Cases Feature trên Login to HRM:

@Given("user navigate to url {string}")
public void userNavigateToUrl(String url) {
  System.out.println("Driver on Steps class: " + driver);
  driver.get(url);
}

@When("user enter username {string} and password {string}")
public void userEnterUsernameAndPassword(String email, String password) {
  WebUI.sleep(1);
  driver.findElement(By.xpath("//input[@id='iusername']")).sendKeys(email);
  driver.findElement(By.xpath("//input[@id='ipassword']")).sendKeys(password);
}

@And("click login button")
public void clickLoginButton() {
  WebUI.sleep(1);
  driver.findElement(By.xpath("//button[@type='submit']")).click();
}

@Then("The user redirect to Dashboard page")
public void theUserRedirectToDashboardPage() {
  WebUI.sleep(5);
  Assert.assertTrue(true, "Lỗi rồi");
}


Cái code bên trong Steps chính là code Page Object Model thuần trong TestNG mang vào.

🔆 Scenario

Scenario là cấu trúc lõi của Gherkin. Kịch bản test khai báo với từ khóa "Scenario:" và theo sau là tên kịch bản. Mỗi tính năng có thể có một hoặc nhiều scenarios, mỗi scenario bao gồm một hoặc nhiều steps.

Ví dụ cái Calculator bên trên từ TDD:

Feature: Calculator

Scenario: Add two Positive numbers
  Given I have entered 10 into the calculator
    And I have entered 20 into the calculator
   When I press add
   Then the result should be 30 on the screen

Scenario: Add two Negative numbers
  Given I have entered -10 into the calculator
    And I have entered -20 into the calculator
   When I press add
   Then the result should be -30 on the screen

Khi nhìn vào Gherkin dành cho 2 test cases Add Two Numbers thì cả người không chuyên kỹ thuật cũng dễ hiểu. Vì nó toàn tiếng Anh. Còn về mặt code xử lý bên trong thì lúc này mới cần người chuyên Kỹ thuật viết. Nó đã phân luồng rất rõ ràng.

✅ Tại sao lại dùng khung BDD?

Giả sử có một yêu cầu từ khách hàng đối với trang web Thương mại điện tử để tăng doanh số bán sản phẩm bằng cách triển khai một số tính năng mới trên trang web. Thách thức duy nhất của nhóm phát triển là biến ý tưởng của khách hàng thành thứ thực sự mang lại lợi ích cho khách hàng.

Ý tưởng ban đầu thật tuyệt vời. Nhưng thách thức duy nhất ở đây là người phát triển ý tưởng không phải là người có ý tưởng này. Nếu người có ý tưởng tình cờ lại là một nhà phát triển phần mềm tài năng, thì chúng ta có thể gặp may: ý tưởng có thể biến thành phần mềm hoạt động mà không cần phải giải thích với bất kỳ ai khác. Giờ đây, ý tưởng cần được truyền đạt và phải đi từ Chủ doanh nghiệp (Khách hàng) đến các nhóm phát triển hoặc nhiều người khác.

Hầu hết các dự án phần mềm liên quan đến các nhóm gồm nhiều người làm việc cộng tác với nhau, vì vậy giao tiếp chất lượng cao là rất quan trọng đối với thành công của họ. Như bạn có thể biết, giao tiếp tốt không chỉ là diễn tả một cách hùng hồn ý tưởng của bạn cho người khác, bạn cũng cần thu hút phản hồi để đảm bảo rằng bạn đã được hiểu đúng. Đây là lý do tại sao các nhóm phần mềm nhanh nhẹn đã học cách làm việc theo từng bước nhỏ, sử dụng phần mềm được xây dựng dần dần như phản hồi cho các bên liên quan “Đây có phải là ý của bạn không?

Bây giờ khi xem đoạn mã ví dụ trên, bất kỳ ai cũng có thể hiểu hoạt động của bài kiểm tra và mục đích của nó là gì. Nó tạo ra một tác động mạnh mẽ bất ngờ bằng cách cho phép mọi người hình dung ra hệ thống trước khi nó được xây dựng. Bất kỳ người dùng doanh nghiệp nào cũng sẽ đọc và hiểu bài kiểm tra và có thể cung cấp cho bạn phản hồi rằng liệu nó có phản ánh sự hiểu biết của họ về những gì hệ thống nên làm hay không và thậm chí nó có thể dẫn đến việc nghĩ đến các tình huống khác cũng cần được xem xét.

Vậy nhiệm vụ của Tester chúng ta là viết Test Cases dạng Gherkin trong test automation thì nó sẽ có lợi đôi điều là viết một lần dùng cho cả Manual và Automation 😊

Teacher

Teacher

Anh Tester

Software Quality Engineer

Đường dẫu khó chân vẫn cần bước đi
Đời dẫu khổ tâm vẫn cần nghĩ thấu

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