Merge pull request #364 from bilibili/go-common/testing_20199197322

この全ては既に世界の選択だ!
pull/325/head
Terry.Mao 5 years ago committed by GitHub
commit a0a87b8520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      go.mod
  2. 4
      go.sum
  3. 48
      pkg/cache/memcache/test/BUILD.bazel
  4. 9
      pkg/cache/memcache/test/docker-compose.yaml
  5. 4
      pkg/testing/lich/README.md
  6. 125
      pkg/testing/lich/composer.go
  7. 85
      pkg/testing/lich/healthcheck.go
  8. 88
      pkg/testing/lich/model.go
  9. 154
      tool/testcli/README.MD
  10. 26
      tool/testcli/docker-compose.yaml
  11. 50
      tool/testcli/main.go
  12. 52
      tool/testgen/README.md
  13. 419
      tool/testgen/gen.go
  14. 57
      tool/testgen/main.go
  15. 193
      tool/testgen/parser.go
  16. 41
      tool/testgen/templete.go
  17. 42
      tool/testgen/utils.go

@ -27,6 +27,7 @@ require (
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/montanaflynn/stats v0.5.0
github.com/openzipkin/zipkin-go v0.2.1
github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3
github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v1.1.0
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect
@ -44,6 +45,7 @@ require (
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2
golang.org/x/time v0.0.0-20190513212739-9d24e82272b4 // indirect
golang.org/x/tools v0.0.0-20190912185636-87d9f09c5d89
google.golang.org/appengine v1.6.1 // indirect
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610
google.golang.org/grpc v1.23.1

@ -168,6 +168,8 @@ github.com/openconfig/gnmi v0.0.0-20190823184014-89b2bf29312c/go.mod h1:t+O9It+L
github.com/openconfig/reference v0.0.0-20190727015836-8dfd928c9696/go.mod h1:ym2A+zigScwkSEb/cVQB0/ZMpU3rqiH6X7WRRsxgOGw=
github.com/openzipkin/zipkin-go v0.2.1 h1:noL5/5Uf1HpVl3wNsfkZhIKbSWCVi5jgqkONNx8PXcA=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3 h1:zjmNboC3QFuMdJSaZJ7Qvi3HUxWXPdj7wb3rc4jH5HI=
github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3/go.mod h1:pLR8n2aimFxvvDJ6n8JuQWthMGezCYMjuhlaTjPTZf0=
github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -307,7 +309,9 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b h1:mSUCVIwDx4hfXJfWsOPfdzEHxzb2Xjl6BQ8YgPnazQA=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190912185636-87d9f09c5d89 h1:WiVZGyzQN7gPNLRkkpsNX3jC0Jx5j9GxadCZW/8eXw0=
golang.org/x/tools v0.0.0-20190912185636-87d9f09c5d89/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=

@ -1,48 +0,0 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
go_library(
name = "go_default_library",
srcs = [],
embed = [":proto_go_proto"],
importpath = "go-common/library/cache/memcache/test",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["@com_github_golang_protobuf//proto:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
proto_library(
name = "test_proto",
srcs = ["test.proto"],
import_prefix = "go-common/library/cache/memcache/test",
strip_import_prefix = "",
tags = ["automanaged"],
)
go_proto_library(
name = "proto_go_proto",
compilers = ["@io_bazel_rules_go//proto:go_proto"],
importpath = "go-common/library/cache/memcache/test",
proto = ":test_proto",
tags = ["automanaged"],
)

@ -0,0 +1,9 @@
version: "3.7"
services:
mc:
image: memcached:1
ports:
- 11211:11211

@ -0,0 +1,4 @@
## testing/lich 运行环境构建
基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。
使用说明参见:https://github.com/bilibili/kratos/tree/master/tool/testcli/README.md

@ -0,0 +1,125 @@
package lich
import (
"bytes"
"crypto/md5"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/bilibili/kratos/pkg/log"
)
var (
retry int
noDown bool
yamlPath string
pathHash string
services map[string]*Container
)
func init() {
flag.StringVar(&yamlPath, "f", "docker-compose.yaml", "composer yaml path.")
flag.BoolVar(&noDown, "nodown", false, "containers are not recycled.")
}
func runCompose(args ...string) (output []byte, err error) {
if _, err = os.Stat(yamlPath); os.IsNotExist(err) {
log.Error("os.Stat(%s) composer yaml is not exist!", yamlPath)
return
}
if yamlPath, err = filepath.Abs(yamlPath); err != nil {
log.Error("filepath.Abs(%s) error(%v)", yamlPath, err)
return
}
pathHash = fmt.Sprintf("%x", md5.Sum([]byte(yamlPath)))[:9]
args = append([]string{"-f", yamlPath, "-p", pathHash}, args...)
if output, err = exec.Command("docker-compose", args...).CombinedOutput(); err != nil {
log.Error("exec.Command(docker-compose) args(%v) stdout(%s) error(%v)", args, string(output), err)
return
}
return
}
// Setup setup UT related environment dependence for everything.
func Setup() (err error) {
if _, err = runCompose("up", "-d"); err != nil {
return
}
defer func() {
if err != err {
go Teardown()
}
}()
if _, err = getServices(); err != nil {
return
}
_, err = checkServices()
return
}
// Teardown unsetup all environment dependence.
func Teardown() (err error) {
if !noDown {
_, err = runCompose("down")
}
return
}
func getServices() (output []byte, err error) {
if output, err = runCompose("config", "--services"); err != nil {
return
}
services = make(map[string]*Container)
output = bytes.TrimSpace(output)
for _, svr := range bytes.Split(output, []byte("\n")) {
if output, err = runCompose("ps", "-a", "-q", string(svr)); err != nil {
return
}
var (
id = string(bytes.TrimSpace(output))
args = []string{"inspect", id, "--format", "'{{json .}}'"}
)
if output, err = exec.Command("docker", args...).CombinedOutput(); err != nil {
log.Error("exec.Command(docker) args(%v) stdout(%s) error(%v)", args, string(output), err)
return
}
if output = bytes.TrimSpace(output); bytes.Equal(output, []byte("")) {
err = fmt.Errorf("service: %s | container: %s fails to launch", svr, id)
log.Error("exec.Command(docker) args(%v) error(%v)", args, err)
return
}
var c = &Container{}
if err = json.Unmarshal(bytes.Trim(output, "'"), c); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", string(output), err)
return
}
services[string(svr)] = c
}
return
}
func checkServices() (output []byte, err error) {
defer func() {
if err != nil && retry < 4 {
retry++
getServices()
time.Sleep(time.Second * 5)
output, err = checkServices()
return
}
retry = 0
}()
for svr, c := range services {
if err = c.Healthcheck(); err != nil {
log.Error("healthcheck(%s) error(%v) retrying %d times...", svr, err, 5-retry)
return
}
// TODO About container check and more...
}
return
}

@ -0,0 +1,85 @@
package lich
import (
"database/sql"
"fmt"
"net"
"strconv"
"strings"
"github.com/bilibili/kratos/pkg/log"
// Register go-sql-driver stuff
_ "github.com/go-sql-driver/mysql"
)
var healthchecks = map[string]func(*Container) error{"mysql": checkMysql, "mariadb": checkMysql}
// Healthcheck check container health.
func (c *Container) Healthcheck() (err error) {
if status, health := c.State.Status, c.State.Health.Status; !c.State.Running || (health != "" && health != "healthy") {
err = fmt.Errorf("service: %s | container: %s not running", c.GetImage(), c.GetID())
log.Error("docker status(%s) health(%s) error(%v)", status, health, err)
return
}
if check, ok := healthchecks[c.GetImage()]; ok {
err = check(c)
return
}
for proto, ports := range c.NetworkSettings.Ports {
if id := c.GetID(); !strings.Contains(proto, "tcp") {
log.Error("container: %s proto(%s) unsupported.", id, proto)
continue
}
for _, publish := range ports {
var (
ip = net.ParseIP(publish.HostIP)
port, _ = strconv.Atoi(publish.HostPort)
tcpAddr = &net.TCPAddr{IP: ip, Port: port}
tcpConn *net.TCPConn
)
if tcpConn, err = net.DialTCP("tcp", nil, tcpAddr); err != nil {
log.Error("net.DialTCP(%s:%s) error(%v)", publish.HostIP, publish.HostPort, err)
return
}
tcpConn.Close()
}
}
return
}
func checkMysql(c *Container) (err error) {
var ip, port, user, passwd string
for _, env := range c.Config.Env {
splits := strings.Split(env, "=")
if strings.Contains(splits[0], "MYSQL_ROOT_PASSWORD") {
user, passwd = "root", splits[1]
continue
}
if strings.Contains(splits[0], "MYSQL_ALLOW_EMPTY_PASSWORD") {
user, passwd = "root", ""
continue
}
if strings.Contains(splits[0], "MYSQL_USER") {
user = splits[1]
continue
}
if strings.Contains(splits[0], "MYSQL_PASSWORD") {
passwd = splits[1]
continue
}
}
var db *sql.DB
if ports, ok := c.NetworkSettings.Ports["3306/tcp"]; ok {
ip, port = ports[0].HostIP, ports[0].HostPort
}
var dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/", user, passwd, ip, port)
if db, err = sql.Open("mysql", dsn); err != nil {
log.Error("sql.Open(mysql) dsn(%s) error(%v)", dsn, err)
return
}
if err = db.Ping(); err != nil {
log.Error("ping(db) dsn(%s) error(%v)", dsn, err)
}
defer db.Close()
return
}

@ -0,0 +1,88 @@
package lich
import (
"strings"
"time"
)
// Container docker inspect resp.
type Container struct {
ID string `json:"Id"`
Created time.Time `json:"Created"`
Path string `json:"Path"`
Args []string `json:"Args"`
State struct {
Status string `json:"Status"`
Running bool `json:"Running"`
Paused bool `json:"Paused"`
Restarting bool `json:"Restarting"`
OOMKilled bool `json:"OOMKilled"`
Dead bool `json:"Dead"`
Pid int `json:"Pid"`
ExitCode int `json:"ExitCode"`
Error string `json:"Error"`
StartedAt time.Time `json:"StartedAt"`
FinishedAt time.Time `json:"FinishedAt"`
Health struct {
Status string `json:"Status"`
FailingStreak int `json:"FailingStreak"`
Log []struct {
Start time.Time `json:"Start"`
End time.Time `json:"End"`
ExitCode int `json:"ExitCode"`
Output string `json:"Output"`
} `json:"Log"`
} `json:"Health"`
} `json:"State"`
Config struct {
Hostname string `json:"Hostname"`
Domainname string `json:"Domainname"`
User string `json:"User"`
Tty bool `json:"Tty"`
OpenStdin bool `json:"OpenStdin"`
StdinOnce bool `json:"StdinOnce"`
Env []string `json:"Env"`
Cmd []string `json:"Cmd"`
Image string `json:"Image"`
WorkingDir string `json:"WorkingDir"`
Entrypoint []string `json:"Entrypoint"`
} `json:"Config"`
Image string `json:"Image"`
ResolvConfPath string `json:"ResolvConfPath"`
HostnamePath string `json:"HostnamePath"`
HostsPath string `json:"HostsPath"`
LogPath string `json:"LogPath"`
Name string `json:"Name"`
RestartCount int `json:"RestartCount"`
Driver string `json:"Driver"`
Platform string `json:"Platform"`
MountLabel string `json:"MountLabel"`
ProcessLabel string `json:"ProcessLabel"`
AppArmorProfile string `json:"AppArmorProfile"`
NetworkSettings struct {
Bridge string `json:"Bridge"`
SandboxID string `json:"SandboxID"`
HairpinMode bool `json:"HairpinMode"`
Ports map[string][]struct {
HostIP string `json:"HostIp"`
HostPort string `json:"HostPort"`
} `json:"Ports"`
} `json:"NetworkSettings"`
}
// GetImage get image name at container
func (c *Container) GetImage() (image string) {
image = c.Config.Image
if images := strings.Split(image, ":"); len(images) > 0 {
image = images[0]
}
return
}
// GetID get id at container
func (c *Container) GetID() (id string) {
if id = c.ID; len(id) > 9 {
id = id[0:9]
}
return
}

@ -0,0 +1,154 @@
## testcli UT运行环境构建工具
基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。
*这个是testing/lich的二进制工具版本(Go请直接使用库版本:github.com/bilibili/kratos/pkg/testing/lich)*
### 功能和特性
- 自动读取 test 目录下的 yaml 并启动依赖
- 自动导入 test 目录下的 DB 初始化 SQL
- 提供特定容器内的 healthcheck (mysql, mc, redis)
- 提供一站式解决 UT 服务依赖的工具版本 (testcli)
### 编译安装
*使用本工具/库需要前置安装好 docker & docker-compose@v1.24.1^*
#### Method 1. With go get
```shell
go get -u github.com/bilibili/kratos/tool/testcli
$GOPATH/bin/testcli -h
```
#### Method 2. Build with Go
```shell
cd github.com/bilibili/kratos/tool/testcli
go build -o $GOPATH/bin/testcli
$GOPATH/bin/testcli -h
```
#### Method 3. Import with Kratos pkg
```Go
import "github.com/bilibili/kratos/pkg/testing/lich"
```
### 构建数据
#### Step 1. create docker-compose.yml
创建依赖服务的 docker-compose.yml,并把它放在项目路径下的 test 文件夹下面。例如:
```shell
mkdir -p $YOUR_PROJECT/test
```
```yaml
version: "3.7"
services:
db:
image: mysql:5.6
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=root
volumes:
- .:/docker-entrypoint-initdb.d
command: [
'--character-set-server=utf8',
'--collation-server=utf8_unicode_ci'
]
redis:
image: redis
ports:
- 6379:6379
```
一般来讲,我们推荐在项目根目录创建 test 目录,里面存放描述服务的yml,以及需要初始化的数据(database.sql等)。
同时也需要注意,正确的对容器内服务进行健康检测,testcli会在容器的health状态执行UT,其实我们也内置了针对几个较为通用镜像(mysql mariadb mc redis)的健康检测,也就是不写也没事(^^;;
#### Step 2. export database.sql
构造初始化的数据(database.sql等),当然也把它也在 test 文件夹里。
```sql
CREATE DATABASE IF NOT EXISTS `YOUR_DATABASE_NAME`;
SET NAMES 'utf8';
USE `YOUR_DATABASE_NAME`;
CREATE TABLE IF NOT EXISTS `YOUR_TABLE_NAME` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='YOUR_TABLE_NAME';
```
这里需要注意,在创建库/表的时候尽量加上 IF NOT EXISTS,以给予一定程度的容错,以及 SET NAMES 'utf8'; 用于解决客户端连接乱码问题。
#### Step 3. change your project mysql config
```toml
[mysql]
addr = "127.0.0.1:3306"
dsn = "root:root@tcp(127.0.0.1:3306)/YOUR_DATABASE?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8"
active = 20
idle = 10
idleTimeout ="1s"
queryTimeout = "1s"
execTimeout = "1s"
tranTimeout = "1s"
```
在 *Step 1* 我们已经指定了服务对外暴露的端口为3306(这当然也可以是你指定的任何值),那理所应当的我们也要修改项目连接数据库的配置~
Great! 至此你已经完成了运行所需要用到的数据配置,接下来就来运行它。
### 运行
开头也说过本工具支持两种运行方式:testcli 二进制工具版本和 go package 源码包,业务方可以根据需求场景进行选择。
#### Method 1. With testcli tool
*已支持的 flag: -f,--nodown,down,run*
- -f,指定 docker-compose.yaml 文件路径,默认为当前目录下。
- --nodown,指定是否在UT执行完成后保留容器,以供下次复用。
- down,teardown 销毁当前项目下这个 compose 文件产生的容器。
- run,运行你当前语言的单测执行命令(如:golang为 go test -v ./)
example:
```shell
testcli -f ../../test/docker-compose.yaml run go test -v ./
```
#### Method 2. Import with Kratos pkg
- Step1. 在 Dao|Service 层中的 TestMain 单测主入口中,import "go-common/library/testing/lich" 引入testcli工具的go库版本。
- Step2. 使用 flag.Set("f", "../../test/docker-compose.yaml") 指定 docker-compose.yaml 文件的路径。
- Step3. 在 flag.Parse() 后即可使用 lich.Setup() 安装依赖&初始化数据(注意测试用例执行结束后 lich.Teardown() 回收下~)
- Step4. 运行 `go test -v ./ `看看效果吧~
example:
```Go
package dao
import (
"flag"
"os"
"strings"
"testing"
"github.com/bilibili/kratos/pkg/conf/paladin"
"github.com/bilibili/kratos/pkg/testing/lich"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
flag.Set("conf", "../../configs")
flag.Set("f", "../../test/docker-compose.yaml")
flag.Parse()
if err := paladin.Init(); err != nil {
panic(err)
}
if err := lich.Setup(); err != nil {
panic(err)
}
defer lich.Teardown()
d = New()
if code := m.Run(); code != 0 {
panic(code)
}
}
```
## 注意
因为启动mysql容器较为缓慢,健康检测的机制会重试3次,每次暂留5秒钟,基本在10s内mysql就能从creating到服务正常启动!
当然你也可以在使用 testcli 时加上 --nodown,使其不用每次跑都新建容器,只在第一次跑的时候会初始化容器,后面都进行复用,这样速度会快很多。
成功启动后就欢乐奔放的玩耍吧~ Good Lucky!

@ -0,0 +1,26 @@
version: "3.7"
services:
db:
image: mysql:5.6
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=root
- TZ=Asia/Shanghai
volumes:
- .:/docker-entrypoint-initdb.d
command: [
'--character-set-server=utf8',
'--collation-server=utf8_unicode_ci'
]
redis:
image: redis
ports:
- 6379:6379
memcached:
image: memcached
ports:
- 11211:11211

@ -0,0 +1,50 @@
package main
import (
"flag"
"os"
"os/exec"
"strings"
"github.com/bilibili/kratos/pkg/testing/lich"
)
func parseArgs() (flags map[string]string) {
flags = make(map[string]string)
for idx, arg := range os.Args {
if idx == 0 {
continue
}
if arg == "down" {
flags["down"] = ""
return
}
if cmds := os.Args[idx+1:]; arg == "run" {
flags["run"] = strings.Join(cmds, " ")
return
}
}
return
}
func main() {
flag.Parse()
flags := parseArgs()
if _, ok := flags["down"]; ok {
lich.Teardown()
return
}
if cmd, ok := flags["run"]; !ok || cmd == "" {
panic("Your need 'run' flag assign to be run commands.")
}
if err := lich.Setup(); err != nil {
panic(err)
}
defer lich.Teardown()
cmds := strings.Split(flags["run"], " ")
cmd := exec.Command(cmds[0], cmds[1:]...)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}

@ -0,0 +1,52 @@
## testgen UT代码自动生成器
解放你的双手,让你的UT一步到位!
### 功能和特性
- 支持生成 Dao|Service 层UT代码功能(每个方法包含一个正向用例)
- 支持生成 Dao|Service 层测试入口文件dao_test.go, service_test.go(用于控制初始化,控制测试流程等)
- 支持生成Mock代码(使用GoMock框架)
- 支持选择不同模式生成不同代码(使用"–m mode"指定)
- 生成单元测试代码时,同时支持传入目录或文件
- 支持指定方法追加生成测试用例(使用"–func funcName"指定)
### 编译安装
#### Method 1. With go get
```shell
go get -u github.com/bilibili/kratos/tool/testgen
$GOPATH/bin/testgen -h
```
#### Method 2. Build with Go
```shell
cd github.com/bilibili/kratos/tool/testgen
go build -o $GOPATH/bin/testgen
$GOPATH/bin/testgen -h
```
### 运行
#### 生成Dao/Service层单元UT
```shell
$GOPATH/bin/testgen YOUR_PROJECT/dao # default mode
$GOPATH/bin/testgen --m test path/to/your/pkg
$GOPATH/bin/testgen --func functionName path/to/your/pkg
```
#### 生成接口类型
```shell
$GOPATH/bin/testgen --m interface YOUR_PROJECT/dao #当前仅支持传目录,如目录包含子目录也会做处理
```
#### 生成Mock代码
```shell
$GOPATH/bin/testgen --m mock YOUR_PROJECT/dao #仅传入包路径即可
```
#### 生成Monkey代码
```shell
$GOPATH/bin/testgen --m monkey yourCodeDirPath #仅传入包路径即可
```
### 赋诗一首
```
莫生气 莫生气
代码辣鸡非我意
自己动手分田地
谈笑风生活长命
```

@ -0,0 +1,419 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/otokaze/mock/mockgen"
"github.com/otokaze/mock/mockgen/model"
)
func genTest(parses []*parse) (err error) {
for _, p := range parses {
switch {
case strings.HasSuffix(p.Path, "_mock.go") ||
strings.HasSuffix(p.Path, ".intf.go"):
continue
case strings.HasSuffix(p.Path, "dao.go") ||
strings.HasSuffix(p.Path, "service.go"):
err = p.genTestMain()
default:
err = p.genUTTest()
}
if err != nil {
break
}
}
return
}
func (p *parse) genUTTest() (err error) {
var (
buffer bytes.Buffer
impts = strings.Join([]string{
`"context"`,
`"testing"`,
`. "github.com/smartystreets/goconvey/convey"`,
}, "\n\t")
content []byte
)
filename := strings.Replace(p.Path, ".go", "_test.go", -1)
if _, err = os.Stat(filename); (_func == "" && err == nil) ||
(err != nil && os.IsExist(err)) {
err = nil
return
}
for _, impt := range p.Imports {
impts += "\n\t\"" + impt.V + "\""
}
if _func == "" {
buffer.WriteString(fmt.Sprintf(tpPackage, p.Package))
buffer.WriteString(fmt.Sprintf(tpImport, impts))
}
for _, parseFunc := range p.Funcs {
if _func != "" && _func != parseFunc.Name {
continue
}
var (
methodK string
tpVars string
vars []string
val []string
notice = "Then "
reset string
)
if method := ConvertMethod(p.Path); method != "" {
methodK = method + "."
}
tpTestFuncs := fmt.Sprintf(tpTestFunc, strings.Title(p.Package), parseFunc.Name, "", parseFunc.Name, "%s", "%s", "%s")
tpTestFuncBeCall := methodK + parseFunc.Name + "(%s)\n\t\t\tConvey(\"%s\", func() {"
if parseFunc.Result == nil {
tpTestFuncBeCall = fmt.Sprintf(tpTestFuncBeCall, "%s", "No return values")
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", tpTestFuncBeCall, "%s")
}
for k, res := range parseFunc.Result {
if res.K == "" {
res.K = fmt.Sprintf("p%d", k+1)
}
var so string
if res.V == "error" {
res.K = "err"
so = fmt.Sprintf("\tSo(%s, ShouldBeNil)", res.K)
notice += "err should be nil."
} else {
so = fmt.Sprintf("\tSo(%s, ShouldNotBeNil)", res.K)
val = append(val, res.K)
}
if len(parseFunc.Result) <= k+1 {
if len(val) != 0 {
notice += strings.Join(val, ",") + " should not be nil."
}
tpTestFuncBeCall = fmt.Sprintf(tpTestFuncBeCall, "%s", notice)
res.K += " := " + tpTestFuncBeCall
} else {
res.K += ", %s"
}
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", res.K+"\n\t\t\t%s", "%s")
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", "%s", so, "%s")
}
if parseFunc.Params == nil {
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", "", "%s")
}
for k, pType := range parseFunc.Params {
if pType.K == "" {
pType.K = fmt.Sprintf("a%d", k+1)
}
var (
init string
params = pType.K
)
switch {
case strings.HasPrefix(pType.V, "context"):
init = params + " = context.Background()"
case strings.HasPrefix(pType.V, "[]byte"):
init = params + " = " + pType.V + "(\"\")"
case strings.HasPrefix(pType.V, "[]"):
init = params + " = " + pType.V + "{}"
case strings.HasPrefix(pType.V, "int") ||
strings.HasPrefix(pType.V, "uint") ||
strings.HasPrefix(pType.V, "float") ||
strings.HasPrefix(pType.V, "double"):
init = params + " = " + pType.V + "(0)"
case strings.HasPrefix(pType.V, "string"):
init = params + " = \"\""
case strings.Contains(pType.V, "*xsql.Tx"):
init = params + ",_ = " + methodK + "BeginTran(c)"
reset += "\n\t" + params + ".Commit()"
case strings.HasPrefix(pType.V, "*"):
init = params + " = " + strings.Replace(pType.V, "*", "&", -1) + "{}"
case strings.Contains(pType.V, "chan"):
init = params + " = " + pType.V
case pType.V == "time.Time":
init = params + " = time.Now()"
case strings.Contains(pType.V, "chan"):
init = params + " = " + pType.V
default:
init = params + " " + pType.V
}
vars = append(vars, "\t\t"+init)
if len(parseFunc.Params) > k+1 {
params += ", %s"
}
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", params, "%s")
}
if len(vars) > 0 {
tpVars = fmt.Sprintf(tpVar, strings.Join(vars, "\n\t"))
}
tpTestFuncs = fmt.Sprintf(tpTestFuncs, tpVars, "%s")
if reset != "" {
tpTestResets := fmt.Sprintf(tpTestReset, reset)
tpTestFuncs = fmt.Sprintf(tpTestFuncs, tpTestResets)
} else {
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "")
}
buffer.WriteString(tpTestFuncs)
}
var (
file *os.File
flag = os.O_RDWR | os.O_CREATE | os.O_APPEND
)
if file, err = os.OpenFile(filename, flag, 0644); err != nil {
return
}
if _func == "" {
content, _ = GoImport(filename, buffer.Bytes())
} else {
content = buffer.Bytes()
}
if _, err = file.Write(content); err != nil {
return
}
if err = file.Close(); err != nil {
return
}
return
}
func (p *parse) genTestMain() (err error) {
var (
new bool
buffer bytes.Buffer
impts string
vars, mainFunc string
content []byte
instance, confFunc string
tomlPath = "**PUT PATH TO YOUR CONFIG FILES HERE**"
filename = strings.Replace(p.Path, ".go", "_test.go", -1)
)
if p.Imports["paladin"] != nil {
new = true
}
// if _intfMode {
// imptsList = append(imptsList, `"github.com/golang/mock/gomock"`)
// for _, field := range p.Structs {
// var hit bool
// pkgName := strings.Split(field.V, ".")[0]
// interfaceName := strings.Split(field.V, ".")[1]
// if p.Imports[pkgName] != nil {
// if hit, err = checkInterfaceMock(strings.Split(field.V, ".")[1], p.Imports[pkgName].V); err != nil {
// return
// }
// }
// if hit {
// imptsList = append(imptsList, "mock"+p.Imports[pkgName].K+" \""+p.Imports[pkgName].V+"/mock\"")
// pkgName = "mock" + strings.Title(pkgName)
// interfaceName = "Mock" + interfaceName
// varsList = append(varsList, "mock"+strings.Title(field.K)+" *"+pkgName+"."+interfaceName)
// mockStmt += "\tmock" + strings.Title(field.K) + " = " + pkgName + ".New" + interfaceName + "(mockCtrl)\n"
// newStmt += "\t\t" + field.K + ":\tmock" + strings.Title(field.K) + ",\n"
// } else {
// pkgName = subString(field.V, "*", ".")
// if p.Imports[pkgName] != nil && pkgName != "conf" {
// imptsList = append(imptsList, p.Imports[pkgName].K+" \""+p.Imports[pkgName].V+"\"")
// }
// switch {
// case strings.HasPrefix(field.V, "*conf."):
// newStmt += "\t\t" + field.K + ":\tconf.Conf,\n"
// case strings.HasPrefix(field.V, "*"):
// newStmt += "\t\t" + field.K + ":\t" + strings.Replace(field.V, "*", "&", -1) + "{},\n"
// default:
// newStmt += "\t\t" + field.K + ":\t" + field.V + ",\n"
// }
// }
// }
// mockStmt = fmt.Sprintf(_tpTestServiceMainMockStmt, mockStmt)
// newStmt = fmt.Sprintf(_tpTestServiceMainNewStmt, newStmt)
// }
if instance = ConvertMethod(p.Path); instance == "s" {
vars = strings.Join([]string{"s *Service"}, "\n\t")
mainFunc = tpTestServiceMain
} else {
vars = strings.Join([]string{"d *Dao"}, "\n\t")
mainFunc = tpTestDaoMain
}
if new {
impts = strings.Join([]string{`"os"`, `"flag"`, `"testing"`, p.Imports["paladin"].V}, "\n\t")
confFunc = fmt.Sprintf(tpTestMainNew, instance+" = New()")
} else {
impts = strings.Join(append([]string{`"os"`, `"flag"`, `"testing"`}), "\n\t")
confFunc = fmt.Sprintf(tpTestMainOld, instance+" = New(conf.Conf)")
}
if _, err := os.Stat(filename); os.IsNotExist(err) {
buffer.WriteString(fmt.Sprintf(tpPackage, p.Package))
buffer.WriteString(fmt.Sprintf(tpImport, impts))
buffer.WriteString(fmt.Sprintf(tpVar, vars))
buffer.WriteString(fmt.Sprintf(mainFunc, tomlPath, confFunc))
content, _ = GoImport(filename, buffer.Bytes())
ioutil.WriteFile(filename, content, 0644)
}
return
}
func genInterface(parses []*parse) (err error) {
var (
parse *parse
pkg = make(map[string]string)
)
for _, parse = range parses {
if strings.Contains(parse.Path, ".intf.go") {
continue
}
dirPath := filepath.Dir(parse.Path)
for _, parseFunc := range parse.Funcs {
if (parseFunc.Method == nil) ||
!(parseFunc.Name[0] >= 'A' && parseFunc.Name[0] <= 'Z') {
continue
}
var (
params string
results string
)
for k, param := range parseFunc.Params {
params += param.K + " " + param.P + param.V
if len(parseFunc.Params) > k+1 {
params += ", "
}
}
for k, res := range parseFunc.Result {
results += res.K + " " + res.P + res.V
if len(parseFunc.Result) > k+1 {
results += ", "
}
}
if len(results) != 0 {
results = "(" + results + ")"
}
pkg[dirPath] += "\t" + fmt.Sprintf(tpIntfcFunc, parseFunc.Name, params, results)
}
}
for k, v := range pkg {
var buffer bytes.Buffer
pathSplit := strings.Split(k, "/")
filename := k + "/" + pathSplit[len(pathSplit)-1] + ".intf.go"
if _, exist := os.Stat(filename); os.IsExist(exist) {
continue
}
buffer.WriteString(fmt.Sprintf(tpPackage, pathSplit[len(pathSplit)-1]))
buffer.WriteString(fmt.Sprintf(tpInterface, strings.Title(pathSplit[len(pathSplit)-1]), v))
content, _ := GoImport(filename, buffer.Bytes())
err = ioutil.WriteFile(filename, content, 0644)
}
return
}
func genMock(files ...string) (err error) {
for _, file := range files {
var pkg *model.Package
if pkg, err = mockgen.ParseFile(file); err != nil {
return
}
if len(pkg.Interfaces) == 0 {
continue
}
var mockDir = pkg.SrcDir + "/mock"
if _, err = os.Stat(mockDir); os.IsNotExist(err) {
err = nil
os.Mkdir(mockDir, 0744)
}
var mockPath = mockDir + "/" + pkg.Name + "_mock.go"
if _, exist := os.Stat(mockPath); os.IsExist(exist) {
continue
}
var g = &mockgen.Generator{Filename: file}
if err = g.Generate(pkg, "mock", mockPath); err != nil {
return
}
if err = ioutil.WriteFile(mockPath, g.Output(), 0644); err != nil {
return
}
}
return
}
func genMonkey(parses []*parse) (err error) {
var (
pkg = make(map[string]string)
)
for _, parse := range parses {
if strings.Contains(parse.Path, "monkey.go") ||
strings.Contains(parse.Path, "/mock/") {
continue
}
var (
path = strings.Split(filepath.Dir(parse.Path), "/")
pack = ConvertHump(path[len(path)-1])
refer = path[len(path)-1]
mockVar, mockType, srcDir string
)
for i := len(path) - 1; i > len(path)-4; i-- {
if path[i] == "dao" || path[i] == "service" {
srcDir = strings.Join(path[:i+1], "/")
break
}
pack = ConvertHump(path[i-1]) + pack
}
if mockVar = ConvertMethod(parse.Path); mockType == "d" {
mockType = "*" + refer + ".Dao"
} else {
mockType = "*" + refer + ".Service"
}
for _, parseFunc := range parse.Funcs {
if (parseFunc.Method == nil) || (parseFunc.Result == nil) ||
!(parseFunc.Name[0] >= 'A' && parseFunc.Name[0] <= 'Z') {
continue
}
var (
funcParams, funcResults, mockKey, mockValue, funcName string
)
funcName = pack + parseFunc.Name
for k, param := range parseFunc.Params {
funcParams += "_ " + param.V
if len(parseFunc.Params) > k+1 {
funcParams += ", "
}
}
for k, res := range parseFunc.Result {
if res.K == "" {
if res.V == "error" {
res.K = "err"
} else {
res.K = fmt.Sprintf("p%d", k+1)
}
}
mockKey += res.K
mockValue += res.V
funcResults += res.K + " " + res.P + res.V
if len(parseFunc.Result) > k+1 {
mockKey += ", "
mockValue += ", "
funcResults += ", "
}
}
pkg[srcDir+"."+refer] += fmt.Sprintf(tpMonkeyFunc, funcName, funcName, mockVar, mockType, funcResults, mockVar, parseFunc.Name, mockType, funcParams, mockValue, mockKey)
}
}
for path, content := range pkg {
var (
buffer bytes.Buffer
dir = strings.Split(path, ".")
mockDir = dir[0] + "/mock"
filename = mockDir + "/monkey_" + dir[1] + ".go"
)
if _, err = os.Stat(mockDir); os.IsNotExist(err) {
err = nil
os.Mkdir(mockDir, 0744)
}
if _, err := os.Stat(filename); os.IsExist(err) {
continue
}
buffer.WriteString(fmt.Sprintf(tpPackage, "mock"))
buffer.WriteString(content)
content, _ := GoImport(filename, buffer.Bytes())
ioutil.WriteFile(filename, content, 0644)
}
return
}

@ -0,0 +1,57 @@
package main
import (
"flag"
"fmt"
"os"
)
var (
err error
_mode, _func string
files []string
parses []*parse
)
func main() {
flag.StringVar(&_mode, "m", "test", "Generating code by Working mode. [test|interface|mock...]")
flag.StringVar(&_func, "func", "", "Generating code by function.")
flag.Parse()
if len(os.Args) == 1 {
println("Creater is a tool for generating code.\n\nUsage: creater [-m]")
flag.PrintDefaults()
return
}
if err = parseArgs(os.Args[1:], &files, 0); err != nil {
panic(err)
}
switch _mode {
case "monkey":
if parses, err = parseFile(files...); err != nil {
panic(err)
}
if err = genMonkey(parses); err != nil {
panic(err)
}
case "test":
if parses, err = parseFile(files...); err != nil {
panic(err)
}
if err = genTest(parses); err != nil {
panic(err)
}
case "interface":
if parses, err = parseFile(files...); err != nil {
panic(err)
}
if err = genInterface(parses); err != nil {
panic(err)
}
case "mock":
if err = genMock(files...); err != nil {
panic(err)
}
default:
}
fmt.Println(print)
}

@ -0,0 +1,193 @@
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
type param struct{ K, V, P string }
type parse struct {
Path string
Package string
// Imports []string
Imports map[string]*param
// Structs []*param
// Interfaces []string
Funcs []*struct {
Name string
Method, Params, Result []*param
}
}
func parseArgs(args []string, res *[]string, index int) (err error) {
if len(args) <= index {
return
}
if strings.HasPrefix(args[index], "-") {
index += 2
parseArgs(args, res, index)
return
}
var f os.FileInfo
if f, err = os.Stat(args[index]); err != nil {
return
}
if f.IsDir() {
if !strings.HasSuffix(args[index], "/") {
args[index] += "/"
}
var fs []os.FileInfo
if fs, err = ioutil.ReadDir(args[index]); err != nil {
return
}
for _, f = range fs {
path, _ := filepath.Abs(args[index] + f.Name())
args = append(args, path)
}
} else {
if strings.HasSuffix(args[index], ".go") &&
!strings.HasSuffix(args[index], "_test.go") {
*res = append(*res, args[index])
}
}
index++
return parseArgs(args, res, index)
}
func parseFile(files ...string) (parses []*parse, err error) {
for _, file := range files {
var (
astFile *ast.File
fSet = token.NewFileSet()
parse = &parse{
Imports: make(map[string]*param),
}
)
if astFile, err = parser.ParseFile(fSet, file, nil, 0); err != nil {
return
}
if astFile.Name != nil {
parse.Path = file
parse.Package = astFile.Name.Name
}
for _, decl := range astFile.Decls {
switch decl.(type) {
case *ast.GenDecl:
if specs := decl.(*ast.GenDecl).Specs; len(specs) > 0 {
parse.Imports = parseImports(specs)
}
case *ast.FuncDecl:
var (
dec = decl.(*ast.FuncDecl)
parseFunc = &struct {
Name string
Method, Params, Result []*param
}{Name: dec.Name.Name}
)
if dec.Recv != nil {
parseFunc.Method = parserParams(dec.Recv.List)
}
if dec.Type.Params != nil {
parseFunc.Params = parserParams(dec.Type.Params.List)
}
if dec.Type.Results != nil {
parseFunc.Result = parserParams(dec.Type.Results.List)
}
parse.Funcs = append(parse.Funcs, parseFunc)
}
}
parses = append(parses, parse)
}
return
}
func parserParams(fields []*ast.Field) (params []*param) {
for _, field := range fields {
p := &param{}
p.V = parseType(field.Type)
if field.Names == nil {
params = append(params, p)
}
for _, name := range field.Names {
sp := &param{}
sp.K = name.Name
sp.V = p.V
sp.P = p.P
params = append(params, sp)
}
}
return
}
func parseType(expr ast.Expr) string {
switch expr.(type) {
case *ast.Ident:
return expr.(*ast.Ident).Name
case *ast.StarExpr:
return "*" + parseType(expr.(*ast.StarExpr).X)
case *ast.ArrayType:
return "[" + parseType(expr.(*ast.ArrayType).Len) + "]" + parseType(expr.(*ast.ArrayType).Elt)
case *ast.SelectorExpr:
return parseType(expr.(*ast.SelectorExpr).X) + "." + expr.(*ast.SelectorExpr).Sel.Name
case *ast.MapType:
return "map[" + parseType(expr.(*ast.MapType).Key) + "]" + parseType(expr.(*ast.MapType).Value)
case *ast.StructType:
return "struct{}"
case *ast.InterfaceType:
return "interface{}"
case *ast.FuncType:
var (
pTemp string
rTemp string
)
pTemp = parseFuncType(pTemp, expr.(*ast.FuncType).Params)
if expr.(*ast.FuncType).Results != nil {
rTemp = parseFuncType(rTemp, expr.(*ast.FuncType).Results)
return fmt.Sprintf("func(%s) (%s)", pTemp, rTemp)
}
return fmt.Sprintf("func(%s)", pTemp)
case *ast.ChanType:
return fmt.Sprintf("make(chan %s)", parseType(expr.(*ast.ChanType).Value))
case *ast.Ellipsis:
return parseType(expr.(*ast.Ellipsis).Elt)
}
return ""
}
func parseFuncType(temp string, data *ast.FieldList) string {
var params = parserParams(data.List)
for i, param := range params {
if i == 0 {
temp = param.K + " " + param.V
continue
}
t := param.K + " " + param.V
temp = fmt.Sprintf("%s, %s", temp, t)
}
return temp
}
func parseImports(specs []ast.Spec) (params map[string]*param) {
params = make(map[string]*param)
for _, spec := range specs {
switch spec.(type) {
case *ast.ImportSpec:
p := &param{V: strings.Replace(spec.(*ast.ImportSpec).Path.Value, "\"", "", -1)}
if spec.(*ast.ImportSpec).Name != nil {
p.K = spec.(*ast.ImportSpec).Name.Name
params[p.K] = p
} else {
vs := strings.Split(p.V, "/")
params[vs[len(vs)-1]] = p
}
}
}
return
}

@ -0,0 +1,41 @@
package main
var (
tpPackage = "package %s\n\n"
tpImport = "import (\n\t%s\n)\n\n"
tpVar = "var (\n\t%s\n)\n"
tpInterface = "type %sInterface interface {\n%s}\n"
tpIntfcFunc = "%s(%s) %s\n"
tpMonkeyFunc = "// Mock%s .\nfunc Mock%s(%s %s,%s) (guard *monkey.PatchGuard) {\n\treturn monkey.PatchInstanceMethod(reflect.TypeOf(%s), \"%s\", func(_ %s, %s) (%s) {\n\t\treturn %s\n\t})\n}\n\n"
tpTestReset = "\n\t\tReset(func() {%s\n\t\t})"
tpTestFunc = "func Test%s%s(t *testing.T){%s\n\tConvey(\"%s\", t, func(){\n\t\t%s\tConvey(\"When everything goes positive\", func(){\n\t\t\t%s\n\t\t\t})\n\t\t})%s\n\t})\n}\n\n"
tpTestDaoMain = `func TestMain(m *testing.M) {
flag.Set("conf", "%s")
flag.Parse()
%s
os.Exit(m.Run())
}
`
tpTestServiceMain = `func TestMain(m *testing.M){
flag.Set("conf", "%s")
flag.Parse()
%s
os.Exit(m.Run())
}
`
tpTestMainNew = `if err := paladin.Init(); err != nil {
panic(err)
}
%s`
tpTestMainOld = `if err := conf.Init(); err != nil {
panic(err)
}
%s`
print = `Generation success!
莫生气
代码辣鸡非我意,
自己动手分田地;
你若气死谁如意?
谈笑风生活长命.
// Release 1.2.3. Powered by Kratos`
)

@ -0,0 +1,42 @@
package main
import (
"fmt"
"strings"
"golang.org/x/tools/imports"
)
// GoImport Use golang.org/x/tools/imports auto import pkg
func GoImport(file string, bytes []byte) (res []byte, err error) {
options := &imports.Options{
TabWidth: 8,
TabIndent: true,
Comments: true,
Fragment: true,
}
if res, err = imports.Process(file, bytes, options); err != nil {
fmt.Printf("GoImport(%s) error(%v)", file, err)
res = bytes
return
}
return
}
// ConvertMethod checkout the file belongs to dao or not
func ConvertMethod(path string) (method string) {
switch {
case strings.Contains(path, "/dao"):
method = "d"
case strings.Contains(path, "/service"):
method = "s"
default:
method = ""
}
return
}
//ConvertHump convert words to hump style
func ConvertHump(words string) string {
return strings.ToUpper(words[0:1]) + words[1:]
}
Loading…
Cancel
Save