Automatically generate HTTP API documentation from your Go unit tests: a simple addition to Go's testing pkg
Given a handler func:
// GetWidget retrieves a single Widget
func GetWidget(w http.ResponseWriter, req *http.Request) {
// get widget
// respond with widget JSON
// ...
}
And a test for this handler func:
func TestGetWidget(t *testing.T) {
urlPath := fmt.Sprintf("/widgets/%d", 2)
resp, err := http.Get(server.URL + urlPath)
// assert all the things...
}
Test2doc will generate markdown documentation for this endpoint in the API Blueprint format, like so:
# Group widgets
## /widgets/{id}
+ Parameters
+ `id`: `2` (number)
### Get Widget [GET]
retrieves a single Widget
+ Response 200
+ Body
{
"Id": 2,
"Name": "Pencil",
"Role": "Utensil"
}
Which you can then parse and host w/ Apiary.io, eg here. Or use a custom parser and host yourself.
- Go pkg name becomes
Group
name - Go handler name becomes endpoint title
- Go handler
godoc
string becomes endpoint description - Everything else is recorded & interpreted directly from the requests and responses
Eg.
package widget
// GetWidget retrieves a single Widget
func GetWidget(w http.ResponseWriter, req *http.Request)
becomes
# Group widget
### Get Widget [*]
retrieves a single Widget
go get github.com/adams-sarah/test2doc/...
Very few additions, and only to your testing code.
import (
"github.com/adams-sarah/test2doc/test"
)
var server *test.Server
func TestMain(m *testing.M) {
// 1. Tell test2doc how to get URL vars out of your HTTP requests
//
// The 'URLVarExtractor' function must have the following signature:
// func(req *http.Request) map[string]string
// where the returned map is of the form map[key]value
test.RegisterURLVarExtractor(myURLVarExtractorFn)
// 2. You must use test2doc/test's wrapped httptest.Server instead of
// the raw httptest.Server, so that test2doc can listen to and
// record requests & responses.
//
// NewServer takes your HTTP handler as an argument
server, err := test.NewServer(router)
if err != nil {
panic(err.Error())
}
// .. then run your tests as usual
// (remember that os.Exit does not respect defers)
exitCode := m.Run()
// 3. Finally, you must tell the wrapped server when you are done testing
// so that the buffer can be flushed to an apib doc file
server.Finish()
// note that os.Exit does not respect defers.
os.Exit(exitCode)
}
gorilla/mux
configurations
// NOTE: if you are using gorilla/mux, you must set the router's
// 'KeepContext' to true, so that url parameters can be accessed
// after the request has been handled.
router := NewRouter()
router.KeepContext = true
// Use mux.Vars func as URLVarExtractor
test.RegisterURLVarExtractor(mux.Vars)
julienschmidt/httprouter
configurations
// MakeURLVarExtractor returns a func which extracts
// url vars from a request for test2doc documentation generation
func MakeURLVarExtractor(router *httprouter.Router) parse.URLVarExtractor {
return func(req *http.Request) map[string]string {
// httprouter Lookup func needs a trailing slash on path
path := req.URL.Path
if !strings.HasSuffix(path, "/") {
path += "/"
}
_, params, ok := router.Lookup(req.Method, path)
if !ok {
return nil
}
paramsMap := make(map[string]string, len(params))
for _, p := range params {
paramsMap[p.Key] = p.Value
}
return paramsMap
}
}
// and then..
test.RegisterURLVarExtractor(MakeURLVarExtractor(router))
test2doc
will spit out one doc file per package.
Eg. A package tree like:
.
├── foos
│ ├── foos.go
│ └── foos_test.go
└── widgets
├── widgets.go
└── widgets_test.go
Will produce separate apib files, eg:
.
├── foos
│ ├── ...
│ └── foos.apib
└── widgets
├── ...
└── widgets.apib
You will need to add the doc header (below) and combine all of the package doc files after your tests run.
eg.:
# find all *.apib files (after tests have run, generated files)
files=`find . -type f -name "*.apib"`
# copy template file to new apiary.apib file
cp apib.tmpl apiary.apib
# copy contents of each generated apib file into apiary.apib
# and delete the apib file
for f in ${files[@]}; do
cat $f >> apiary.apib
rm $f
done
where apib.tmpl
includes the doc header information.
Something like:
FORMAT: 1A
HOST: https://api.mysite.com
# The API for My Site
My Site is a fancy site. The API is also fancy.