1. Trang chủ
  2. » Công Nghệ Thông Tin

PHP in Action phần 5 potx

55 240 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 55
Dung lượng 810,18 KB

Nội dung

DATABASE SELECT 195 The test plays the part of a typical piece of client code. By writing the test first, we get to play with the interface of our class a little before we commit to it. In effect, we get to try it out first in the test. We already have an empty test class called TestOfMysqlTransaction. Each individ- ual test will be implemented as a method in the test class. Here is our first real test: require_once(' /transaction.php'); class TestOfMysqlTransaction extends UnitTestCase { function testCanReadSimpleSelect() { b $transaction = new MysqlTransaction(); $result = $transaction->select('select 1 as one'); $row = $result->next(); $this->assertEqual($row['one'], 1); d } } b SimpleTest does some magic here. When the test case executes, it searches itself for all methods that start with “test” and runs them. If the method starts with any other name, it will be skipped. We’ll make use of this later, but for now just remember to put “test” at the beginning of each method you want to run. C Now we start pretending that the feature has been implemented as outlined in figure 9.2. “Select” sounds like a good name for an SQL select method. We pretend that the transaction class has a select() method that is able to run an SQL SELECT. We also pretend that the results of the select() call will come back as an iterator (see section 7.5). Each call to next() on the iterator will give us a row as a PHP array(). Here we only expect to fetch one row, so the usual iterator loop is absent. D The assertEqual() method is a SimpleTest assertion, one of quite a few avail- able. If the two parameters do not match up, a failure message will be dispatched to the test reporter and we will get a big red bar. Figure 9.3 is a simplified class diagram of the test setup. The MysqlTransaction and MysqlResult classes are in gray because they don’t exist yet. They are implied by the code in the test method. The UnitTestCase class is part of the SimpleTest framework. Only one method of this class is shown, although it has many others. When we run this test case, we don’t get to see the red bar. Instead the results are quite spectacular, as in figure 9.4. We haven’t yet created the file classes/transaction.php, causing a crash. This is because we are writing the tests before we write the code, any code, even creating the file. Why? Because we want the least amount of code that we can get away with. It’s easy to make assumptions about what you will need and miss a much simpler solution. c 196 CHAPTER 9 TEST-DRIVEN DEVELOPMENT 9.2.3 Make it pass The test result tells us what we need to do next. It’s telling us that it’s unable to open the file transaction.php . This is not surprising, since the file does not exist. We have to create the file. If we create an empty transaction.php file and run the test again, it will tell us that the MysqlTransaction class does not exist. If we create the class, we get another fatal error telling us that we are trying to run a nonexistent method. This process leads us to the following code, the minimum needed to avoid a fatal PHP error: <?php class MysqlTransaction { function select() { return new MysqlResult(); } } Figure 9.3 Our first real test, the infrastructure needed to make it work, and the classes implied by the test Figure 9.4 Now the test causes a fatal er- ror, since we have a test, but the code to be tested does not exist yet. DATABASE SELECT 197 class MysqlResult { function next() { } } ?> It isn’t fully functional, but does prevent a PHP crash. The output is in figure 9.5. It takes only a single failure to get that big red bar. That’s the way it works. This might seem brutal, but there are no partially passing test suites, in the same way as there is no such thing as partially correct code. The only way to get the green bar back is with 100 percent passing tests. We can achieve a green bar simply by returning the correct row: class MysqlResult { function next() { return array('one' => '1'); } } And sure enough, we get the green bar (see figure 9.6). Notice the small steps: write a line, look at the tests, write a line, check whether it’s green. Did we just cheat by simply hard-coding the desired result? Well, yes we did. This is what Kent Beck, the inventor of TDD, calls the FakeIt pattern. We will find it’s easier to work with code when we have a green bar. For this reason, we get to the green bar any way we can, even if it’s a simplistic, stupid, fake implementation. Once green, we can refactor the code to the solution we really want. In a way, the code is actually correct despite our hack. It works; it just doesn’t meet any real user requirements. Any other developer looking at the tests might be a bit dis- appointed when she sees our current implementation, but it’s pretty obvious that we Figure 9.5 The test case no longer crashes, but the test fails since the code is not fully functional yet. Figure 9.6 We've made the test pass by hard-coding the output of the desired result. 198 CHAPTER 9 TEST-DRIVEN DEVELOPMENT have done a temporary hack. If we were run over by a bus, she could carry on from this point without confusion. All code is a work in progress, and in a way this is no different. 9.2.4 Make it work Since we weren’t run over by a bus and we’re still alive, it’s still our job to write some more code. We want to go from the fake implementation to code that actually does something useful. Instead of just returning a hard-coded value that satisfies the test, we want to get the real value that’s stored in the database and return it. But before we can get anything from the database, we need to connect to it, so let’s start with this: class MysqlTransaction { function select($sql) { $connection = mysql_connect( 'localhost', 'me', 'secret', 'test', true); return new MysqlResult(); } } Not much of a change, just adding the connect call and doing nothing with it. The choice of call is quite interesting here. Assuming that we want to be backward com- patible with version 4.0 of My SQL and don’t currently have PDO installed, we use the older PHP function mysql_connect() rather than the newer Mysqli or PDO interfaces. Note that this doesn’t affect the tests. If you want to write your Mysql- Transaction class using PDO, it won’t substantially affect this chapter. When we run the tests, we get the result in figure 9.7. We haven’t set up the access to My SQL, and so PHP generates a warning about our failure to connect. SimpleTest reports this as an exception, because it cannot be tied to any failed assertion. Note that we only added one line before we ran the test suite. Running the tests is easy, just a single mouse click, so why not run them often? That way we get feedback the instant a line of code fails. Saving up a whole slew of errors before running the tests will take longer to sort out. With a small investment of a mouse click every few lines, we maintain a steady rhythm. Figure 9.7 This time we're unable to get the MySQL connection, and the test case tells us what's wrong. DATABASE SELECT 199 Once the user name, password, and database have been set up, we are back to green. We’ll skip a few steps here and go straight to the resulting code (see listing 7.1). Nor- mally this would take a couple of test cycles to sort out. class MysqlTransaction { private $connection; function __construct($host, $user, $password, $db) { $this->connection = mysql_connect( $host, $user, $password, $db, true); } function select($sql) { $result = @mysql_query($sql, $this->connection); return new MysqlResult($result); } } class MysqlResult { private $result; function __construct($result) { $this->result = $result; } function next() { return mysql_fetch_assoc($this->result); } } Depending on the settings in your php.ini, you will receive various warnings about My SQL queries. We are going to trap all errors with exceptions, so we’ll suppress the legacy PHP errors with the “@” operator. The test has also been modified slightly, so that the connection now takes the connection parameters from the test case: class TestOfMysqlTransaction extends UnitTestCase { function testCanReadSimpleSelect() { $transaction = new MysqlTransaction( 'localhost', 'me', 'secret', 'test'); $result = $transaction->select('select 1 as one'); $row = $result->next(); $this->assertEqual($row['one'], '1'); } } Job done. We have implemented our first feature. In doing so, we have left a trail of tests ( OK, just one) which specify the program so far. We have also gone in small steps, and so written only enough code to get the test to pass. 100 percent test coverage and lean code. That’s a nice benefit of this style of coding. We are building quality in. Right now we are green, as shown in figure 9.8. Listing 9.1 The MysqlTransaction class fully implemented 200 CHAPTER 9 TEST-DRIVEN DEVELOPMENT At last our Transaction class is up and running, and we have implemented the select() feature. From now on, things get faster. We need to implement the abil- ity to write to the database as well. But first, we want to do some error checking. 9.2.5 Test until you are confident The rules of this game are, write a test and watch it fail, get it green, modify (refactor) the code while green. This cycle is often abbreviated “red, green, refactor.” We only add features once we have a failing test. We are only allowed to add a test once all the other tests are passing. If you try to add features with other code not working, you just dig yourself into a mess. If you ever catch yourself doing that, stop, roll back, and recode in smaller steps. It will be quicker than floundering. We are green, so let’s add a test for some error checking: class TestOfMysqlTransaction extends UnitTestCase { function testShouldThrowExceptionOnBadSelectSyntax() { $transaction = new MysqlTransaction( 'localhost', 'me', 'secret', 'test'); $this->expectException(); $transaction->select('not valid SQL'); } } b That’s a long method name, isn’t it? We prefer long test method names that exactly explain what the test does. This makes the test more readable, makes the test output more readable when things go wrong, and also helps to keep us focused. With a wool- ier name such as testErrorChecking(), we might be tempted to test many more things. With a precise goal, we know when we are finished and ready to move on to the next feature. A test has to tell a story. C This time there is a funny sort of assertion. expectException() tells SimpleTest to expect an exception to be thrown before the end of the test. If it isn’t, SimpleTest registers a failure. We must get an exception to get to green. Getting the test to pass is pretty easy, and involves changing only the select() method of our transaction class: Figure 9.8 Finally, when the feature has been fully implemented, the test passes. b Long, intention- revealing method name c We had better get an exception DATABASE INSERT AND UPDATE 201 class MysqlTransaction { function select($sql) { $result = @mysql_query($sql, $this->connection); if ($error = mysql_error($this->connection)) { throw new Exception($error); } return new MysqlResult($result); } } Normally we would add more error checking here. In fact, we would keep adding tests until we had covered every type of error we could think of. At that point, we are confident in our code and can move on. For brevity, we are going to skip connection errors and so on, and move on to the execute() method. We have a lot of ground to cover. 9.3 DATABASE INSERT AND UPDATE We are now the proud owners of a read-only database transaction class. It can do SQL SELECT, but no INSERT or UPDATE. We need some way to get data into the data- base as well; typing it manually on the My SQL command line gets tedious. Insert and update is actu- ally simpler than select, since we need not worry about how to process the result. Figure 9.9 shows how simple it is. We’ll add an execute() method to our MysqlTransaction class. The exe- cute() method is like the select() method, but returns no result. It’s used for inserting or updating data. Because we have been moving forward successfully, we’ll also move in larger steps. That’s one of the joys of test-driven development; you can adjust the speed as you go. Clear run of green? Speed up. Keep getting failures? Slow down and take smaller steps. The idea is steady, confident progress. In the first sub- section, we’ll take a first shot at writing a test and then clean it up by separating the database setup code from the test itself. In the second subsection, we’ll implement the execute() method, committing a small sin by cutting and pasting from the select() method. Then we’ll atone for our sin by eliminating the duplication we just caused. 9.3.1 Making the tests more readable We want to write data to the database. Since we already have a way to read data, we can test the ability to write data by reading it back and checking that we get the same value back. Here is a test that writes a row and reads it back again. It’s a more aggres- sive test, but it’s not well written: Figure 9.9 Inserting or updating data involves just one call from the client to the MysqlTransac- tion class. 202 CHAPTER 9 TEST-DRIVEN DEVELOPMENT class TestOfMysqlTransaction extends UnitTestCase { function testCanWriteRowAndReadItBack() { $transaction = new MysqlTransaction( 'localhost', 'me', 'secret', 'test'); $transaction->execute('create table numbers (integer n)'); $transaction->execute('insert into numbers (n) values (1)'); $result = $transaction->select('select * from numbers'); $row = $result->next(); $this->assertEqual($row['n'], '1'); $transaction->execute('drop table numbers'); } } bd We need a test table in the database so that we insert and retrieve data without affect- ing anything else. Before the main test code, we create and drop the table. c We use the transaction class to insert a value into the database and retrieve it. Then we assert that the value retrieved is the equal to the one we inserted. What we see here is that the setup code (creating and dropping the table) and the test code are hopelessly intermingled. As a result, this test doesn’t tell a story. It’s difficult to read. We’ll rewrite the test case to make things clearer. First the schema handling: class TestOfMysqlTransaction extends UnitTestCase { private function createSchema() { $transaction = new MysqlTransaction( 'localhost', 'me', 'secret', 'test'); $transaction->execute('drop table if exists numbers'); $transaction->execute( 'create table numbers (n integer) type=InnoDB'); } private function dropSchema() { $transaction = new MysqlTransaction( 'localhost', 'me', 'secret', 'test'); $transaction->execute('drop table if exists numbers'); } } We’ve pulled the schema handling code out into separate methods. These methods won’t be run automatically by the testing tool, because they are private and don’t start with the string ‘test’. This is handy for adding helper methods to the test case, useful for common test code. Note that you will need a transactional version of My SQL for the following to work. That type=InnoDB statement at the end of the table creation tells MySQL to use a transactional table type. My SQL’s default table type is non-transactional, which could lead to a surprise. You might need to install My SQL-max rather than the stan- dard My SQL distribution for this feature to be present, depending on which version you are using. Create the table b Insert and retrieve data c d Drop the table DATABASE INSERT AND UPDATE 203 Extracting this code makes the main test flow a little easier. We have a setup section, the code snippet, the assertion, and finally we tear down the schema: class TestOfMysqlTransaction extends UnitTestCase { function testCanWriteRowAndReadItBack() { $this->createSchema(); $transaction = new MysqlTransaction( 'localhost', 'me', 'secret', 'test'); $transaction->execute('insert into numbers (n) values (1)'); $result = $transaction->select('select * from numbers'); $row = $result->next(); $this->assertEqual($row['n'], '1'); $this->dropSchema(); } } Later on, we will find a way to clean this code up even more. Why so much effort getting the tests to read well? After all, we only get paid for production code, not test code. It’s because we are not just writing test code. It’s about having an executable specification that other developers can read. As the tests become an executable design document, they gradually replace the paper artifacts. It becomes less about testing the code, and more about designing the code as you go. We’d put a lot of effort into our design documents to make them readable, so now that the tests are specifying the design, we’ll expend the same effort on the tests. The other devel- opers will thank us. 9.3.2 Red, green, refactor Right now, the test will crash. Our next goal is not to get the test to pass, but to get it to fail in a well-defined, informative way by giving us a red bar. To get the test from crash to red, we have to add the execute() method to MysqlTransaction. Then we’re ready to go for green. Here is the MysqlTransaction code I added to get to green, running the tests at each step. In the first step, we had never selected a data- base after logging on. This is easily fixed by selecting a database in the constructor and checking for errors: class MysqlTransaction { function __construct($host, $user, $password, $db) { $this->connection = mysql_connect( $host, $user, $password, $db, true); mysql_select_db($db, $this->connection); if ($error = mysql_error($this->connection)) { throw new Exception($error); } } // } 204 CHAPTER 9 TEST-DRIVEN DEVELOPMENT Then we have to actually write the execute() method. Most of the code is already in the select() method. As we want to get to green as quickly as possible, we’ll cut and paste the code we need from the select() method to the execute() method. class MysqlTransaction { function execute($sql) { mysql_query($sql, $this->connection); if ($error = mysql_error($this->connection)) { throw new Exception($error); } } } OK, the cut and paste got us to green, but we have a lot of duplicated code now. Once green, though, it’s much easier to refactor the code. We just go in small steps and run the tests each time. If we tried to do this on red, trying to get a perfect solution in one go, likely we would get into a tangle. Refactoring is easier with passing tests. First we’ll create a new method: class MysqlTransaction { private function throwOnMysqlError() { if ($error = mysql_error($this->connection)) { throw new Exception($error); } } } We run the tests. Next we make select() use the new method: class MysqlTransaction { function select($sql) { $result = mysql_query($sql, $this->connection); $this->throwOnMysqlError(); return new MysqlResult($result); } // } We run the tests again (still green) and then factor the error check out of the con- structor and the execute() method (not shown). Once we are happy that the code cannot be improved, we are ready to add another test. That’s a strange order to do things. Normally we design, then code, then test, then debug. Here we test, then code, then design once the first draft of the code is written. This takes faith that we will be able to shuffle the code about once it is already written. This faith is actually well placed. Did you notice we no longer have a debug step? You would have thought that making changes would now involve changing tests as well as code. Sometimes it does, but that’s a small price to pay. The biggest barrier to change is usually fear: fear that something will break, and that the damage will not [...]... testing and not in an ordinary program 210 For an interesting example of fakeness from the presumably real world of physical technology, consider incubators, the kind that help premature infants survive From our unit-testing point of view, an incubator is a complete fake implementation of a womb It maintains a similar stable environment, using a high-precision thermostat, feeding tubes, and monitoring... be thinking that we have skipped all of the SimpleTest scaffolding at this point What happened to including SimpleTest, and all that stuff about running with a reporter that we have in the transaction test script? In fact, it is rarely needed Instead, we will place the test scaffold code into its own file called classes/test/all_tests .php Here it is: < ?php require_once('simpletest/unit_tester .php' );... is now similar to our transaction_test .php in the previous chapter We let the test define the interface, and then write enough code to avoid a PHP crash Here is the minimum code in classes/contact .php that gives us a red bar instead of a crash: < ?php class Contact { function getEmail() { } function save($transaction) { } } class ContactFinder { function findByName($transaction, $name) { return new... these figures? By testing in very small units, we reduce combinatorial effects of features In addition, the code we write is naturally easy to test, as that was one of the running constraints in its production This also helps to make features independent during time As we combine our units of code, we will also write integration tests specifically aimed at testing combinations of actions These are much... coming in suddenly, it struck me how extraordinary a statement this was, a fictional character bombastically proclaiming himself real Working with software, we’re used to juggling the real and the unreal In computing, it’s a matter of taste whether you consider anything real or not, other than hardware and moving electrons Ultimately, it’s mostly fake The kind of fiction in which dreams and reality mingle... unit testing the end of debugging? Sadly, no If you are developing the usual hacky way, your manual tests will catch about 25 percent of the bugs in your program (see Facts and Fallacies of Software Engineering by Robert Glass) By manual tests, we mean print statements and run-throughs with a debugger The remaining bugs will either be from failure to think of enough tests ( 35 percent), or combinatorial... much easier when we know that the underlying parts are working perfectly in isolation Simply forgetting a test happens less often when you have the attitude that “we have finished when we cannot think of any more tests.” By having an explicit point in the process, this thought allows us to explore new testing ideas Again, we would expect a small reduction in missing tests due to this pause If optimistically... Deming? Building quality into the system had its own rewards for the twentieth-century Japanese economy With less money being spent on finding defects, especially finding them late, industry was actually able to cut costs while raising quality Buyers of Japanese products benefited not just from a lower price, but more reliability and better design TQM would turn Japan into an industrial power In 1 950 , though,... parameter tells fakemail where to store the incoming mails If you’ve started fakemail in a terminal, it will output: Starting fakemail Listening on port 25 The server is now waiting for incoming mail Next, we fire up a mail client and create a new account This account will use our local machine as the mail server The A FAKE MAIL SERVER 2 25 Figure 10.6 Example of configuring a mail client to use fakemail exact... organize an application into packages We’ll take the simplest approach: using an SQL script to create the table that the exception is screaming about To avoid mixing SQL scripts with our PHP code, we create a top-level directory called database and place the following scripts in it The first is database/create_schema.sql: create table contacts( name varchar( 255 ), email varchar( 255 ) ) type=InnoDB; Then there . then declaring test classes. Adding a test case is a single line of code and adding a test is a single line of code. We don’t like duplicating test code any more than we like duplicating production. transaction_test .php in the previous chapter. We let the test define the interface, and then write enough code to avoid a PHP crash. Here is the minimum code in classes/contact .php that gives us a red bar instead. “we have finished when we cannot think of any more tests.” By having an explicit point in the process, this thought allows us to explore new testing ideas. Again, we would expect a small reduction in

Ngày đăng: 12/08/2014, 21:21