How do I test a web service in one program, calling it from its own client?
← Web Services · Ref: Q1760
Use serveUntilComplete(readyHandler) instead of serve(). It binds the application's services, starts listening and only THEN runs your ready handler, on its own thread, passing it the serving HTTP. The handler is the service's client: it can make requests and print what comes back, with no race against the server starting. Serving ends when the handler calls server.stop() or simply returns - and if it throws, the server is stopped and the Exception (or Panic) reaches the caller of serveUntilComplete, so a failed test never hangs. It returns true, like serve().
The server is STOPPED, never closed: it is a resource of the try that made it, and only that owner closes it, so the program below prints that it is still set after serveUntilComplete returns. Closing it in the handler is an error - server.close() is E81060, there is no close statement (E01094), and closing it is the owning try's job.
There are two ready types. An HTTPReadyHandler can act - make requests, change state, stop the server. An HTTPReadyObserver is pure - it can only observe that the server is up (log a readiness line, say); serving ends when it returns. TCP has the same pair for acceptUntilComplete(handler, readyHandler): TCPReadyHandler and TCPReadyObserver.
Bind to port 0 so no fixed port can collide: HTTP writes the port the operating system chose back into the NetworkProperties it was given, and the ready handler captures those properties to find it. The client below speaks HTTP/1.1 over a TCP socket - a request line, headers, a blank line - and reads the status line and the last line, the body. A program may create only one HTTP (E12023), so test a second scenario in a second program.
Example
defines module qa.webservices.testinprocess defines service GreetingService :/greeting open greet() as GET for :/{name} -> personName as String :=: PATH "name" <- response as HTTPResponse: (who: personName) with trait of HTTPResponse override status() as pure <- rtn as Integer: 200 override content() <- rtn as Bytes: Bytes().withUtf8(`hello ${who}`) override contentType() as pure <- rtn as String: "text/plain" default operator ? defines function httpGet() -> port as Integer path as String <- rtn as String: "no reply" loopback <- Networking().loopbackV4() try -> connection <- TCP(NetworkProperties(loopback, port)).connect() if connection? connection.output().println(`GET ${path} HTTP/1.1`) connection.output().println("Host: localhost") connection.output().println("Connection: close") connection.output().println("") statusLine <- connection.input().next() body <- String() while connection.input()? body := connection.input().next() rtn: `${statusLine} | ${body}` defines application GreetingApp register GreetingService() defines program TestServiceInProcess() with application of GreetingApp stdout <- Stdout() properties <- NetworkProperties(0, true) try -> httpServer <- HTTP(properties) client <- (stdout, properties) is HTTPReadyHandler as function stdout.println(`client got: ${httpGet(properties.port, "/greeting/Steve")}`) server.stop() served <- httpServer.serveUntilComplete(client) stdout.println(`serveUntilComplete returned ${served}, server still set ${httpServer?}`) stdout.println("the try has closed the server")
Common mistakes
E81060 — The ready handler is lent the server; it does not own it, and the close operator is never called as a method. Call server.stop() - serving ends, and the try that made the server closes it. See ek9 -h E81060 for details.
Incorrect:
server.close()
Correct:
server.stop()
Other ways to ask this
- How do I run an HTTP service and a client against it in the same EK9 program?
- What is HTTP serveUntilComplete for?
- How do I know the HTTP server is listening before my client calls it?
- How do I stop serve() once my test requests are done?
- What is the difference between HTTPReadyHandler and HTTPReadyObserver?
Coming from another language?
Java: Spring's @SpringBootTest(webEnvironment = RANDOM_PORT) or an embedded Jetty/HttpServer started in a @BeforeEach with a latch or polling to know it is listening, then stopped in @AfterEach. Go: httptest.NewServer gives a running server and its URL. Python: a TestClient or a thread running the server. EK9: serveUntilComplete runs the client only once the server listens, and stops the server when the client ends, whatever way it ends.
Keywords: HTTPReadyHandler, serveUntilComplete, HTTP, client, HTTPReadyObserver, service, in-process, port, stop, test