如何枚举某种类型的常量

时间:2017-04-24 09:26:28

标签: testing go reflection

我希望通过测试确保对于每个APIErrorCode常量定义如下,地图APIErrorCodeMessages包含一个条目。如何在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",
}

我已查看reflectgo/importer并尝试tools/cmd/stringer但未成功。

2 个答案:

答案 0 :(得分:1)

基本概念

reflect包不提供对导出标识符的访问,因为不能保证它们将链接到可执行二进制文件(因此在运行时可用);更多内容:Splitting client/server code;和How to remove unused code at compile time?

这是源代码级别检查。我要做的是编写一个测试,检查错误代码常量的数量是否与地图长度匹配。以下解决方案仅检查地图长度。改进版本(见下文)也可以检查地图中的键是否也与常量声明的值匹配。

您可以使用go/parser来解析包含错误代码常量的Go文件,它会为您提供描述文件的ast.File,其中包含常量声明。您只需要遍历它,并计算错误代码常量声明。

假设您的原始文件名为"errcodes.go",请写一个名为"errcodes_test.go"的测试文件。

这是测试功能的样子:

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)
    }
}

运行go test将导致:

--- FAIL: TestMap (0.00s)
    errcodes_test.go:39: Expected 2 err codes, got: 1

测试正确显示有2个常量错误代码声明,但APIErrorCodeMessages映射只包含1个条目。

如果我们现在“完成”地图:

var APIErrorCodeMessages = map[APIErrorCode]string{
    APIErrorCodeInternalError:  "Internal Error",
    APIErrorCodeAuthentication: "asdf",
}

再次运行go test

PASS

注意:这是一个风格问题,但是可以用这种方式编写大循环以降低嵌套级别:

// 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++
            }
        }
    }
}

完整,改进的检测

这次我们将检查常量的确切类型,而不是它们的名称。我们还将收集所有常量值,最后我们将检查每个值是否在地图中确切的常量值。如果缺少某些内容,我们将打印缺失代码的确切值。

所以这是:

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)
        }
    }
}

使用“不完整”go test地图运行APIErrorCodeMessages,我们得到以下输出:

--- FAIL: TestMap (0.00s)
    errcodes_test.go:58: Could not found key in map: 1000

答案 1 :(得分:0)

缺少静态代码分析,无法生成测试,您可以

您只需要在某处维护已知类型的列表。最明显的地方可能是你的考试:

func TestAPICodes(t *testing.T) {
    for _, code := range []APIErrorCode{APIErrorCodeAuthentication, ...} {
        // Do your test here
    }
}

如果您希望列表更接近代码定义,您也可以将它放在主包中:

// 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, ...}

或者,如果您确信自己的APIErrorCodeMessages地图会保持最新状态,那么您已经拥有了解决方案。只需在测试中循环遍历该地图:

func TestAPICodes(t *testing.T) {
    for code := range APIErrorCodeMessages {
        // Do your tests...
    }
}