Advertisement
  1. Code
  2. Coding Fundamentals
  3. Testing

TDD một ứng dụng đơn giản trong PHP

Scroll to top
Read Time: 22 min
This post is part of a series called Test-Driven PHP.
Automatic Testing for TDD with PHP
Deciphering Testing Jargon

() translation by (you can also view the original English article)

Trong hướng dẫn này, tôi sẽ trình bày một ví dụ từ bắt đầu đến kết thúc của một ứng dụng đơn giản - hoàn toàn được thực hiện với TDD trong PHP. Tôi sẽ hướng dẫn bạn từng bước một, đồng thời giải thích các quyết định tôi đưa ra để hoàn thành tác vụ. Ví dụ bám sát các quy tắc của TDD: viết test, code và refactor.


Bước 1 - Giới thiệu về TDD & PHPUnit

Phát triển dựa trên test (TDD)

TDD là một kỹ thuật "test trước" để phát triển và thiết kế phần mềm. Nó hầu như luôn được sử dụng trong các nhóm agile, là một trong những công cụ quan trọng của phát triển phần mềm nhanh. TDD lần đầu tiên được Kent Beck định nghĩa và giới thiệu với cộng đồng chuyên nghiệp vào năm 2002. Kể từ đó, nó đã trở thành một kỹ thuật được chấp nhận - và được khuyến dùng - trong lập trình hàng ngày.

TDD có ba quy tắc cốt lõi:

  1. Bạn không được phép viết bất kỳ production code nào, nếu không có test thất bại để chứng minh nó.
  2. Bạn không được phép viết nhiều unit test hơn mức cần thiết để khiến nó thất bại. Không biên dịch hoặc không chạy là thất bại.
  3. Bạn không được phép viết nhiều unit test hơn mức cần thiết để khiến nó thất bại.

PHPUnit

PHPUnit là công cụ cho phép các lập trình viên PHP thực hiện unit test và thực hành phát triển dựa trên test (test-driven). Nó là một framework hoàn chỉnh cho unit test với sự hỗ trợ của mock. Mặc dù có vài lựa chọn thay thế, nhưng PHPUnit là giải pháp được sử dụng nhiều nhất và đầy đủ nhất cho PHP hiện nay.

Để cài đặt PHPUnit, bạn có thể làm theo hướng dẫn trước đó trong phần "TDD in PHP" của chúng tôi hoặc bạn có thể sử dụng PEAR, được giải thích trong tài liệu chính thức:

  • sử dụng root hoặc dùng sudo
  • đảm bảo bạn cài PEAR mới nhất: pear upgrade PEAR
  • cho phép tự động phát hiện: pear config-set auto_discover 1
  • cài đặt PHPUnit: pear install pear.phpunit.de/PHPUnit

Thông tin và hướng dẫn bổ sung cài đặt thêm các mô-đun PHPUnit có thể được tìm thấy trong tài liệu chính thức.

Vài bản phân phối Linux cung cấp phpunit dưới dạng package được biên dịch sẵn, dù tôi luôn khuyên bạn nên cài đặt, thông qua PEAR, vì nó đảm bảo rằng phiên bản mới nhất và cập nhật nhất được cài đặt và sử dụng.

NetBeans & PHPUnit

Nếu bạn là fan của NetBeans, bạn có thể định cấu hình nó để hoạt động với PHPUnit bằng cách thực hiện theo các bước sau:

  • Chuyển đến cấu hình của NetBeans (Tools/Options)
  • Chọn PHP/PHPUnit
  • Kiểm tra xem entry point "PHPUnit Script" trỏ đến file thực thi PHPUnit hợp lệ. Nếu không, NetBeans sẽ cho bạn biết điều này, vì vậy nếu bạn không thấy bất kỳ thông báo màu đỏ nào trên trang, thì bạn đã sẵn sàng. Nếu không, hãy tìm PHPUnit thực thi trên hệ thống của bạn và nhập đường dẫn của nó vào trường input. Đối với các hệ thống Linux, đường dẫn này thường là /usr/bin/phpunit.

Nếu bạn không sử dụng IDE có hỗ trợ unit test, bạn thì luôn có thể chạy test trực tiếp từ console:

1
2
cd /my/applications/test/folder
3
phpunit

Bước 2 - Vấn đề cần giải quyết

Nhóm của chúng tôi được giao tác vụ triển khai tính năng "word wrap".

Giả sử rằng chúng ta là một phần của một tập đoàn lớn, có một ứng dụng tinh vi cần phát triển và bảo trì. Nhóm của chúng tôi được giao nhiệm vụ triển khai tính năng "word wrap". Khách hàng của chúng tôi không muốn thấy các thanh cuộn ngang và nhiệm vụ của chúng tôi là thực hiện.

Trong trường hợp đó, chúng ta cần tạo một class có khả năng định dạng một bit văn bản tùy ý được nhập vào. Kết quả phải là từ được word wrap ở một số lượng ký tự được chỉ định. Các quy tắc word wrap phải tuân theo hành vi của các ứng dụng hàng ngày khác, như text editor, textarea trên trang web, v.v. Khách hàng của chúng tôi không hiểu tất cả các quy tắc word wrap, nhưng họ biết họ muốn nó và họ biết rằng nó sẽ hoạt động giống như cách họ đã trải nghiệm trong các ứng dụng khác.


Bước 3 - Lập kế hoạch

TDD giúp bạn đạt được một thiết kế tốt hơn, nhưng nó không loại bỏ nhu cầu thiết kế và tư duy đi trước.

Một trong những điều mà nhiều lập trình viên quên, sau khi họ bắt đầu TDD, là suy nghĩ và lên kế hoạch trước. TDD giúp bạn đạt được một thiết kế tốt hơn, với ít code và chức năng được xác minh, nhưng nó không loại bỏ nhu cầu thiết kế trước và suy nghĩ của con người.

Mỗi khi bạn cần giải quyết một vấn đề, bạn nên dành thời gian để suy nghĩ về nó, để hình dung ra một thiết kế nhỏ - không có gì đặc biệt - nhưng đủ để bạn bắt đầu. Phần này của công việc cũng giúp bạn hình dung và đoán các tình huống có thể xảy ra đối với logic của ứng dụng.

Chúng ta hãy nghĩ về các quy tắc cơ bản cho một tính năng word wrap. Tôi cho rằng một số văn bản chưa được wrap (bao bọc) sẽ được mang đến cho chúng tôi. Chúng tôi sẽ biết số lượng ký tự trên mỗi dòng và chúng tôi sẽ muốn nó được wrap. Vì vậy, điều đầu tiên tôi nghĩ đến là, nếu văn bản có nhiều ký tự hơn số lượng trên một dòng, chúng ta nên thêm một dòng mới thay vì ký tự khoảng trắng cuối cùng vẫn còn trên dòng.

Được rồi, điều này sẽ tổng hợp hành vi của hệ thống, nhưng nó quá phức tạp đối với bất kỳ test nào. Ví dụ, sẽ ra sao nếu một từ dài hơn con số ký tự được cho phép trên một dòng? Hmmm ... nó trông giống như một trường hợp bất khả thi; chúng ta không thể thay thế một khoảng trắng bằng một dòng mới vì chúng ta không có khoảng trắng trên dòng đó. Chúng ta sẽ buộc phải wrap từ đó, chia ra nó làm hai phần.

Những ý tưởng này phải đủ rõ ràng để chúng ta có thể bắt đầu lập trình. Chúng tôi sẽ cần một dự án và một class. Hãy gọi nó là Wrapper.


Bước 4 - Bắt đầu dự án và tạo test đầu tiên

Hãy tạo dự án của chúng tôi. Cần có một thư mục chính cho các class nguồn và một thư mục Tests/ cho các test.

File đầu tiên chúng tôi sẽ tạo là một test trong thư mục Tests. Tất cả các test sắp tới của chúng tôi sẽ được chứa trong thư mục này, vì vậy tôi sẽ không xác định rõ ràng một lần nữa trong hướng dẫn này. Đặt tên cho test class có tính mô tả, nhưng đơn giản. WrapperTest sẽ làm phù hợp; test đầu tiên của chúng tôi trông giống như thế này:

1
2
require_once dirname(__FILE__) . '/../Wrapper.php';
3
4
class WrapperTest extends PHPUnit_Framework_TestCase {
5
6
  function testCanCreateAWrapper() {
7
		$wrapper = new Wrapper();
8
	}
9
10
}

Hãy nhớ lại! Chúng tôi không được phép viết bất kỳ production code nào trước khi test thất bại - thậm chí là khai báo class! Đó là lý do tại sao tôi đã viết test đơn giản đầu tiên ở trên, được gọi là canCreateAWrapper. Một số người coi bước này là vô ích, nhưng tôi coi đó là một cơ hội tốt để suy nghĩ về class mà chúng tôi sẽ tạo ra. Chúng ta có cần một class không? Chúng ta nên gọi nó là gì? Có nên static không?

Khi bạn chạy test ở trên, bạn sẽ nhận được thông báo Fatal Error, như sau:

1
2
PHP Fatal error:  require_once(): Failed opening required '/path/to/WordWrapPHP/Tests/../Wrapper.php' (include_path='.:/usr/share/php5:/usr/share/php') in /path/to/WordWrapPHP/Tests/WrapperTest.php on line 3

Rất tiếc! Chúng ta nên làm điều gì đó. Tạo một class Wrapper trống trong thư mục chính của dự án.

1
2
class Wrapper {}

Thế đấy. Nếu bạn chạy test một lần nữa, nó sẽ thành công. Chúc mừng test đầu tiên của bạn!


Bước 5 - Test thực tế đầu tiên

Vậy chúng tôi có dự án được khởi động, giờ chúng ta cần suy nghĩ về test thực sự đầu tiên của mình.

Điều đơn giản nhất ... ngu ngốc nhất ... mà test cơ bản nhất sẽ khiến production code hiện tại của chúng ta thất bại? Chà, điều đầu tiên xuất hiện trong đầu là "Hãy đưa ra một từ đủ ngắn và mong đợi kết quả sẽ không thay đổi." Điều này có vẻ khả thi; Hãy viết test.

1
2
require_once dirname(__FILE__) . '/../Wrapper.php';
3
4
class WrapperTest extends PHPUnit_Framework_TestCase {
5
6
	function testDoesNotWrapAShorterThanMaxCharsWord() {
7
		$wrapper = new Wrapper();
8
		assertEquals('word', $wrapper->wrap('word', 5));
9
	}
10
11
}

Điều đó có vẻ khá phức tạp. "MaxChars" trong tên hàm có nghĩa là gì? 5 trong phương thức wrap nói đến điều gì?

Tôi nghĩ rằng có gì không hẳn đúng ở đây. Không có test đơn giản hơn mà chúng ta có thể chạy? Vâng, chắc chắn là có! Điều gì xảy ra nếu chúng ta wrap ... không có gì - một chuỗi trắng? Điều đó nghe có vẻ tốt. Xóa test phức tạp ở trên và thay vào đó, hãy thêm test mới, đơn giản hơn, như bên dưới:

1
2
require_once dirname(__FILE__) . '/../Wrapper.php';
3
4
class WrapperTest extends PHPUnit_Framework_TestCase {
5
6
	function testItShouldWrapAnEmptyString() {
7
		$wrapper = new Wrapper();
8
		$this->assertEquals('', $wrapper->wrap(''));
9
	}
10
11
}

Nó tốt hơn nhiều. Tên của test rất dễ hiểu, chúng tôi không có chuỗi hoặc con số tinh vi, và hầu hết, THẤT BẠI!

1
2
Fatal error: Call to undefined method Wrapper::wrap() in ...

Như bạn quan sát, tôi đã xóa test đầu tiên của chúng tôi. Thật vô ích khi kiểm tra một cách rõ ràng nếu có một đối tượng có thể được khởi tạo, khi các test khác cũng cần nó. Điều này là bình thường. Lâu dần bạn sẽ thấy rằng việc xóa các test là một điều bình thường. Các test, đặc biệt là unit test, phải chạy nhanh - thực sự nhanh ... và thường xuyên - rất thường xuyên. Cân nhắc điều này, việc loại bỏ phần dư thừa trong các test rất quan trọng. Hãy hình dung rằng bạn chạy hàng ngàn test mỗi khi bạn lưu dự án. Tối đa sẽ mất không quá một vài phút để chúng chạy. Vậy đừng e ngại khi xóa một test, nếu cần thiết.

Quay trở lại production code của chúng tôi, hãy thực hiện test đó:

1
class Wrapper {
2
3
	function wrap($text) {
4
		return;
5
	}
6
7
}

Ở trên, chúng tôi hoàn toàn không bổ sung thêm code hơn mức cần thiết để thành công test.


Step 6 - nhấn vào

Bây giờ, cho test thất bại tiếp theo:

1
	function testItDoesNotWrapAShortEnoughWord() {
2
		$wrapper = new Wrapper();
3
		$this->assertEquals('word', $wrapper->wrap('word', 5));
4
	}

Thông báo thất bại:

1
Failed asserting that null matches expected 'word'.

Và code làm giúp nó thành công:

1
	function wrap($text) {
2
		return $text;
3
	}

Ồ! Thật dễ dàng dàng phải không?

Trong khi chúng ta ở màu xanh lá cây, hãy quan sát rằng test code của chúng ta có thể bắt đầu vô lý. Chúng ta cần cấu trúc lại một vài thứ. Nhớ rằng: luôn refactor khi các test mà bạn thành công; đây là cách duy nhất mà bạn có thể chắc chắn rằng bạn đã refactor chính xác.

Trước tiên, hãy loại bỏ phần trùng lặp của việc khởi tạo đối tượng trình wrapper. Chúng ta chỉ có thể làm điều này một lần trong phương thức setUp() và sử dụng nó cho cả hai test.

1
class WrapperTest extends PHPUnit_Framework_TestCase {
2
3
	private $wrapper;
4
5
	function setUp() {
6
		$this->wrapper = new Wrapper();
7
	}
8
9
	function testItShouldWrapAnEmptyString() {
10
		$this->assertEquals('', $this->wrapper->wrap(''));
11
	}
12
13
	function testItDoesNotWrapAShortEnoughWord() {
14
		$this->assertEquals('word', $this->wrapper->wrap('word', 5));
15
	}
16
17
}

Phương pháp setup sẽ chạy trước mỗi test mới.

Tiếp theo, có một số bit mơ hồ trong test thứ hai. 'Word' là gì? '5' là gì? Chúng ta hãy làm rõ để các lập trình viên tiếp theo đọc các test này không phải đoán.

Không bao giờ quên rằng các test của bạn cũng là tài liệu cập mới nhất cho code của bạn.

Một lập trình viên khác có thể đọc các test dễ dàng giống như họ đọc tài liệu.

1
	function testItDoesNotWrapAShortEnoughWord() {
2
		$textToBeParsed = 'word';
3
		$maxLineLength = 5;
4
		$this->assertEquals($textToBeParsed, $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Bây giờ, đọc phần khẳng định này một lần nữa. Điều đó không đọc tốt hơn sao? Tất nhiên nó tốt hơn. Đừng sợ tên biến dài cho các test của bạn; auto-completion là bạn của bạn! Tốt hơn hết là càng mô tả càng tốt.

Bây giờ, cho test thất bại tiếp theo:

1
	function testItWrapsAWordLongerThanLineLength() {
2
		$textToBeParsed = 'alongword';
3
		$maxLineLength = 5;
4
		$this->assertEquals("along\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Và code giúp cho nó thành công:

1
	function wrap($text, $lineLength) {
2
		if (strlen($text) > $lineLength)
3
			return substr ($text, 0, $lineLength) . "\n" . substr ($text, $lineLength);
4
		return $text;
5
	}

Đó là code rõ ràng để thực hiện test cuối cùng của chúng tôi. Nhưng hãy cẩn thận - đó cũng là code khiến test đầu tiên của chúng tôi không thành công!

Chúng tôi có hai lựa chọn để khắc phục vấn đề này:

  • sửa đổi code - làm cho tham số thứ hai trở thành tùy chọn
  • sửa đổi test đầu tiên - và làm cho nó gọi code với một tham số

Nếu bạn theo chọn lựa đầu tiên, làm cho tham số tro83 thành tùy chọn, điều đó sẽ gây ra chút vấn đề với code hiện tại. Một tham số tùy chọn cũng được khởi tạo với giá trị mặc định. Giá trị như vậy có thể là gì? 0 có vẻ hợp lý, nhưng nó sẽ ám chỉ việc viết code chỉ để xử lý trường hợp đặc biệt đó. Thiết lập một số rất lớn, sao cho câu lệnh if đầu tiên không có kết quả đúng có thể là một giải pháp khác. Nhưng, con số đó là gì? Có phải là 10 không? Có phải 10000 không? Có phải 10000000 không? Thực sự không thể biết.

Cân nhắc tất cả những điều này, tôi sẽ chỉ sửa đổi test đầu tiên:

1
	function testItShouldWrapAnEmptyString() {
2
		$this->assertEquals('', $this->wrapper->wrap('', 0));
3
	}

Một lần nữa, tất cả đều xanh. Bây giờ chúng ta có thể chuyển sang test tiếp theo. Hãy bảo đảm rằng nếu chúng ta có một từ rất dài, nó sẽ nằm trên nhiều dòng.

1
	function testItWrapsAWordSeveralTimesIfItsTooLong() {
2
		$textToBeParsed = 'averyverylongword';
3
		$maxLineLength = 5;
4
		$this->assertEquals("avery\nveryl\nongwo\nrd", $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Điều này rõ ràng là thất bại, bởi vì production code thực tế của chúng tôi chỉ có một dòng.

1
2
Failed asserting that two strings are equal.
3
--- Expected
4
+++ Actual
5
@@ @@
6
 'avery

7
-veryl

8
-ongwo

9
-rd'
10
+verylongword'

Bạn có thể đoán ra vòng lặp while sắp xuất hiện không? Vâng, hãy nghĩ lại. Có phải vòng lặp while là code đơn giản nhất giúp test thành công không?

Theo "Transformation Priorities" (của Robert C. Martin), thì không. Phép đệ quy luôn đơn giản hơn một vòng lặp và nó dễ test hơn nhiều.

1
	function wrap($text, $lineLength) {
2
		if (strlen($text) > $lineLength)
3
			return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength);
4
		return $text;
5
	}

Bạn thậm chí có thể phát hiện ra sự thay đổi? Đó là một thay đổi đơn giản. Tất cả những gì chúng ta đã làm, thay vì nối với phần còn lại của chuỗi, chúng ta nối với giá trị trả về với phần còn lại của chuỗi. Hoàn hảo!


Bước 7 - Chỉ hai từ

Test đơn giản tiếp theo? Hai từ có thể wrap thì sao, khi nào có khoảng trắng ở cuối dòng.

1
	function testItWrapsTwoWordsWhenSpaceAtTheEndOfLine() {
2
		$textToBeParsed = 'word word';
3
		$maxLineLength = 5;
4
		$this->assertEquals("word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Điều đó hoàn toàn phù hợp. Tuy nhiên, lần này giải pháp có thể trở nên phức tạp hơn một chút.

Lúc đầu, bạn có thể tham khảo hàm str_replace() để loại bỏ khoảng trắng và chèn vào một dòng mới. Đừng làm vậy; cách đó dẫn đến ngõ cụt.

Chọn lựa tất yếu thứ hai sẽ là câu lệnh if. Kiểu như thế này:

1
	function wrap($text, $lineLength) {
2
		if (strpos($text,' ') == $lineLength)
3
			return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength);
4
		if (strlen($text) > $lineLength)
5
			return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength);
6
		return $text;
7
	}

Tuy nhiên, chọn lựa đó đi vào một vòng lặp vô tận, và sẽ khiến các test bị lỗi.

1
PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted

Lần này, chúng ta cần suy nghĩ! Vấn đề là test đầu tiên của chúng tôi có một văn bản có độ dài bằng 0. Ngoài ra, strpos() trả về false khi không thể tìm thấy chuỗi. So sánh false với 0 ... là? Đó là true. Điều này không tốt cho chúng tôi vì vòng lặp sẽ trở thành vô hạn. Giải pháp? Hãy thay đổi điều kiện đầu tiên. Thay vì tìm kiếm một khoảng trắng và so sánh vị trí của nó với độ dài của dòng, thay vào đó, hãy thử trực tiếp lấy ký tự ở vị trí được chỉ định bởi độ dài của dòng. Chúng tôi sẽ thực hiện substr() chỉ lấy một ký tự, bắt đầu từ đúng điểm bên phải của văn bản.

1
	function wrap($text, $lineLength) {
2
		if (substr($text, $lineLength - 1, 1) == ' ')
3
			return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength);
4
		if (strlen($text) > $lineLength)
5
			return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength);
6
		return $text;
7
	}

Nhưng, nếu khoảng trắng không ở cuối dòng thì sao?

1
	function testItWrapsTwoWordsWhenLineEndIsAfterFirstWord() {
2
		$textToBeParsed = 'word word';
3
		$maxLineLength = 7;
4
		$this->assertEquals("word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Hmm ... chúng ta phải điều chỉnh lại các điều kiện của mình một lần nữa. Tôi nghĩ rằng, rốt cuộc, chúng ta sẽ cần tìm kiếm vị trí của ký tự khoảng trắng.

1
	function wrap($text, $lineLength) {
2
		if (strlen($text) > $lineLength) {
3
			if (strpos(substr($text, 0, $lineLength), ' ') != 0)
4
				return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength);
5
			return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength);
6
		}
7
		return $text;
8
	}

Ồ Điều đó thực sự hiệu quả. Chúng tôi đã đưa điều kiện đầu tiên vào trong điều kiện thứ hai để tránh vòng lặp vô tận và chúng tôi đã bổ sung phần tìm kiếm khoảng trắng. Tuy nhiên, trông nó khá xấu xí. Các điều kiện lồng vào nhau? Kinh quá. Đã đến lúc refactor (tái cấu trúc).

1
	function wrap($text, $lineLength) {
2
		if (strlen($text) <= $lineLength)
3
			return $text;
4
		if (strpos(substr($text, 0, $lineLength), ' ') != 0)
5
			return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength);
6
		return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength);
7
	}

Điều đó tốt hơn tốt hơn.


Bước 8 - Nhiều từ thì thế nào?

Kết quả của việc viết test không có gì là xấu.

Test đơn giản tiếp theo sẽ là có ba từ nằm trên ba dòng. Nhưng test đó đã hoàn thành. Bạn có nên viết một test khi bạn biết nó sẽ thành công? Đa số là không. Nhưng, nếu bạn có nghi ngờ, hoặc bạn có thể hình dung những thay đổi rõ ràng đối với code khiến cho cho test mới thất bại và những người khác thành công, thì hãy viết nó! Kết quả của việc viết test không có gì là xấu. Ngoài ra, hãy xem các test là tài liệu của bạn. Nếu test của bạn đại diện cho một phần thiết yếu trong logic của bạn, thì hãy viết nó!

Hơn nữa, thực tế các test mà chúng tôi đưa ra đang thành công là một dấu hiệu cho thấy chúng tôi đang tiến gần đến một giải pháp. Rõ ràng, khi bạn có một thuật toán hiệu quả, bất kỳ test nào của chúng tôi viết sẽ thành công.

Bây giờ - 3 từ trên hai dòng với kết thúc dòng bên trong từ cuối cùng; Bây giờ, điều đó thất bại.

1
	function testItWraps3WordsOn2Lines() {
2
		$textToBeParsed = 'word word word';
3
		$maxLineLength = 12;
4
		$this->assertEquals("word word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Tôi gần như mong đợi nó sẽ hiệu quả. Khi chúng tôi điều tra lỗi, chúng tôi nhận:

1
Failed asserting that two strings are equal.
2
--- Expected
3
+++ Actual
4
@@ @@
5
-'word word

6
-word'
7
+'word

8
+word word'

Vâng. Chúng ta nên wrap ở khoảng trắng ngoài cùng bên phải trong một dòng.

1
	function wrap($text, $lineLength) {
2
		if (strlen($text) <= $lineLength)
3
			return $text;
4
		if (strpos(substr($text, 0, $lineLength), ' ') != 0)
5
			return substr ($text, 0, strrpos($text, ' ')) . "\n" . $this->wrap(substr($text, strrpos($text, ' ') + 1), $lineLength);
6
		return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength);
7
	}

Đơn giản chỉ cần thay thế strpos() bằng strrpos() bên trong câu lệnh if thứ hai.


Bước 9 - Các test thất bại khác thì sao? Trường hợp bất khả thi?

Mọi thứ đang trở nên khó khăn hơn. Khá vất vả để tìm một test thất bại ... hoặc bất kỳ test nào, đối với vấn đề đó, vẫn chưa được viết.

Đây là một dấu hiệu cho thấy chúng ta tiến khá gần với một giải pháp cuối cùng. Nhưng, này, tôi chỉ nghĩ về một test sẽ thất bại!

1
	function testItWraps2WordsOn3Lines() {
2
		$textToBeParsed = 'word word';
3
		$maxLineLength = 3;
4
		$this->assertEquals("wor\nd\nwor\nd", $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Nhưng, tôi đã sai. Nó thành công. Hmm ... chúng ta xong chưa nhỉ? Chờ đã! Cái này thì sao?

1
	function testItWraps2WordsAtBoundry() {
2
		$textToBeParsed = 'word word';
3
		$maxLineLength = 4;
4
		$this->assertEquals("word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength));
5
	}

Thất bại! Xuất sắc. Khi dòng có cùng độ dài với từ, chúng tôi muốn dòng thứ hai không bắt đầu bằng khoảng trắng.

1
Failed asserting that two strings are equal.
2
--- Expected
3
+++ Actual
4
@@ @@
5
 'word

6
-word'
7
+ wor
8
+d'

Có vài giải pháp. Chúng tôi có thể giới thiệu một câu lệnh if khác để kiểm tra nếu có khoảng trắng ở bắt đầu. Điều đó sẽ phù hợp với phần còn lại của các điều kiện mà chúng tôi đã tạo ra. Nhưng, không có một giải pháp đơn giản hơn sao? Sẽ ra sao nếu chúng ta dùng trim() để cắt văn bản?

1
	function wrap($text, $lineLength) {
2
		$text = trim($text);
3
		if (strlen($text) <= $lineLength)
4
			return $text;
5
		if (strpos(substr($text, 0, $lineLength), ' ') != 0)
6
			return substr ($text, 0, strrpos($text, ' ')) . "\n" . $this->wrap(substr($text, strrpos($text, ' ') + 1), $lineLength);
7
		return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength);
8
	}

Đây rồi.


Bước 10 - Chúng tôi đã hoàn thành

Tại thời điểm này, tôi không thể phát minh ra bất kỳ test thất bại nào để viết. Chúng ta đã hoàn tất! Bây giờ chúng ta đã sử dụng TDD để xây dựng một thuật toán sáu dòng đơn giản nhưng hữu ích.

Một vài từ về việc dừng lại và "hoàn tất". Nếu bạn sử dụng TDD, bạn buộc mình phải suy nghĩ về tất cả các tình huống. Sau đó, bạn viết test cho những tình huống đó, và trong quá trình đó, bắt đầu hiểu vấn đề tốt hơn nhiều. Thông thường, quá trình này mang đến kiến thức sâu về thuật toán. Nếu bạn không thể nghĩ ra bất kỳ test thất bại nào khác để viết, có phải điều này nghĩa là thuật toán của bạn hoàn hảo? Không nhất thiết, trừ khi có một bộ quy tắc được xác định trước. TDD không đảm bảo code không có lỗi; nó chỉ giúp bạn viết code tốt hơn để có thể hiểu và thay đổi tốt hơn.

Thậm chí tốt hơn, nếu bạn phát hiện ra một lỗi, việc viết một test tái tạo lỗi đó sẽ dễ dàng hơn nhiều. Bằng cách này, bạn có thể đảm bảo rằng lỗi không bao giờ xảy ra nữa - vì bạn đã kiểm tra nó!


Chú ý sau cùng

Bạn có thể tranh cãi rằng quy trình này không phải là "TDD". Và bạn đã đúng! Ví dụ này sát với công việc hằng ngày của nhiều lập trình viên. Nếu bạn muốn có một ví dụ "TDD đúng như ý bạn", vui lòng để lại bình luận bên dưới và tôi sẽ lên kế hoạch cho bài viết sắp tới.

Cảm ơn vì đã đọc bài viết!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.