+ Testing classes is all very well, but PHP is predominately + a language for creating functionality within web pages. + How do we test the front end presentation role of our PHP + applications? + Well the web pages are just text, so we should be able to + examine them just like any other test data. +
++ This leads to a tricky issue. + If we test at too low a level, testing for matching tags + in the page with pattern matching for example, our tests will + be brittle. + The slightest change in layout could break a large number of + tests. + If we test at too high a level, say using mock versions of a + template engine, then we lose the ability to automate some classes + of test. + For example, the interaction of forms and navigation will + have to be tested manually. + These types of test are extremely repetitive and error prone. +
++ SimpleTest includes a special form of test case for the testing + of web page actions. + The WebTestCase includes facilities + for navigation, content and cookie checks and form handling. + Usage of these test cases is similar to the + UnitTestCase... +
+class TestOfLastcraft extends WebTestCase { +} ++ Here we are about to test the + Last Craft site itself. + If this test case is in a file called lastcraft_test.php + then it can be loaded in a runner script just like unit tests... +
+<?php + require_once('simpletest/web_tester.php'); + require_once('simpletest/reporter.php'); + + $test = &new GroupTest('Web site tests'); + $test->addTestFile('lastcraft_test.php'); + exit ($test->run(new TextReporter()) ? 0 : 1); +?> ++ I am using the text reporter here to more clearly + distinguish the web content from the test output. + +
+ Nothing is being tested yet. + We can fetch the home page by using the + get() method... +
+class TestOfLastcraft extends WebTestCase { + + function testHomepage() { + $this->assertTrue($this->get('http://www.lastcraft.com/')); + } +} ++ The get() method will + return true only if page content was successfully + loaded. + It is a simple, but crude way to check that a web page + was actually delivered by the web server. + However that content may be a 404 response and yet + our get() method will still return true. + +
+ Assuming that the web server for the Last Craft site is up + (sadly not always the case), we should see... +
+Web site tests +OK +Test cases run: 1/1, Failures: 0, Exceptions: 0 ++ All we have really checked is that any kind of page was + returned. + We don't yet know if it was the right one. + + + +
+ To confirm that the page we think we are on is actually the + page we are on, we need to verify the page content. +
+class TestOfLastcraft extends WebTestCase { + + function testHomepage() { + $this->get('http://www.lastcraft.com/'); + $this->assertWantedPattern('/why the last craft/i'); + } +} ++ The page from the last fetch is held in a buffer in + the test case, so there is no need to refer to it directly. + The pattern match is always made against the buffer. + +
+ Here is the list of possible content assertions... +
assertTitle($title) | Pass if title is an exact match | +
assertWantedPattern($pattern) | A Perl pattern match against the page content | +
assertNoUnwantedPattern($pattern) | A Perl pattern match to not find content | +
assertWantedText($text) | Pass if matches visible and "alt" text | +
assertNoUnwantedText($text) | Pass if doesn't match visible and "alt" text | +
assertLink($label) | Pass if a link with this text is present | +
assertNoLink($label) | Pass if no link with this text is present | +
assertLinkById($id) | Pass if a link with this id attribute is present | +
assertNoLinkById($id) | Pass if no link with this id attribute is present | +
assertField($name, $value) | Pass if an input tag with this name has this value | +
assertFieldById($id, $value) | Pass if an input tag with this id has this value | +
assertResponse($codes) | Pass if HTTP response matches this list | +
assertMime($types) | Pass if MIME type is in this list | +
assertAuthentication($protocol) | Pass if the current challenge is this protocol | +
assertNoAuthentication() | Pass if there is no current challenge | +
assertRealm($name) | Pass if the current challenge realm matches | +
assertHeader($header, $content) | Pass if a header was fetched matching this value | +
assertNoUnwantedHeader($header) | Pass if a header was not fetched | +
assertHeaderPattern($header, $pattern) | Pass if a header was fetched matching this Perl regex | +
assertCookie($name, $value) | Pass if there is currently a matching cookie | +
assertNoCookie($name) | Pass if there is currently no cookie of this name | +
+ So now we could instead test against the title tag with... +
+$this->assertTitle('The Last Craft? Web developer tutorials on PHP, Extreme programming and Object Oriented development'); ++ As well as the simple HTML content checks we can check + that the MIME type is in a list of allowed types with... +
+$this->assertMime(array('text/plain', 'text/html')); ++ More interesting is checking the HTTP response code. + Like the MIME type, we can assert that the response code + is in a list of allowed values... +
+class TestOfLastcraft extends WebTestCase { + + function testRedirects() { + $this->get('http://www.lastcraft.com/test/redirect.php'); + $this->assertResponse(200);</strong> + } +} ++ Here we are checking that the fetch is successful by + allowing only a 200 HTTP response. + This test will pass, but it is not actually correct to do so. + There is no page, instead the server issues a redirect. + The WebTestCase will + automatically follow up to three such redirects. + The tests are more robust this way and we are usually + interested in the interaction with the pages rather + than their delivery. + If the redirects are of interest then this ability must + be disabled... +
+class TestOfLastcraft extends WebTestCase { + + function testHomepage() { + $this->setMaximumRedirects(0); + $this->get('http://www.lastcraft.com/test/redirect.php'); + $this->assertResponse(200); + } +} ++ The assertion now fails as expected... +
+Web site tests +1) Expecting response in [200] got [302] + in testhomepage + in testoflastcraft + in lastcraft_test.php +FAILURES!!! +Test cases run: 1/1, Failures: 1, Exceptions: 0 ++ We can modify the test to correctly assert redirects with... +
+class TestOfLastcraft extends WebTestCase { + + function testHomepage() { + $this->setMaximumRedirects(0); + $this->get('http://www.lastcraft.com/test/redirect.php'); + $this->assertResponse(array(301, 302, 303, 307)); + } +} ++ This now passes. + + + +
+ Users don't often navigate sites by typing in URLs, but by + clicking links and buttons. + Here we confirm that the contact details can be reached + from the home page... +
+class TestOfLastcraft extends WebTestCase { + ... + function testContact() { + $this->get('http://www.lastcraft.com/'); + $this->clickLink('About'); + $this->assertTitle('About Last Craft'); + } +} ++ The parameter is the text of the link. + +
+ If the target is a button rather than an anchor tag, then + clickSubmit() should be used + with the button title... +
+$this->clickSubmit('Go!'); ++ +
+ The list of navigation methods is... +
getUrl() | The current location | +
get($url, $parameters) | Send a GET request with these parameters | +
post($url, $parameters) | Send a POST request with these parameters | +
head($url, $parameters) | Send a HEAD request without replacing the page content | +
retry() | Reload the last request | +
back() | Like the browser back button | +
forward() | Like the browser forward button | +
authenticate($name, $password) | Retry after a challenge | +
restart() | Restarts the browser as if a new session | +
getCookie($name) | Gets the cookie value for the current context | +
ageCookies($interval) | Ages current cookies prior to a restart | +
clearFrameFocus() | Go back to treating all frames as one page | +
clickSubmit($label) | Click the first button with this label | +
clickSubmitByName($name) | Click the button with this name attribute | +
clickSubmitById($id) | Click the button with this ID attribute | +
clickImage($label, $x, $y) | Click an input tag of type image by title or alt text | +
clickImageByName($name, $x, $y) | Click an input tag of type image by name | +
clickImageById($id, $x, $y) | Click an input tag of type image by ID attribute | +
submitFormById($id) | Submit a form without the submit value | +
clickLink($label, $index) | Click an anchor by the visible label text | +
clickLinkById($id) | Click an anchor by the ID attribute | +
getFrameFocus() | The name of the currently selected frame | +
setFrameFocusByIndex($choice) | Focus on a frame counting from 1 | +
setFrameFocus($name) | Focus on a frame by name | +
+ The parameters in the get(), post() or + head() methods are optional. + The HTTP HEAD fetch does not change the browser context, only loads + cookies. + This can be useful for when an image or stylesheet sets a cookie + for crafty robot blocking. +
++ The retry(), back() and + forward() commands work as they would on + your web browser. + They use the history to retry pages. + This can be handy for checking the effect of hitting the + back button on your forms. +
++ The frame methods need a little explanation. + By default a framed page is treated just like any other. + Content will be searced for throughout the entire frameset, + so clicking a link will work no matter which frame + the anchor tag is in. + You can override this behaviour by focusing on a single + frame. + If you do that, all searches and actions will apply to that + frame alone, such as authentication and retries. + If a link or button is not in a focused frame then it cannot + be clicked. +
++ Testing navigation on fixed pages only tells you when you + have broken an entire script. + For highly dynamic pages, such as for bulletin boards, this can + be crucial for verifying the correctness of the application. + For most applications though, the really tricky logic is usually in + the handling of forms and sessions. + Fortunately SimpleTest includes + tools for testing web forms + as well. +
+ + ++ Although SimpleTest does not have the goal of testing networking + problems, it does include some methods to modify and debug + the requests it makes. + Here is another method list... +
getTransportError() | The last socket error | +
showRequest() | Dump the outgoing request | +
showHeaders() | Dump the incoming headers | +
showSource() | Dump the raw HTML page content | +
ignoreFrames() | Do not load framesets | +
setCookie($name, $value) | Set a cookie from now on | +
addHeader($header) | Always add this header to the request | +
setMaximumRedirects($max) | Stop after this many redirects | +
setConnectionTimeout($timeout) | Kill the connection after this time between bytes | +
useProxy($proxy, $name, $password) | Make requests via this proxy URL | +