One of the traits of applications that call themselves Web 2.0 is the availability of an API which allows you to integrate it into your own service (generally called a mashup). Given the public facing nature of the API it needs to be rock-solid from a testing perspective and the easiest way to get that comfort is to do what is known as eating your own dog food. In this case it means “using the documentation that is available with the API, make your own mashup for testing purposes”.

Most Web 2.0 APIs work by sending something specific to a specific URL then dealing with the returned XML appropriately. This means that the API is magically language independennt. For this reason, I would recommend testing it ina language other than the one the application was originally written for and in a language that is fast to write in (python, ruby, perl).

I had hoped to illustrate how easy it is to test an API from a local company (such as Nuvvo or BubbleShare) but neither have released APIs to their services. Instead I used FeedBurner.

The script I whipped up in an hour is below the cut or here connects to the FeedBurner server and collects the stats for a feed and displays them. It has had very little architectural thought, and limited testing, but from an example perspective I think it is sufficient to get the point across. Clearly in a full-bore test scenario you would want more features, like the rest of the API implemented for instance and checking whether the back-end servers the API is manipulating have been manipulated correctly.

One other side effect of implementing the API testing in Python (or again, Perl or Ruby) is that you can instantly release it as a module to the associated community and have native support for your product in that language, which increases your total market, which increases your revenue, which increases…

And of course, if you want to contract me to exercise your API in Python (with the aforementioned ‘more thought’), email me.


import httplib, base64, xml.dom.minidom

class FeedBurner(object):
    def __init__(self, id, password):
        self.id = id
        self.password = password
        self.fb = None
        self.headers = {}
        self.base_api = "api.feedburner.com"

    def connect_http(self):
        """basic authentication over http"""
        self.access_method = "http"
        tmp = "%s:%s" % (self.id, self.password)
        self.headers["Authorization"] = "Basic %s" % (base64.encodestring(tmp).strip())
        if not self.fb:
            self.fb = httplib.HTTPSConnection(self.base_api)
            self.fb.connect()
        else:
            pass

    def connect_https(self):
        """basic authentication over https"""
        self.access_method = "https"
        tmp = "%s:%s" % (self.id, self.password)
        self.headers["Authorization"] = "Basic %s" % (base64.encodestring(tmp).strip())
        if not self.fb:
            self.fb = httplib.HTTPSConnection(self.base_api)
            self.fb.connect()
        else:
            pass

    def connect_params(self):
        """credentials in each GET"""
        self.access_method = "params"
        self.auth_message = "user=%s&password=%s" % (self.id, self.password)

class AwAPI(FeedBurner):
    """http://www.feedburner.com/fb/a/api/awareness"""
    awapi_url = "/awareness/1.0"

    def __init__(self, id, password):
        FeedBurner.__init__(self, id, password)

    def GetFeedDataURI(self, uri):
        getfeed_url= "/GetFeedData"
        if self.access_method.startswith("http"):
            args = "uri=%s" % uri
        elif self.access_method == "params":
            args = "uri=%s&%s" % (uri, self.auth_message)
        else:
            raise SystemExit, "unknown access method"
        self.fb.request("GET", "%s%s?%s" % (self.awapi_url, getfeed_url, args), "", self.headers)
        self.processFeedData()

    def GetFeedDataID(self, id):
        pass

    def processFeedData(self):
        self.feed_data = {}
        resp = self.fb.getresponse()
        rsp = xml.dom.minidom.parseString(resp.read())
        # stat is either "ok" or "fail"
        stat = rsp.documentElement.getAttribute("stat")
        if stat == "ok":
            for feed in rsp.getElementsByTagName("feed"):
                furi = str(feed.getAttribute("uri"))
                if furi not in self.feed_data:
                    self.feed_data[furi] = {}
                entry = feed.getElementsByTagName("entry")
                if len(entry) == 1:
                    self.feed_data[furi]["date"] = str(entry[0].getAttribute("data"))
                    self.feed_data[furi]["circulation"] = str(entry[0].getAttribute("circulation"))
                    self.feed_data[furi]["hits"] = str(entry[0].getAttribute("hits"))
                else:
                    raise SystemExit, "incorrect number of entries for feed '%s'" % furi
        elif stat == "fail":
            err = rsp.getElementsByTagName("err")
            if len(err) == 1:
                code = err[0].getAttribute("code")
                msg = err[0].getAttribute("msg")
                raise SystemExit, "recieved code %s - %s" % (code, msg)
            else:
                raise SystemExit, "incorrect number of error codes"
        else:
            raise SystemExit, "'stat' is an unknown value"

    def displayFeedData(self):
        for uri in self.feed_data.keys():
            print "Summary data for feed: %s" % uri
            print "   Date:        %s" % self.feed_data[uri]["date"]
            print "   Circulation: %s" % self.feed_data[uri]["circulation"]
            print "   Hits: %s" % self.feed_data[uri]["hits"]

class FlareAPI(FeedBurner):
    pass

class MgmtAPI(FeedBurner):
    pass

class MobileAPI(FeedBurner):
    pass

if __name__ == "__main__":
    uri = "your feed uri"
    id = "your feedburner login"
    password = "your feedburner password"
    f = AwAPI(id, password)
    f.connect_http()
    f.GetFeedDataURI(uri)
    f.displayFeedData()