Wie man http-Server unter Python mockt

Führt der eigene Code http-Calls aus, z.B. um Dateien von einem externen Server runterzuladen stellt sich die Frage nach der Testbarkeit. Idealerweise ist der Code so aufgebaut, daß er nicht direkt von einer http-Library abhängt und man entsprechende Calls mocken kann. Ist dies nicht einfach möglich, kann man den in der Standarlibrary eingebauten http.server als localhost-Gegenstelle für Tests verwenden.

Der Trick ist dabei http.server in einem eigenen Prozeß auf einem freien Port zu starten, damit test-Code und mock-Server nicht blockieren. Multithreading bietet sich hier nicht an wegen dem Global Interpreter Lock (GIL).

Die Info über einen freien Port liefert übrigens socketserver.TCPServer, wenn man diesen testweise mit einem Port von “0” instantiiert:

        with socketserver.TCPServer(("localhost", 0), None) as server:
            self.port = server.server_address[1]

Mit einer komfortablen Wrapper-Klasse außenrum lässt sich der Prozeß vom Test-Runner starten und stoppen.

Der komplette Code inkl. Beispielen befindet sich auf github, hier nur der relevante Ausschnitt:

class HttpServer():
    '''Simple http server'''

    def __init__(self, folder):
        self.port = 0
        self.process = None
        self.folder = folder

    def _runner(self):
        '''Starts the http server'''
        server_address = ('localhost', self.port)
        server_class = http.server.HTTPServer
        os.chdir(self.folder)
        handler_class = http.server.SimpleHTTPRequestHandler
        httpd = server_class(server_address, handler_class)
        httpd.serve_forever()

    def start(self):
        '''Starts the http server in a background process'''

        with socketserver.TCPServer(("localhost", 0), None) as server:
            self.port = server.server_address[1]
        self.process = multiprocessing.Process(target=self._runner, args=())
        self.process.start()
        # Sleep a bit to make sure port is actually served
        time.sleep(0.1)

    def stop(self):
        '''Stops the http background server'''
        self.process.terminate()
        self.process = None
        self.port = 0