Chapter 14: Testing and Readability

Trong chương này, chúng tôi sẽ chỉ cho bạn một số kĩ thuật đơn giản để viết các test sạch sẽ và hiệu quả.

Testing định nghĩa khác nhau với những người khác nhau. Trong chương này, chúng tôi sử dụng “test” có nghĩa là một vài code mà mục đích duy nhất của nó là kiểm tra hành vi của 1 phần (“real”) của code khác. Chúng tôi sẽ tập trung vào việc khía cạnh khả năng đọc của test và không nói sâu bạn nên viết test code trước khi viết code thực sự (“test-driven development”) hoặc khái cạnh về sự phát triển của test.

“Real” code vs “test” code :D

Make Tests Easy to Read and Maintain

Nó có vai trò quan trọng như viết code thực sự. Các coders khác sẽ thường nhìn các test code như các dạng tài liệu không chính thức của cách real code làm việc và nên được sử dụng. Do đó, các test dễ đọc, người dùng sẽ dễ hiểu hơn hành vi thực sự của code.

K E Y I D E A
Test code should be readable so that other coders are comfortable changing or adding tests.

Khi test code lớn và đáng sợ, đây là 1 vài thứ sẽ xảy ra:

  • Coders ngại sửa đổi real code. Oh, chúng tôi không muốn làm bẩn đoạn code này - cập nhất tất cả các test sẽ là 1 cơn ác mộng!
  • Coders không muốn thêm các new tests khi họ thêm code mới. Và theo thời gian, càng ngày các ít module của bạn được test và bạn không còn tự tin với mọi việc.

Thay vì đó, bạn muốn khuyến khích ai đó (trừ bạn) làm việc với code được thoải mái với test code. Chúng nên có thể đọc và chẩn đoán được tại sao có 1 sự thay đổi khác với test đã tồn tại và cảm giác như thêm các tests mới là việc dễ dàng.

What’s Wrong with This Test?

Trong codebase của chúng tôi, chúng tôi có 1 hàm sắp xếp và lọc danh sách kết quả tìm kiếm đã được ghi lại. Đây là khai báo hàm:

1
2
// Sort 'docs' by score (highest first) and remove negative-scored documents.
void SortAndFilterDocs(vector<ScoredDocument>* docs);

Đoạn code Test hàm này sẽ như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Test1() {
vector<ScoredDocument> docs;
docs.resize(5);
docs[0].url = "http://example.com";
docs[0].score = -5.0;
docs[1].url = "http://example.com";
docs[1].score = 1;
docs[2].url = "http://example.com";
docs[2].score = 4;
docs[3].url = "http://example.com";
docs[3].score = -99998.7;
docs[4].url = "http://example.com";
docs[4].score = 3.0;

SortAndFilterDocs(&docs);

assert(docs.size() == 3);
assert(docs[0].score == 4);
assert(docs[1].score == 3.0);
assert(docs[2].score == 1);
}

Có ít nhất 8 vấn đề khác nhau trong đoạn test code này. Trong phần cuối của chương, bạn sẽ có thể tìm và sửa chúng

Making This Test More Readable

Như là 1 nguyên tắc chung thiết kế, bạn nên ẩn những chi tiết ít quan trọng từ người dùng, để những phần chi tiết quan trọng nổi bật lên.

Đoạn test code từ phần trước rõ ràng vi phạm nguyên tắc này. Mỗi phần chi tiết của test ở ngay phía trước và trung tâm là các chi tiết vụn vặt không quan trọng setting cho vector<ScoredDocument>. Đa số đoạn code trong ví dụ liên quan đến url, score, và docs[] cái mà chỉ nói chi tiết một object trong C++ được thiết lập như thế nào mà không nói về test này đang làm gì ở mức level cao.

Do đó, bước đầu tiên để loại bỏ điều này, bạn nên tạo 1 helper như sau:

1
2
3
4
void MakeScoredDoc(ScoredDocument* sd, double score, string url) {
sd->score = score;
sd->url = url;
}

Sử dụng hàm này, test code của chúng ta trở nên gọn hơn 1 chút:
1
2
3
4
5
6
7
8
9
void Test1() {
vector<ScoredDocument> docs;
docs.resize(5);
MakeScoredDoc(&docs[0], -5.0, "http://example.com");
MakeScoredDoc(&docs[1], 1, "http://example.com");
MakeScoredDoc(&docs[2], 4, "http://example.com");
MakeScoredDoc(&docs[3], -99998.7, "http://example.com");
...
}

Nhưng nó vẫn chưa đủ tốt - vẫn còn nhiều chi tiết không quan trọng. Ví dụ, đối số http://example.com. Nó luôn luôn giống nhau và URL chính xác như vậy không quan trọng - nó chỉ nên là các giá trị điền vào ScoredDocument.

Một thứ không quan trọng khác chúng ta buộc phải xem xét là docs.resize(5)&docs[0], &docs[1],.... Hãy thay đổi hepler để làm nhiều việc hơn, và gọi là AddScoredDoc():

1
2
3
4
5
6
void AddScoredDoc(vector<ScoredDocument>& docs, double score) {
ScoredDocument sd;
sd.score = score;
sd.url = "http://example.com";
docs.push_back(sd);
}

Sử dụng hàm này, test code của chúng ta thậm chí gọn hơn:
1
2
3
4
5
6
7
8
void Test1() {
vector<ScoredDocument> docs;
AddScoredDoc(docs, -5.0);
AddScoredDoc(docs, 1);
AddScoredDoc(docs, 4);
AddScoredDoc(docs, -99998.7);
...
}

Đoạn code đã tốt hơn nhưng vẫn không đáp ứng yêu cầu vào về khả năng đọc và viết test. Nếu bạn muốn thêm test khác với 1 tập các tài liệu ghi điểm, nó yêu cầu rất nhiều copy và paste. Liệu chúng ta có thể cải thiện vấn đề này?

Creating the Minimal Test Statement

Để cải thiện vấn đề test code này, hãy sử dụng kĩ thuật từ Chapter 12, Turning Thoughts into Code.. Hãy mô tả cái mà test của chúng ta đang cố làm bằng tiếng Anh:

1
2
We have a list of documents whose scores are [-5, 1, 4, -99998.7, 3]. After
SortAndFilterDocs(), the remaining documents should have scores of [4, 3, 1], in that order.

Như bạn có thể thấy, không 1 chỗ nào trong mô tả này chúng ta nhắc đến vector<ScoredDocument>. Mảng của điểm không quan trọng ở đây. Về ý tưởng, test code của chúng ta sẽ như sau:
1
CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");

Chúng tôi có thể làm nổi bật lên vấn đề của test này xuống 1 dòng code!

Điều này không hiếm. Bản chất của hầu hết các test là làm nổi lên đối với input/trường hợp này, mong chờ hành vi/ouput này. Và nhiều lần, mục đích này có thể mô tả chỉ trong 1 dòng. Thêm vào đó copy rất ngắn gọn và dễ đọc, giữ các test của bạn ngắn và thực sự rất dễ dàng để thêm nhiều test cases.

Implementing Custom “Minilanguages”

Nhận thấy rằng CheckScoresBeforeAfter() nhận vào 1 đối số chuỗi mô tả mảng các điểm. Trong các phiên bản gần nhất của C++, bạn có thể truyền mảng như sau:

1
CheckScoresBeforeAfter({-5, 1, 4, -99998.7, 3}, {4, 3, 1});

Vì chúng tôi không làm điều đó ở thời điểm hiện tại, chúng tôi đặt điểm số trong 1 chuỗi, phân tách bằng dấu phẩy. Với cách làm này CheckScoresBeforeAfter() cần phải phân tích đối số chuỗi này.

Nói chung, việc xác định một ngôn ngữ tùy chỉnh có thể là một cách mạnh mẽ để thể hiện nhiều thông tin trong một khoảng trống nhỏ. Các ví dụ khác bao gồm printf() và các thư viện biểu thức chính quy.

Trong trường hợp này, viết 1 vài helper để phân tích danh sách số phân tách bằng dấu phẩy không quá khó. Hàm CheckScoresBeforeAfter() sẽ như sau:

1
2
3
4
5
6
void CheckScoresBeforeAfter(string input, string expected_output) {
vector<ScoredDocument> docs = ScoredDocsFromString(input);
SortAndFilterDocs(&docs);
string output = ScoredDocsToString(docs);
assert(output == expected_output);
}

Và để cho đầy đủ, đây là hepler chuyển đổi giữa string và vector<ScoredDocument>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vector<ScoredDocument> ScoredDocsFromString(string scores) {
vector<ScoredDocument> docs;

replace(scores.begin(), scores.end(), ',', ' ');

// Populate 'docs' from a string of space-separated scores.
istringstream stream(scores);
double score;
while (stream >> score) {
AddScoredDoc(docs, score);
}

return docs;
}
string ScoredDocsToString(vector<ScoredDocument> docs) {
ostringstream stream;
for (int i = 0; i < docs.size(); i++) {
if (i > 0) stream << ", ";
stream << docs[i].score;
}

return stream.str();
}

Dường như có nhiều code trong lần nhìn đầu tiên, nhưng những gì nó làm cho bạn và vô cùng mạnh mẽ. Vì bạn có thể viết toàn bộ test chỉ với 1 lời gọi CheckScoresBeforeAfter(), bạn sẽ có xu hướng thêm được nhiều test hơn (như chúng tôi sẽ làm trong phần sau của chương này).

Making Error Messages Readable

Đoạn code trước đẹp rồi, nhưng vẫn còn vấn đề ở chỗ assert(output == expected_output) lỗi? Nó sẽ đưa ra thông báo lỗi như sau:

1
2
Assertion failed: (output == expected_output),
function CheckScoresBeforeAfter, file test.cc, line 37.

Chắc chắn, nếu bạn nhìn thấy lỗi này, bạn sẽ tự hỏi, Giá trị của ouput và expected_output là gì?

Using Better Versions of assert()

May mắn thay, đa số các ngôn ngữ và thư viện đều có phiên bản “tinh vi” hơn của hàm assert() bạn có thể dùng. Thay vì viết:

1
assert(output == expected_output);

bạn có thể sử dụng thư viện Boost C++:
1
BOOST_REQUIRE_EQUAL(output, expected_output)

Bây giờ, nếu test lỗi, bạn sẽ nhận được 1 thông báo chi tiết như sau:
1
2
test.cc(37): fatal error in "CheckScoresBeforeAfter": critical check
output == expected_output failed ["1, 3, 4" != "4, 3, 1"]

với nhiều thông tin hữu ích hơn.

Bạn nên sử dụng các phương thức hữu ích này để assert() khi chúng khả dụng. Nó sẽ trả về cho bạn mỗi test khi lỗi.

BETTER ASSERT() IN OTHER LANGUAGES
Trong Python, built-in assert a == b sẽ tạo ra lỗi thông báo như:

1
2
3
File "file.py", line X, in <module>
assert a == b
AssertionError

Thay vào đó bạn có thể sử dụng phương thức assertEqual() trong module unittest:
1
2
3
4
5
6
7
8
9
10
import unittest

class MyTestCase(unittest.TestCase):
def testFunction(self):
a = 1
b = 2
self.assertEqual(a, b)

if __name__ == '__main__':
unittest.main()

Sẽ tạo ra thông báo lỗi như:
1
2
3
File "MyTestCase.py", line 7, in testFunction
self.assertEqual(a, b)
AssertionError: 1 != 2

Với mỗi ngôn ngữ bạn đang sử dụng, có thể có các library/framework (ví dụ XUnit) có thể giúp bạn. It pays to know your libraries

Hand-Crafted Error Messages

Sử dụng BOOST_REQUIRE_EQUAL(), bạn có thể nhận được thông báo lỗi đẹp hơn:

1
output == expected_output failed ["1, 3, 4" != "4, 3, 1"]

Tuy nhiên thông báo này vẫn có thể được cải thiện. Ví dụ, sẽ hữu ích hơn khi nhìn thấy input gốc cái mà đang nhận lỗi. Ý tưởng thì thông báo lỗi sẽ là cái gì đó như:
1
2
3
4
CheckScoresBeforeAfter() failed,
Input: "-5, 1, 4, -99998.7, 3"
Expected Output: "4, 3, 1"
Actual Output: "1, 3, 4"

Nếu đó là thứ mà bạn muốn, thử viết nó thôi!

1
2
3
4
5
6
7
8
9
void CheckScoresBeforeAfter(...) {
...
if (output != expected_output) {
cerr << "CheckScoresBeforeAfter() failed," << endl;
cerr << "Input: \"" << input << "\"" << endl;
cerr << "Expected Output: \"" << expected_output << "\"" << endl;
cerr << "Actual Output: \"" << output << "\"" << endl;
abort();
}

Về mặt tâm lý, các thông báo lỗi nên hữu ích nhất có thể. Đôi khi, hiển thị các thông báo bằng cách xây dựng lại 1 “custom assert” là cách tốt nhất để làm việc này.

Choosing Good Test Inputs

Có 1 nghệ thuật chọn giá trị cho các đầu vào tốt đối với các test của bạn. Những cái chúng ta có ngay bây giờ có vẻ hơi khó hiểu:

1
CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");

Làm sao để chúng ta chọn các giá trị đầu vào tốt? Các good inputs nên test triệt để được code. Nhưng chúng cũng nên đơn giản để dễ đọc.

KEY IDEA
In general, you should pick the simplest set of inputs that completely exercise the code.

Ví dụ, giả định bạn vừa mới viết:

1
CheckScoresBeforeAfter("1, 2, 3", "3, 2, 1");

Mặc dù test này đơn giản, nó không test hành vi “lọc các điểm số âm” của hàm SortAndFilterDocs(). Nếu có 1 bug trong phần này của code, input này không kích hoạt nó.

Ở 1 chiều hướng khác, giả định bạn viết test như:

1
2
CheckScoresBeforeAfter("123014, -1082342, 823423, 234205, -235235",
"823423, 234205, 123014");

Các giá trị này phức tạp không cần thiết. (Và chúng thậm chí không kiểm tra kĩ được code)

Simplifying the Input Values

Vậy chúng ta có thể cải thiện input này với giá trị nào?

1
CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");

Well, thứ đầu tiên bạn có thể nhận thấy là giá trị rất “ồn ào” -99998.7. Giá trị này chỉ có nghĩa “một vài số âm” do đó để đơn giản, nó nên chỉ là -1 (Nếu -99998.7 thì nên có nghĩa là “một số rất âm”, mà kể cả như vậy, một giá trị tốt hơn của nó nên được sắc nét thành -1e100).

KEY IDEA
Prefer clean and simple test values that still get the job done.

Những giá trị khác trong test của chúng ta cũng không quá tệ, nhưng trong khi chúng ta ở đâu, chúng ta có thể giảm chúng thành các số nguyên đơn giản nhất có thể. Ngoài ra, chỉ cần 1 giá trị âm là cần để test việc giá trị âm bị loại bỏ. Đây là phiên bản mới của test:

1
CheckScoresBeforeAfter("1, 2, -1, 3", "3, 2, 1");

Chúng ta đã đơn giản hóa các giá trị của test mà không hề giảm hiệu quả đi :D

LARGE “SMASHER” TESTS
(Tôi đang hiểu là không nên test các giá trị khủng, thay vào đó hiệu quả hơn là xây dựng nó theo chương trình)

Chắc chắn có những giá trị trong các test của bạn chống lại việc đầu vào lớn, điên rỗ. Ví dụ, bạn có thể bị cám dỗ bởi các test như:

1
2
CheckScoresBeforeAfter("100, 38, 19, -25, 4, 84, [lots of values] ...",
"100, 99, 98, 97, 96, 95, 94, 93, ...");

Những đầu vào lớn như vậy là 1 phần công việc tốt để tìm ra lỗi như các lỗi tràn số hoặc các lỗi khác bạn không mong đợi.

Nhưng code như vậy là lớn và đáng sợ để nhìn vào và không hoàn toàn hiệu quả trong việc stress-testing code. Thay vào đó, sẽ hiệu của hơn để xây dựng đầu vào 1 đầu vào lớn theo chương trình, khởi tạo 1 giá trị lớn của 100.000 giá trị.

Multiple Tests of Functionality

Thay vì chỉ xây dựng 1 đầu vào đơn “hoàn hảo” để xem xét kĩ lưỡng code, nó thường dễ dàng hơn, hiệu quả hơn và dễ đọc hơn để viết nhiều bài kiểm tra nhỏ hơn.

Mỗi test nên đẩy vào code của bạn 1 hướng nhất định và thử tìm lỗi cụ thể. Ví dụ đây là 4 tests cho SortAndFilterDocs():

1
2
3
4
CheckScoresBeforeAfter("2, 1, 3", "3, 2, 1");	// Basic sorting
CheckScoresBeforeAfter("0, -0.1, -10", "0"); // All values < 0 removed
CheckScoresBeforeAfter("1, -2, 1, -2", "1, 1"); // Duplicates not a problem
CheckScoresBeforeAfter("", ""); // Empty input OK

Thậm chí có nhiều test bạn có thể viết nếu bạn muốn kiểm tra chi tiết. Có các test tách biệt cũng làm nó dễ đọc với các người tiếp theo làm việc với code. Nếu ai đó tình cờ gặp 1 lỗi, test lỗi sẽ xác định cính xác test nào lỗi.

Naming Test Functions

Test code là thường được tổ chức vào trong hàm - một cho mỗi phương thức và/hoặc tình huống bạn đang test. Ví dụ, đoạn code test SortAndFilterDocs() trong 1 hàm tên là Test1():

1
2
3
void Test1() {
...
}

Chọn 1 cái tên tốt cho 1 hàm test có vẻ tẻ nhạt và không liên quan, nhưng đừng vì thế mà chọn những cái tên không có nhiều ý nghĩa như Test1(), Test2() hoặc tương tự.

Thay vì đó bạn nên sử dụng 1 cái tên mô tả chi tiết về test. Cụ thể, nó rất tiện dụng nếu ai đó đọc test code của bạn mà có thể tìm ra:

  • Class đang được test (nếu có).
  • Hàm đang được test.
  • Vấn đề hoặc bug đang được test.

Một cách tiếp cận đơn giản để khởi tạo 1 tên hàm test tốt là chỉ cần kết hợp các thông tin lại với nhau, có thể sử dụng prefix “Test_”

Ví dụ, thay vì đặt tên nó là Test1(), chúng ta có thể sử dụng format Test_<FunctionName>():

1
2
3
void Test_SortAndFilterDocs() {
...
}

Tùy thuộc vào mức độ chi tiết của test, bạn nên cân nhắc việc tách các hàm test ra thành mỗi vấn đề để test. Bạn có thể sử dụng Test_<FunctionName>_<Situation>() format:
1
2
3
4
5
6
7
8
9
void Test_SortAndFilterDocs_BasicSorting() {
...
}

void Test_SortAndFilterDocs_NegativeValues() {
...
}
...


Đừng lo lắng là có các tên dài và lộn xộn ở đây. Đây không phải là 1 hàm chúng ta sẽ gọi trong codebase, do đó lý do để tránh 1 tên dài không áp dụng ở đây. Các tên hàm test hoạt động như 1 comment. Và như vậy, nếu 1 test lỗi, đa số các frameworks sẽ in tên hàm assert lỗi ra, do đó một tên hàm rõ ràng sẽ rất hữu ích ở đây.

Chú ý rằng nếu bạn đang sử dụng 1 framework testing, có thể có những nguyên tắc và conventions về tên của các phương thức. Ví dụ, unittest Python module mong đợi tên của các phương thức test bắt đầu với “test”.

Khi nói đến việc đặt tên cho các hàm helper trong test code, sẽ hữu ích để làm nổi bật lên hàm này có tự làm một vài assert nào không hay nó chỉ là một helper test-unaware thông thường. Ví dụ, trong chương này, một vào hepler được gọi như assert() với tên Check()…Nhưng hàm AddScoredDoc() được đặt tên chỉ là 1 helper thông thường.

What Was Wrong with That Test?

Ở phần đầu của chương, chúng tôi đã tuyên bố rằng có ít nhất 8 thứ đang sai với test này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Test1() {
vector<ScoredDocument> docs;
docs.resize(5);
docs[0].url = "http://example.com";
docs[0].score = -5.0;
docs[1].url = "http://example.com";
docs[1].score = 1;
docs[2].url = "http://example.com";
docs[2].score = 4;
docs[3].url = "http://example.com";
docs[3].score = -99998.7;
docs[4].url = "http://example.com";
docs[4].score = 3.0;

SortAndFilterDocs(&docs);

assert(docs.size() == 3);
assert(docs[0].score == 4);
assert(docs[1].score == 3.0);
assert(docs[2].score == 1);
}

Bây giờ chúng ta sẽ tìm hiểu 1 vài kĩ thuật để viết tests tốt hơn, hãy nhận dạng chúng:

  1. Test rất dài và đầy đủ các chi tiết không quan trọng. Bạn có thể mô tả thứ mà test này đang làm trong 1 câu, do đó các lệnh trong test này không nên quá dài.
  2. Thêm 1 test khác không phải dễ. Khi bạn cần copy/paste/modify, cài mà sẽ làm cho code dài hơn và yêu cầu đầy đủ bản sao.
  3. Thông điệp test lỗi không hữu ích. Nếu 1 test lỗi, nó sẽ nói rằng Assertion failed: docs.size() == 3, cái mà không cho bạn đủ thông tin để debug thêm.
  4. Test đang cố gắng test mọi thứ trong 1 lúc. Nó đang cố gắng test cả lọc số âm và chức năng sắp xếp. Nó sẽ dễ đọc hơn nếu phá vỡ vào trong nhiều tests.
  5. Đầu vào test không đơn giản. Cụ thể, điểm ví dụ -9998.7 “ồn ào” và nhận sự chú ý mặc dù nó không phải và 1 vài ý nghĩa của các giá trị đặc biệt. Giá trị của 1 số âm đơn giản hơn sẽ là đủ.
  6. Các input của test không thực hiện kiểm tra kĩ lưỡng được code. Ví dụ, nó không test trường hợp điểm là 0 (Bạn cần phải xem tài liệu để biết rằng nó có filter hay không?).
  7. Nó không test các đầu vào cực đoan khác, như 1 vector trống, 1 vector lớn hoặc 1 điểm lặp lại.
  8. Tên Test1() không có nhiều ý nghĩa - tên nên mô tả chức năng và vấn đề đang test.

Test-Friendly Development

Một vài code dễ dàng để test hơn code khác. Code lý tưởng để test là có giao diện được định nghĩa mà mà không có nhiều trạng thái hoặc “setup” khác và không có nhiều dữ liệu ẩn để kiểm tra.

Nếu bạn viết code và bạn biết sẽ viết test cho nó sau đó, một thứ vui vẻ sẽ diễn ra: bạn bắt đầu thiết kế code để dễ dàng test!. May mắn thay, coding theo cách này cũng có nghĩa bạn tạo code tốt hơn thông thường. Thiết kế Test-friendly thường 1 cách tự nhiên sẽ dẫn đến code tổ chức tốt hơn, với các phần riêng biệt làm những thứ riêng biệt.

TEST-DRIVEN DEVELOPMENT
Test-driven development (TDD) là cách lập trình khi mà bạn sẽ xây dựng các tests trước khi bạn viết code thực tế. Những người đề xuất TDD tin tưởng rằng quá trình này cải thiện sâu sắc chất lượng các đoạn nontest code hơn nhiều so với nếu bạn viết các tests sau khi viết code.

Đây là 1 chủ đề tranh luận sôi nổi mà chúng tôi không đi sâu vào. Ít nhất, chúng tôi đã tìm thấy rằng, chỉ cần giữ các testing trong khi viết code sẽ giúp code tốt hơn.

Nhưng mà bất kể cho bạn triển khai TDD, kết quả cuối cùng là bạn có code để test các code khác. Mục đích của chương này là giúp bạn tạo các test dễ đọc và dễ viết.

Trong tất cả các cách để chia 1 chương trình vào trong lớp và phương thức, những cách tách rời nhất thường dễ test nhất. Nói 1 cách khác, hãy nói chương trình của bạn rất kết nối với nhau, với 1 vào phương thức được gọi giữa các lớp và nhiều tham số cho tất cả các phương thức. Chương trình đó không chỉ có code khó hiểu mà test code cũng xấu xí và khó đọc, khó viết.

Có nhiều thành phần “bên ngoài” (các biến global cần được khởi tạo, thư viện hoặc file cấu hình cần được load) cùng làm nó khó chịu hơn để viết tests.

Nói chung, nếu bạn đang thiết kế code của bạn và nhận ra Hmm, thật là 1 cơn ác mộng để test, đây là 1 lý do tốt để dừng và suy nghĩa lại các thiết kế. Bảng 14-1 chỉ ra 1 vài các testing điển hình và các vấn đề về thiết kế.

TABLE 14-1. Characteristics of less testable code, and how this leads to problems with design

Characteristic Testability problem Design problem
Use of global variables All the global state needs to reset for every test (otherwise, different tests can interfere with each other). Hard to understand which functions have what side effects. Can’t think about each function in isolation; need to consider the whole program to understand if everything works.
Code depends on a lot of external components It’s harder to write any tests because there’s so much scaffolding to set up first. Tests are less fun to write, so people avoid writing tests. System is more likely to fail when one of the dependencies fails. It’s harder to understand what impact any given change might make. It’s harder to refactor classes. System has more failure modes and recovery paths to think about.
Code has nondeterministic behavior Tests are flaky and unreliable. Tests that occasionally fail end up being ignored. The program is more likely to have race conditions or other nonreproducible bugs. The program is harder to reason about. Bugs in production are very difficult to track down and fix

Trong 1 chiều hướng khác, nếu bạn có 1 thiết kế code dễ dàng để viết test, đó là 1 dấu hiệu tốt. Bảng 14-2 chỉ ra 1 vài lợi ích của testing và các đặc điểm thiết kế.

TABLE 14-2. Characteristics of more testable code, and how this leads to good design

Characteristic Testability problem Design problem
Classes have little or no internal state Tests are easier to write because there is less setup needed to test a method and less hidden state to inspect. Classes with less state are simpler and easier to understand.
Classes/functions only do one thing Fewer test cases are required to fully test it. Smaller/simpler components are more modular, and the system is generally more decoupled.
Classes depend on few other classes; high decoupling Each class can be tested independently (much easier than testing multiple classes at once). System can be developed in parallel. Classes can be easily modified or removed without disrupting the rest of the system
Functions have simple, well-defined interfaces There are well-defined behaviors to test for. Simple interfaces take less work to test. Interfaces are easier for coders to learn and are more likely to be reused.

Going Too Far

Nó cũng có thể tập trung quá nhiều thứ trong test. Đây là 1 vài ví dụ:

  • Hi sinh khả năng đọc real code để cho phép test. Thiết kế real code để có thể test được sẽ là vấn đề win-win: real code trở nên đơn giản và nhiều phần tách rời và các test của bạn cũng dễ viết. Nhưng nếu bạn phải chèn nhiều các đường ống xấu xí vào real code để bạn có thể test được, thì có gì đó sai sai ở đây.
  • Bị ám ảnh về 100% phạm vi test. Testing lần đầu 90% code của bạn thường làm việc ít hơn 10% cuối. 10% cuối có thể liên quan đến giao diện người dùng, hoặc đưa ra các trường hợp lỗi, mà cái giá của bug không thực sự code và các nỗ lực để test nó cũng không đáng.

Sự thật là bạn không bao giờ có phạm vi test 100%. Nếu nó không phải là 1 lỗi bị bỏ qua, nó có thể là 1 chức năng bị bỏ qua hoặc bạn không nhận ra được rằng spec đã được thay đổi.

Tùy thuộc vào lỗi của bạn tốn kém như thế nào, sẽ có điểm thú vị về thời gian phát triển đáng kể để dành cho test code. Nếu bạn đang xây dựng nguyên mẫu 1 trang website, nó có thể không đáng để viết 1 vài test code nào cả. Mặt khác, nếu bạn đang viết 1 bộ điều khiển cho tàu vũ trụ hoặc các thiết bị y tế, testing có thể là mục tiêu chính của bạn.

  • Hãy để testing có trong cách phát triển sản phẩm. Chúng ta đã thấy các tình huống trong đó thử nghiệm, vốn chỉ là một khía cạnh của dự án, chi phối toàn bộ dự án. Thử nghiệm trở thành một loại tinh thần được xoa dịu, và các lập trình viên chỉ trải qua các nghi thức và chuyển động mà không nhận ra rằng thời gian kỹ thuật quý giá của họ có thể được chi tiêu tốt hơn ở nơi khác.

Summary

Trong test code, khả năng đọc là rất quan trọng. Nếu test của bạn dễ đọc, chúng sẽ dễ viết, mọi người sẽ thêm được nhiều. Cũng như vậy, nếu bạn thiết kế real code dễ test, code của bạn sẽ có 1 thiết kế tổng thể tốt hơn.

Đây là 1 vài điểm để cải thiện test của bạn:

  • Ở mức cao nhất của mỗi test nên ngắn gọn nhất có thể, lý tưởng nhất, mỗi test input/ouput có thể mô tả trong 1 dòng code.
  • Nếu test của bạn lỗi, nó nên bắn ra 1 thông điệp lỗi làm cho lỗi dễ dàng tìm ra và sửa
  • Sử dụng các input test đơn giản nhất kiểm tra code của bạn
  • Tên các hàm test cần đầy đủ mô tả và rõ ràng cho mỗi test. Thay vì tên Test1(), sử dụng 1 cái tên như Test_<FunctionName>_<Situation>.

Và trên hết, làm cho việc sửa đổi và thêm 1 test dễ dàng.

Author

Ming

Posted on

2020-05-24

Updated on

2024-08-08

Licensed under

Comments