Làm thế nào để kiểm tra đơn vị tạo điều kiện cho thiết kế?


43

Đồng nghiệp của chúng tôi khuyến khích viết bài kiểm tra đơn vị như thực sự giúp chúng tôi tinh chỉnh thiết kế và tái cấu trúc những thứ của chúng tôi, nhưng tôi không thấy làm thế nào. Nếu tôi đang tải tệp CSV và phân tích cú pháp, kiểm tra đơn vị (xác thực các giá trị trong các trường) sẽ giúp tôi xác minh thiết kế của mình như thế nào? Ông đã đề cập đến khớp nối và mô-đun, v.v. nhưng với tôi nó không có nhiều ý nghĩa - mặc dù tôi không có nhiều nền tảng lý thuyết.

Điều đó không giống với câu hỏi mà bạn đánh dấu là trùng lặp, tôi sẽ quan tâm đến các ví dụ thực tế về cách thức này giúp ích, không chỉ là lý thuyết nói rằng "nó giúp". Tôi thích câu trả lời dưới đây và bình luận nhưng tôi muốn tìm hiểu thêm.


7
gnat

3
Câu trả lời dưới đây thực sự là tất cả những gì bạn cần biết. Ngồi bên cạnh những người viết các nhà máy sản xuất nhà máy phụ thuộc gốc tổng hợp suốt ngày là một anh chàng lặng lẽ viết mã đơn giản chống lại các bài kiểm tra đơn vị hoạt động chính xác, dễ xác minh và đã được ghi lại.
Robert Harvey

4
@gnat thực hiện kiểm tra đơn vị không tự động ngụ ý TDD, đó là một câu hỏi khác
Joppe

11
"kiểm tra đơn vị (xác thực các giá trị trong các trường)" - bạn dường như đang kết hợp các kiểm tra đơn vị với xác thực đầu vào.
jonrsharpe

1
@jonrsharpe Cho rằng đó là mã phân tích tệp CSV, anh ta có thể đang nói về một thử nghiệm đơn vị thực sự xác minh một chuỗi CSV nhất định cho đầu ra dự kiến.
JollyJoker

Câu trả lời:


3

Không chỉ kiểm tra đơn vị tạo điều kiện cho thiết kế, mà đó là một trong những lợi ích chính của họ.

Viết thử nghiệm đầu tiên ổ đĩa mô-đun và cấu trúc mã sạch.

Khi bạn viết mã kiểm tra trước, bạn sẽ thấy rằng bất kỳ "điều kiện" nào của một đơn vị mã nhất định sẽ tự nhiên bị đẩy ra phụ thuộc (thường thông qua giả hoặc sơ khai) khi bạn giả sử chúng trong mã của mình.

"Đưa ra điều kiện x, mong đợi hành vi y", thường sẽ trở thành một sơ khai để cung cấp x(đó là một kịch bản trong đó thử nghiệm cần xác minh hành vi của thành phần hiện tại) và ysẽ trở thành một giả, một cuộc gọi sẽ được xác minh tại kết thúc thử nghiệm (trừ khi đó là "nên trả về y", trong trường hợp đó, thử nghiệm sẽ chỉ xác minh rõ ràng giá trị trả về).

Sau đó, một khi đơn vị này hoạt động như được chỉ định, bạn chuyển sang viết các phụ thuộc (cho xy) bạn đã phát hiện ra.

Điều này làm cho việc viết mã sạch, mô-đun trở thành một quy trình rất dễ dàng và tự nhiên, trong khi nếu không, nó thường dễ làm mờ các trách nhiệm và hành vi của cặp đôi với nhau mà không nhận ra.

Kiểm tra viết sau này sẽ cho bạn biết khi mã của bạn có cấu trúc kém.

Khi việc kiểm tra viết cho một đoạn mã trở nên khó khăn vì có quá nhiều thứ còn sơ khai hoặc giả, hoặc vì mọi thứ quá chặt chẽ với nhau, bạn biết rằng bạn có những cải tiến để thực hiện trong mã của mình.

Khi "thay đổi bài kiểm tra" trở thành gánh nặng vì có quá nhiều hành vi trong một đơn vị, bạn biết rằng bạn có những cải tiến để thực hiện mã của mình (hoặc đơn giản là trong cách tiếp cận viết bài kiểm tra - nhưng đây không phải là trường hợp theo kinh nghiệm của tôi) .

Khi kịch bản của bạn trở nên quá phức tạp ( "nếu xyzsau đó ...") bởi vì bạn cần phải trừu tượng, bạn biết bạn có những cải tiến để thực hiện trong mã của bạn.

Khi bạn kết thúc với các thử nghiệm tương tự trong hai đồ đạc khác nhau vì trùng lặp và dự phòng, bạn biết bạn có những cải tiến để thực hiện trong mã của mình.

Dưới đây là một bài nói chuyện tuyệt vời của Michael Feathers chứng minh mối quan hệ rất chặt chẽ giữa khả năng kiểm tra và thiết kế trong mã (ban đầu được đăng bởi displayName trong các bình luận). Buổi nói chuyện cũng đề cập đến một số khiếu nại và quan niệm sai lầm về thiết kế tốt và khả năng kiểm tra nói chung.


@SSECommunity: Chỉ với 2 lần upvote tính đến ngày hôm nay, câu trả lời này rất dễ bị bỏ qua. Tôi đánh giá cao bài nói chuyện của Michael Feathers đã được liên kết trong câu trả lời này.
displayName

103

Điều tuyệt vời về các bài kiểm tra đơn vị là chúng cho phép bạn sử dụng mã của mình như thế nào các lập trình viên khác sẽ sử dụng mã của bạn.

Nếu mã của bạn khó xử khi kiểm tra đơn vị, thì có lẽ sẽ rất khó sử dụng. Nếu bạn không thể tiêm phụ thuộc mà không nhảy qua vòng, thì mã của bạn có thể sẽ không linh hoạt để sử dụng. Và nếu bạn cần dành nhiều thời gian để thiết lập dữ liệu hoặc tìm ra thứ tự để thực hiện, mã của bạn đang được kiểm tra có thể có quá nhiều khớp nối và sẽ rất khó để làm việc.


7
Câu trả lời chính xác. Tôi luôn thích nghĩ về các thử nghiệm của mình như là ứng dụng khách đầu tiên của mã; nếu viết các bài kiểm tra sẽ rất khó khăn, sẽ rất khó để viết mã tiêu thụ API hoặc bất cứ thứ gì tôi đang phát triển.
Stephen Byrne

41
Theo kinh nghiệm của tôi, hầu hết các bài kiểm tra đơn vị không "sử dụng mã của bạn như thế nào các lập trình viên khác sẽ sử dụng mã của bạn." Họ sử dụng mã của bạn vì các bài kiểm tra đơn vị sẽ sử dụng mã. Đúng, họ sẽ tiết lộ nhiều sai sót nghiêm trọng. Nhưng một API được thiết kế để kiểm tra đơn vị có thể không phải là API phù hợp nhất cho sử dụng chung. Các bài kiểm tra đơn vị được viết đơn giản thường yêu cầu mã bên dưới để lộ quá nhiều phần bên trong. Một lần nữa, dựa trên kinh nghiệm của tôi - sẽ thích thú khi nghe cách bạn xử lý việc này. (Xem câu trả lời của tôi dưới đây)
user949300

7
@ user949300 - Trước tiên tôi không phải là người tin tưởng vào thử nghiệm. Câu trả lời của tôi dựa trên ý tưởng về mã (và chắc chắn là thiết kế) trước tiên. API không nên được thiết kế để kiểm tra đơn vị, chúng nên được thiết kế cho khách hàng của bạn. Kiểm tra đơn vị giúp ước tính khách hàng của bạn, nhưng chúng là một công cụ. Họ ở đó để phục vụ bạn chứ không phải ngược lại. Và họ chắc chắn sẽ không ngăn bạn tạo mã tào lao.
Telastyn

3
Vấn đề lớn nhất với các bài kiểm tra đơn vị theo kinh nghiệm của tôi là viết những bài tốt cũng khó như viết mã tốt ngay từ đầu. Nếu bạn không thể nói mã tốt từ mã xấu, viết bài kiểm tra đơn vị sẽ không làm cho mã của bạn tốt hơn. Khi viết bài kiểm tra đơn vị, bạn phải có khả năng phân biệt giữa việc sử dụng trơn tru, dễ chịu và "khó xử" hay khó khăn. Họ có thể khiến bạn sử dụng mã của mình một chút, nhưng họ không buộc bạn phải nhận ra rằng những gì bạn đang làm là xấu.
jpmc26

2
@ user949300 - ví dụ kinh điển mà tôi có trong tâm trí ở đây là Kho lưu trữ cần có ConnString. Giả sử bạn phơi bày rằng đó là một tài sản có thể ghi công khai và phải đặt nó sau khi bạn mới () một Kho lưu trữ. Ý tưởng là sau lần thứ 5 hoặc thứ 6 bạn đã viết một bài kiểm tra mà quên thực hiện bước đó - và do đó gặp sự cố - bạn sẽ "tự nhiên" nghiêng về việc buộc ConnString trở thành một lớp bất biến - được thông qua trong hàm tạo - do đó làm cho bạn API tốt hơn và làm cho nhiều khả năng mã sản xuất có thể được viết để tránh bẫy này. Nó không phải là một sự đảm bảo nhưng nó có ích, imo.
Stephen Byrne

31

Tôi mất khá nhiều thời gian để nhận ra, nhưng lợi ích thực sự (chỉnh sửa: với tôi, khả năng của bạn có thể thay đổi) khi thực hiện phát triển theo hướng kiểm tra ( sử dụng kiểm tra đơn vị) là bạn phải thực hiện thiết kế API trước !

Một cách tiếp cận điển hình để phát triển là trước tiên tìm ra cách giải quyết một vấn đề nhất định, và với kiến ​​thức và thiết kế triển khai ban đầu đó, một số cách để gọi giải pháp của bạn. Điều này có thể cho một số kết quả khá thú vị.

Khi làm TDD, bạn phải viết mã đầu tiên sẽ sử dụng giải pháp của mình. Thông số đầu vào, và đầu ra dự kiến ​​để bạn có thể đảm bảo nó là đúng. Điều đó đòi hỏi bạn phải tìm ra những gì bạn thực sự cần phải làm nó, để bạn có thể tạo ra các bài kiểm tra có ý nghĩa. Sau đó và chỉ sau đó bạn thực hiện các giải pháp. Đó cũng là kinh nghiệm của tôi khi bạn biết chính xác những gì mã của bạn cần đạt được, nó sẽ trở nên rõ ràng hơn.

Sau đó, sau khi kiểm tra đơn vị thực hiện giúp bạn đảm bảo rằng tái cấu trúc không phá vỡ chức năng và cung cấp tài liệu về cách sử dụng mã của bạn (mà bạn biết là đúng khi thử nghiệm được thông qua!). Nhưng đây chỉ là thứ yếu - lợi ích lớn nhất là tư duy tạo mã ngay từ đầu.


Đó chắc chắn là một lợi ích nhưng tôi không nghĩ đó là lợi ích "thực sự" - lợi ích thực sự đến từ việc viết các bài kiểm tra cho mã của bạn một cách tự nhiên đẩy "điều kiện" ra khỏi sự phụ thuộc và bỏ qua việc phụ thuộc quá mức (thúc đẩy tiếp tục trừu tượng hóa ) trước khi nó bắt đầu.
Ant P

Vấn đề là bạn viết toàn bộ các thử nghiệm trả trước phù hợp với API đó, sau đó nó không hoạt động chính xác khi cần và bạn phải viết lại mã của mình và tất cả các thử nghiệm. Đối với các API đối mặt công khai, có khả năng chúng sẽ không thay đổi và phương pháp này vẫn ổn. Tuy nhiên, các API cho mã chỉ được sử dụng trong nội bộ sẽ thay đổi rất nhiều khi bạn tìm ra cách triển khai một tính năng cần nhiều API bán riêng hoạt động cùng nhau
Juan Mendes

@AntP Vâng, đây là một phần của thiết kế API.
Thorbjørn Ravn Andersen

@JuanMendes Điều này không phổ biến và những thử nghiệm đó sẽ cần phải được thay đổi, giống như bất kỳ mã nào khác khi bạn thay đổi yêu cầu. Một IDE tốt sẽ giúp bạn cấu trúc lại các lớp như một phần công việc được thực hiện tự động khi bạn thay đổi chữ ký phương thức, v.v.
Thorbjørn Ravn Andersen

@JuanMendes nếu bạn đang viết các bài kiểm tra tốt và các đơn vị nhỏ thì tác động của hiệu ứng bạn mô tả là không nhỏ trong thực tế.
Ant P

6

Tôi đồng ý 100% rằng các bài kiểm tra đơn vị giúp "giúp chúng tôi tinh chỉnh những thứ thiết kế và tái cấu trúc của chúng tôi".

Tôi có hai suy nghĩ về việc họ có giúp bạn thực hiện thiết kế ban đầu hay không . Vâng, họ tiết lộ những sai sót rõ ràng và buộc bạn phải suy nghĩ về "làm thế nào tôi có thể làm cho mã có thể kiểm tra được"? Điều này sẽ dẫn đến ít tác dụng phụ hơn, cấu hình và thiết lập dễ dàng hơn, v.v.

Tuy nhiên, theo kinh nghiệm của tôi, các bài kiểm tra đơn vị quá đơn giản, được viết trước khi bạn thực sự hiểu thiết kế nên là gì, (phải thừa nhận rằng đó là sự phóng đại của TDD lõi cứng, nhưng quá thường các lập trình viên viết một bài kiểm tra trước khi họ nghĩ nhiều) thường dẫn đến thiếu máu mô hình miền mà quá nhiều nội bộ.

Kinh nghiệm của tôi với TDD đã có từ vài năm trước, vì vậy tôi rất muốn nghe những kỹ thuật mới hơn có thể giúp ích trong việc viết các bài kiểm tra không thiên vị thiết kế cơ bản quá nhiều. Cảm ơn.


Số lượng lớn các tham số phương thức là một mùi mã và một lỗi thiết kế.
Sufian

5

Kiểm tra đơn vị cho phép bạn xem các giao diện giữa các chức năng hoạt động như thế nào và thường cung cấp cho bạn cái nhìn sâu sắc về cách cải thiện cả thiết kế cục bộ và thiết kế tổng thể. Hơn nữa, nếu bạn phát triển các bài kiểm tra đơn vị của mình trong khi phát triển mã của mình, bạn có một bộ kiểm tra hồi quy đã sẵn sàng. Không thành vấn đề nếu bạn đang phát triển UI hoặc thư viện phụ trợ.

Khi chương trình được phát triển (với các bài kiểm tra đơn vị), khi các lỗi được phát hiện, bạn có thể thêm các kiểm tra để xác nhận rằng các lỗi đã được sửa.

Tôi sử dụng TDD cho một số dự án của tôi. Tôi đã nỗ lực rất nhiều trong việc tạo ra các ví dụ mà tôi lấy từ sách giáo khoa hoặc từ các bài báo được coi là chính xác và kiểm tra mã tôi đang phát triển bằng cách sử dụng các ví dụ này. Bất kỳ sự hiểu lầm nào tôi có liên quan đến các phương pháp đều trở nên rất rõ ràng.

Tôi có xu hướng lỏng lẻo hơn một số đồng nghiệp của mình, vì tôi không quan tâm nếu mã được viết trước hay bài kiểm tra được viết trước.


Đó là một câu trả lời tuyệt vời cho tôi. Bạn có phiền để đưa ra một vài ví dụ, ví dụ một ví dụ cho mỗi trường hợp (khi bạn hiểu rõ hơn về thiết kế, v.v.).
Người dùng039402

5

Khi bạn muốn kiểm tra đơn vị trình phân tích cú pháp của mình để phát hiện giá trị phân định chính xác, bạn có thể muốn chuyển nó một dòng từ tệp CSV. Để làm cho bài kiểm tra của bạn trực tiếp và ngắn gọn, bạn có thể muốn kiểm tra nó thông qua một phương thức chấp nhận một dòng.

Điều này sẽ tự động làm cho bạn tách việc đọc các dòng khỏi việc đọc các giá trị riêng lẻ.

Ở một cấp độ khác, bạn có thể không muốn đưa tất cả các loại tệp CSV vật lý vào dự án thử nghiệm của mình nhưng hãy làm điều gì đó dễ đọc hơn, chỉ cần khai báo một chuỗi CSV lớn trong thử nghiệm của bạn để cải thiện khả năng đọc và mục đích của thử nghiệm. Điều này sẽ dẫn bạn tách rời trình phân tích cú pháp của bạn khỏi bất kỳ I / O nào bạn làm ở nơi khác.

Chỉ là một ví dụ cơ bản, chỉ cần bắt đầu thực hành nó, bạn sẽ cảm thấy kỳ diệu ở một số điểm (tôi có).


4

Nói một cách đơn giản, viết các bài kiểm tra đơn vị giúp phơi bày các lỗ hổng trong mã của bạn.

Hướng dẫn ngoạn mục này để viết mã có thể kiểm tra , được viết bởi Jonathan Wolter, Russ Ruffer và Miško Hevery, chứa nhiều ví dụ về cách sai sót trong mã, xảy ra để ức chế kiểm tra, cũng ngăn chặn việc tái sử dụng dễ dàng và tính linh hoạt của cùng một mã. Vì vậy, nếu mã của bạn có thể kiểm tra được, nó sẽ dễ sử dụng hơn. Hầu hết các "đạo đức" là những mẹo đơn giản đến nực cười giúp cải thiện đáng kể thiết kế mã ( Dependency Injection FTW).

Ví dụ: Rất khó để kiểm tra xem phương thức computeStuff có hoạt động đúng không khi bộ đệm bắt đầu gỡ bỏ nội dung. Điều này là do bạn phải thêm thủ công vào bộ đệm cho đến khi "bigCache" gần đầy.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Tuy nhiên, khi chúng tôi sử dụng phép nội xạ phụ thuộc, sẽ dễ dàng hơn để kiểm tra xem phương thức computeStuff có hoạt động đúng không khi bộ đệm bắt đầu gỡ bỏ nội dung. Tất cả những gì chúng tôi làm là tạo ra một thử nghiệm trong đó chúng tôi gọi là new HereIUseDI(buildSmallCache()); Thông báo, chúng tôi có nhiều quyền kiểm soát đối tượng hơn và nó trả cổ tức ngay lập tức.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Những lợi ích tương tự có thể có khi mã của chúng tôi yêu cầu dữ liệu thường được lưu giữ trong cơ sở dữ liệu ... chỉ cần chuyển chính xác dữ liệu bạn cần.


2
Thành thật mà nói, tôi không chắc bạn muốn nói ví dụ như thế nào. Phương thức computeStuff liên quan đến bộ đệm như thế nào?
John V

1
@ user970696 - Có, tôi đang ám chỉ rằng "computeStuff ()" sử dụng bộ đệm. Câu hỏi là "ComputeStuff () có hoạt động chính xác mọi lúc không (phụ thuộc vào trạng thái của bộ đệm)" Do đó, thật khó để xác nhận rằng computeStuff () thực hiện những gì bạn muốn CHO TẤT CẢ CÁC PACHIBLE CACHE STATE nếu bạn không thể trực tiếp đặt / xây dựng bộ đệm vì bạn đã mã hóa dòng "cacheOfExpensiveComputing = buildBigCache ();" (trái ngược với việc chuyển trực tiếp vào bộ đệm qua bộ tạo)
Ivan

0

Tùy thuộc vào ý nghĩa của 'Bài kiểm tra đơn vị', tôi không nghĩ các bài kiểm tra Đơn vị cấp thấp thực sự tạo điều kiện cho thiết kế tốt cũng như các bài kiểm tra tích hợp cấp cao hơn một chút - kiểm tra xem một nhóm các tác nhân (lớp, chức năng, bất cứ điều gì) trong mã của bạn kết hợp đúng cách để tạo ra một loạt các hành vi mong muốn đã được thỏa thuận giữa nhóm phát triển và chủ sở hữu sản phẩm.

Nếu bạn có thể viết bài kiểm tra ở các cấp độ đó, điều đó thúc đẩy bạn tạo ra mã đẹp, logic, giống như API mà không yêu cầu nhiều phụ thuộc điên rồ - mong muốn có một thiết lập thử nghiệm đơn giản sẽ tự nhiên khiến bạn không có nhiều phụ thuộc điên hoặc mã kết hợp chặt chẽ.

Mặc dù vậy, không có lỗi - Các bài kiểm tra đơn vị có thể dẫn bạn đến thiết kế xấu, cũng như thiết kế tốt. Tôi đã thấy các nhà phát triển lấy một chút mã đã có thiết kế logic đẹp và một mối quan tâm duy nhất, và tách nó ra và giới thiệu nhiều giao diện hoàn toàn cho mục đích thử nghiệm, và kết quả là làm cho mã dễ đọc hơn và khó thay đổi hơn , cũng như thậm chí có thể có nhiều lỗi hơn nếu nhà phát triển đã quyết định rằng có nhiều bài kiểm tra đơn vị cấp thấp có nghĩa là họ không phải có bài kiểm tra cấp cao hơn. Một ví dụ yêu thích cụ thể là một lỗi tôi đã sửa trong đó có rất nhiều mã bị hỏng, 'có thể kiểm tra' liên quan đến việc lấy thông tin trên và ngoài bảng tạm. Tất cả được chia nhỏ và tách rời thành các mức độ chi tiết rất nhỏ, với rất nhiều giao diện, rất nhiều giả trong các bài kiểm tra và các công cụ thú vị khác. Chỉ có một vấn đề - không có bất kỳ mã nào thực sự tương tác với cơ chế clipboard của HĐH,

Các bài kiểm tra đơn vị chắc chắn có thể thúc đẩy thiết kế của bạn - nhưng chúng không tự động hướng dẫn bạn đến một thiết kế tốt. Bạn cần phải có ý tưởng về thiết kế tốt là gì vượt ra ngoài 'mã này đã được thử nghiệm, do đó nó có thể kiểm tra được, do đó nó tốt'.

Tất nhiên, nếu bạn là một trong những người mà 'bài kiểm tra đơn vị' nghĩa là 'mọi bài kiểm tra tự động không được điều khiển qua UI', thì một số cảnh báo đó có thể không phù hợp - như tôi đã nói, tôi nghĩ những bài kiểm tra đó cao hơn kiểm tra tích hợp -level thường là những bài kiểm tra hữu ích hơn khi nói đến thiết kế của bạn.


-2

Các thử nghiệm đơn vị có thể giúp tái cấu trúc khi mã mới vượt qua tất cả các thử nghiệm .

Giả sử bạn đã triển khai một bong bóng vì bạn đang vội và không quan tâm đến hiệu suất, nhưng bây giờ bạn muốn có một quicksort vì dữ liệu ngày càng dài hơn. Nếu tất cả các bài kiểm tra vượt qua, mọi thứ có vẻ tốt.

Tất nhiên các bài kiểm tra phải toàn diện để làm cho công việc này. Trong ví dụ của tôi, các thử nghiệm của bạn có thể không bao gồm sự ổn định vì điều đó không liên quan đến bubbleort.


1
Điều này đúng nhưng đó là lợi ích bảo trì nhiều hơn là tác động trực tiếp đến chất lượng thiết kế mã.
Ant P

@AntP, OP đã hỏi về tái cấu trúc và kiểm tra đơn vị.
om

1
Câu hỏi đề cập đến tái cấu trúc nhưng câu hỏi thực tế là về cách kiểm tra đơn vị có thể cải thiện / xác minh thiết kế mã - không làm giảm quá trình tái cấu trúc chính nó.
Ant P

-3

Tôi đã thấy các thử nghiệm đơn vị có giá trị nhất để tạo điều kiện cho việc bảo trì dự án dài hạn hơn. Khi tôi trở lại một dự án sau nhiều tháng và không nhớ nhiều chi tiết, việc chạy thử giúp tôi không phá vỡ mọi thứ.


6
Đó chắc chắn là một khía cạnh quan trọng của các bài kiểm tra, nhưng không thực sự trả lời câu hỏi (đó không phải là lý do tại sao bài kiểm tra tốt, nhưng chúng ảnh hưởng đến thiết kế như thế nào).
Hulk
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.