I'd like to ensure with a test, that for each APIErrorCode
constant defined as below, the map APIErrorCodeMessages
contains an entry. How can I enumerate all constants of a certain type in Go?
// APIErrorCode represents the API error code
type APIErrorCode int
const (
// APIErrorCodeAuthentication represents an authentication error and corresponds with HTTP 401
APIErrorCodeAuthentication APIErrorCode = 1000
// APIErrorCodeInternalError represents an unknown internal error and corresponds with HTTP 500
APIErrorCodeInternalError APIErrorCode = 1001
)
// APIErrorCodeMessages holds all error messages for APIErrorCodes
var APIErrorCodeMessages = map[APIErrorCode]string{
APIErrorCodeInternalError: "Internal Error",
}
I've looked into reflect
and go/importer
and tried tools/cmd/stringer
without success.
The reflect
package does not provide access to exported identifiers, as there is no guarantee they will be linked to the executable binary (and thus available at runtime); more on this: Splitting client/server code; and How to remove unused code at compile time?
This is a source-code level checking. What I would do is write a test that checks if the number of error code constants matches the map length. The solution below will only check the map length. An improved version (see below) may also check if the keys in the map match the values of the constant declarations too.
You may use the go/parser
to parse the Go file containing the error code constants, which gives you an ast.File
describing the file, containing the constant declarations. You just need to walk through it, and count the error code constant declarations.
Let's say your original file is named "errcodes.go"
, write a test file named "errcodes_test.go"
.
This is how the test function could look like:
func TestMap(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
if err != nil {
t.Errorf("Failed to parse file: %v", err)
return
}
errCodeCount := 0
// Range through declarations:
for _, dd := range f.Decls {
if gd, ok := dd.(*ast.GenDecl); ok {
// Find constant declrations:
if gd.Tok == token.CONST {
for _, sp := range gd.Specs {
if valSp, ok := sp.(*ast.ValueSpec); ok {
for _, name := range valSp.Names {
// Count those that start with "APIErrorCode"
if strings.HasPrefix(name.Name, "APIErrorCode") {
errCodeCount++
}
}
}
}
}
}
}
if exp, got := errCodeCount, len(APIErrorCodeMessages); exp != got {
t.Errorf("Expected %d err codes, got: %d", exp, got)
}
}
Running go test
will result in:
--- FAIL: TestMap (0.00s)
errcodes_test.go:39: Expected 2 err codes, got: 1
The test properly reveals that there are 2 constant error code declarations, but the APIErrorCodeMessages
map contains only 1 entry.
If we now "complete" the map:
var APIErrorCodeMessages = map[APIErrorCode]string{
APIErrorCodeInternalError: "Internal Error",
APIErrorCodeAuthentication: "asdf",
}
And run go test
again:
PASS
Note: it's a matter of style, but the big loop may be written this way to decrease nesting level:
// Range through declarations:
for _, dd := range f.Decls {
gd, ok := dd.(*ast.GenDecl)
if !ok {
continue
}
// Find constant declrations:
if gd.Tok != token.CONST {
continue
}
for _, sp := range gd.Specs {
valSp, ok := sp.(*ast.ValueSpec)
if !ok {
continue
}
for _, name := range valSp.Names {
// Count those that start with "APIErrorCode"
if strings.HasPrefix(name.Name, "APIErrorCode") {
errCodeCount++
}
}
}
}
This time we will check the exact type of the constants, not their names. We will also gather all the constant values, and in the end we will check each if that exact constant value is in the map. If something is missing, we will print the exact values of the missing codes.
So here it is:
func TestMap(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
if err != nil {
t.Errorf("Failed to parse file: %v", err)
return
}
var keys []APIErrorCode
// Range through declarations:
for _, dd := range f.Decls {
gd, ok := dd.(*ast.GenDecl)
if !ok {
continue
}
// Find constant declrations:
if gd.Tok != token.CONST {
continue
}
for _, sp := range gd.Specs {
// Filter by APIErrorCode type:
valSp, ok := sp.(*ast.ValueSpec)
if !ok {
continue
}
if id, ok2 := valSp.Type.(*ast.Ident); !ok2 ||
id.Name != "APIErrorCode" {
continue
}
// And gather the constant values in keys:
for _, value := range valSp.Values {
bslit, ok := value.(*ast.BasicLit)
if !ok {
continue
}
keyValue, err := strconv.Atoi(bslit.Value)
if err != nil {
t.Errorf("Could not parse value from %v: %v",
bslit.Value, err)
}
keys = append(keys, APIErrorCode(keyValue))
}
}
}
for _, key := range keys {
if _, found := APIErrorCodeMessages[key]; !found {
t.Errorf("Could not found key in map: %v", key)
}
}
}
Running go test
with an "incomplete" APIErrorCodeMessages
map, we get the following output:
--- FAIL: TestMap (0.00s)
errcodes_test.go:58: Could not found key in map: 1000
Short of static code analysis, which generates your tests, you can't.
You'll just need to maintain a list of known types somewhere. The most obvious place is probably in your test:
func TestAPICodes(t *testing.T) {
for _, code := range []APIErrorCode{APIErrorCodeAuthentication, ...} {
// Do your test here
}
}
If you want the list defined closer to the code definitions, you could also put it in your main package:
// APIErrorCode represents the API error code
type APIErrorCode int
const (
// APIErrorCodeAuthentication represents an authentication error and corresponds with HTTP 401
APIErrorCodeAuthentication APIErrorCode = 1000
// APIErrorCodeInternalError represents an unknown internal error and corresponds with HTTP 500
APIErrorCodeInternalError APIErrorCode = 1001
)
var allCodes = []APIErrorCode{APIErrorCodeAuthentication, ...}
Or, if you're confident that your APIErrorCodeMessages
map will be kept up-to-date, then you already have the solution. Just loop over that map in your test:
func TestAPICodes(t *testing.T) {
for code := range APIErrorCodeMessages {
// Do your tests...
}
}