I'm trying to write a function getTargetServer()
to return a polymorphic type that has both a data member URL
and a method Close()
. This would be a generalization of the *Server
returned from httptest.NewServer()
but I want to alternatively be able to return a custom type for which Close()
is a NOP.
type externalTestServer struct {
URL string
}
func (externalTestServer) Close() {}
func getTargetServer() *externalTestServer {
if urlbase, ok := optionals["urlbase"].(string); ok {
return &externalTestServer{URL: urlbase}
} else {
testServer := httptest.NewServer(newMyServerHandler())
// return testServer // ### Error ###
return &externalTestServer{URL: testServer.URL}
}
}
func Test_health_check(t *testing.T) {
testServer := getTargetServer()
defer testServer.Close()
response, err := http.Get(testServer.URL + "/health")
assert.NilError(t, err)
assert.Assert(t, cmp.Equal(response.StatusCode, http.StatusOK))
}
This works like a charm except that Close()
is always a NOP. When I uncomment the indicated ### Error ###
line in order to return a closable *Server
, I get the following error message:
cannot use testServer (type *httptest.Server) as type *externalTestServer in return argument
I understand the error, but haven't discovered a solution that lets me return a polymorphic type that generalizes *Server
Note: "A Tour of Go" defines an interface type as follows:
An interface type is defined as a set of method signatures.
Therefore, returning a simple interface will not allow for directly-accessible data members.
You could create a struct that has a field which is a string URL
and a field that is a Close
func
. The Close
func
can be implemented by either externalTestServer
or httptest.Server
:
type server struct {
URL string
Close func()
}
if urlbase, ok := optionals["urlbase"].(string); ok {
extServer := &externalTestServer{URL: urlbase}
return &server{
URL: urlbase,
Close: extServer.Close,
}
}
testServer := httptest.NewServer(newMyServerHandler())
return &server{
URL: testServer.URL,
Close: testServer.Close,
}
http.Server is a struct, so you cannot return a polymorphic object that generalizes that. You can do something else though:
type Server interface {
GetURL() string
Close() error
}
type testServer struct {
URL string
}
func (t testServer) Close() error {}
func (t testServer) GetURL() string {return t.URL}
type httpServer struct {
*http.Server
}
func (t httpServer) GetURL() string { return the url }
You can then return Server from your function.
Chris Drew's approach is ideal for this specific case, because it requires minimal code overhead. Put another way, it is the simplest thing that will solve today's problem. The solution uses the magic of an implicit closure (for the receiver) to reference the implementation of Close()
polymorphically.
My slightly simplified version of that is here...
type genericServer struct {
URL string // snapshot of internal data
Close func() // single-function polymorphism without 'interface'
}
func getTargetServer() genericServer {
if urlbase, ok := optionals["urlbase"].(string); ok {
return genericServer{URL: urlbase, Close: func() {}}
}
testServer := httptest.NewServer(newMyServerHandler())
return genericServer{URL: testServer.URL, Close: testServer.Close}
}
By embedding an actual interface type into to return struct, this concept can be seamlessly extended to better support non-trivial polymorphic interfaces. In this case, the Polymorphism is explicit based on the internal use of an interface type. Still, the returned type is a wrapper that includes a copy of the (constant) member data, so the usage is identical -- effectively generalizing that of *Server
for this use-case.
type Server interface {
Close()
}
type nopServer struct {
}
func (nopServer) Close() {}
type genericServer struct {
URL string // snapshot of specific data
Server // embedded interface type for explicit polymorphism
}
func getTargetServer() genericServer {
if urlbase, ok := optionals["urlbase"].(string); ok {
return genericServer{URL: urlbase, Server: nopServer{}}
}
testServer := httptest.NewServer(newMyServerHandler())
return genericServer{URL: testServer.URL, Server: testServer}
}
Note that the URL
value is not a live member of the implementation, so these solutions, as presented, are only meaningful when the data value will not change, although perhaps that limitation could be overcome by using a pointer.