CSC309: Web ProgrammingGreg Wilson 1 Remember testing?It’s ironic: distributed applications are harder to debug, but we test them lessStandard web applications are not harder to test than other programsProvided you design them for testability in the first placeThe things you do to make them testable are things you should be doing anyway !" Legacy code (n.): source code that relates to a no-longer supported computer systemOften used as an insult, meaning “a mess you’d wish on your worst enemy”Feathers’ definition: “legacy code is code that doesn’t have unit tests”Without comprehensive unit tests, you can never be sure that your changes haven’t broken something#$ How do you test this?HTTP requestHTMLApache Python CGIPostgreSQLFilesystemCSC309: Web ProgrammingGreg Wilson 2%& '(")(Accept the system as givenSend HTTP, parse HTMLFaithfulLots of workAnd remember, screen scraping is fragileHTTP requestHTMLApache Python CGIPostgreSQLFilesystem*+ "" ,Do regression testing to make sure that we haven’t broken anything that used to workSo:Record a working system (HTTP in, text out)Replay the HTTP, and diff the resultWinRunner is the gold standard for desktop appsMaxQ (maxq.tigris.org)Allows non-programmers to create and run testsBut you have to have a nearly-working system……and it’s still screen-scraping-./!$$ 0Don’t look at HTML directlyInstead, insert markers and look for thoseMuch less likely to change over time<p> User ID:<em class=“test” role=“uid”>GVW</em></p>Going to use CSS to style the page anywayNot checking everything……but giving yourself more time to check the things that require human eyeballs12 !$$ HTTP requestXMLApache Python CGIPostgreSQLFilesystemCSC309: Web ProgrammingGreg Wilson 334,$ How about getting rid of the web server?Run the CGI ourselvesTell it to produce XML (or plain text) rather than HTMLMakes debugging easierBut testing even less faithfulTest CodeFakeCGI Python CGIPostgreSQLFilesystem2 4,$ "All" we need is our own cgi.FieldStorageAnd os.environAnd sys.stdin and sys.stdoutDesign for testabilityCode to interfaces, not implementations 5 ' Database is persistent storageThat’s its whole purposeBut tests are supposed to be independentIn theory, create a new database for each In practice, just wipe and re-fill the tablesIf tests are slow, programmers won’t run them2 ' “If tests are slow” covers a lot of sinsHow long does it take developers to set up?Install the software the first time?Reinstall/reconfigure for testing?Will it interrupt real service?Will it impact other developers?SelectAccess: could take two full days to install and configureWhich was eight days less than the competitionCSC309: Web ProgrammingGreg Wilson 42 ' Solution: change what you’re testingSQLite and HSQLDB are much smaller than PostgreSQLAnd both can store database in memoryUseless for real work, but great for testingDesign for testing:Abstract away the details of external packages A good idea anywayRun developer tests using replacementsStill do final tests with real system, of course#5,)6 If no replacement is available, build a mock objectBehaves like a limited version of the real thingE.g., when asked for contents of a file, hands back the path, reversedOnly works if you have a clean interface between your application and the subsystemBut you should anyway/5 Much simpler to (re)install and (re)initializeAnd much less likely to mess up production systemAs faithful as you make itWeeding out the easy problems gives developers more time to look at the hard onesTest CodeFakeCGI Python CGISQLite-MMockFS*'/ $ Goal is to produce something that can:Be run from the command line, so that its output can be compared to saved output (testing via Makefile)Be called inside unittest (Python’s equivalent of JUnit) as part of a larger test suiteWould like to avoid creating temporary filesMay need to pass command-line parameters to the thing being testedCSC309: Web ProgrammingGreg Wilson 5-/!' cgi, requestType, url, headers = sys.argv[1:5]data = sys.stdin.read()if data:headers.append('Content-Length=%d' % len(data))os.putenv('REQUEST_METHOD', requestType)os.putenv('PATH_INFO', url)for h in headers:key, value = h.split('=', 1)os.putenv(key, value)childIn, childOut = os.popen2(cgi)childIn.write(data)childIn.close()result = childOut.read()while result:sys.stdout.write(result)result = childOut.read()1/ . if __name__ == '__main__':# Parse arguments.settings = parseArgs()# Convert headers.settings['headers'] = convertHeaders(settings['headers'])# Create server if required.if not settings['useExisting']:# Get rid of pre-existing .pyc (if any).if os.path.isfile('Nitinat.pyc'):os.remove('Nitinat.pyc')32 . # Create server.RequestHandler.RootDirectory = settings['root']childArgs = ['python', 'Nitinat.py','-h',
View Full Document