最开始的项目目录
$ tree
.
├── ./api/
├── ./service/
├── ./dao/
├── ./model/
├── main.go
func main(){
flag.Parse()
artemis := NewArtemis(flag.String(`config`)) // artemis 是我们的框架,类似beego
log.init()
service.init()
dao.init()
worker.init()
api.Route(artemis.Router)
artemis.Run()
}
写个测试吧
packege dao
func TestGetNote(t *testing.T){
db := NewNoteDB() // panic: app not init
db.Get(1)
}
改:
- copy启动代码
- 把config路径改一下
- 去除不需要的部分,比如在 dao 里去掉 service.init()
func TestGetNote(t *testing.T){
artemis := NewArtemis(`../../config`)
log.init()
ddns.init()
service.init()
dao.init()
// test code
}
// 也可以放在 TestMain 里统一处理
func TestMain(m *testing.M){
// init()
m.Run()
}
简单粗暴的解决办法,调试也够用了。
如果 package 比较少,也没那么多脚本要用,已经能满足需要了
但当时我觉得还是不完美,我的项目又比较复杂,然后继续尝试优化:
先抽出个函数放重复代码
package boot
func Boot(){
// init()
}
func BootTest(){
SetRootPath() // 在子目录跑ut时,要把 wd 对齐到项目目录
Boot()
}
package main
func main(){
boot.Boot()
artemis.Run()
}
package dao
func test(){
boot.BootTest()
// test code
}
$ go build
import cycle not allowed
# dao 和 boot 互相依赖
改成外部测试
package dao_test
import "dao"
func test(){
db := dao.NewDB()
}
缺点:
- ugly
- 不能测私有函数
- 每次改动几乎重新编译整个项目
最后一点很重要,测试代码时会频繁修改,而每次修改几乎都要重新编译整个项目。 我的项目在忽略缓存的情况下,编译需要30s 调个bug,一下午就过去了。。
回头一看整个过程,总结就是
民科
于是,放下一切,重新开始
我想要什么?
- 不用太多重复代码,好维护
- 需要时再加载,不需要在测试时搞一堆没用的东西
看看成熟的框架是如何处理的,以我熟悉的 Laravel 为例,很多地方都采用了这样的思路:先注册,后使用
class FooServiceProvider {
function register($app) {
$app->singleton("noteDB", function(){
$noteDB = new NoteDB();
})
$app->sinleton(IRouter::class, new Router())
}
function boot() {
make(IRouter::class)->load();
}
}
突然发现,Go的init就是天然的注册机制
- 所有的init都一定会在main之前执行
- 依赖包的init一定先于本包的init,连优先级都帮我做好了
于是大概轮廓就出来了:
- 在每个 init 里定义自己需要启动什么,代码里依赖了哪个包,就会调用对应的init,不依赖的不会启动
- 运行cmd(webserver/script/test)时,再逐个执行init时注册的任务
package dao
func init(){
boot.Register(func(){
// ...
})
}
// web server
func main(){
boot.Boot()
defer boot.Shutdown()
api.Route(artemis.App)
artemis.Run()
}
// script
func main(){
boot.Boot()
// ...
}
// test
func TestMain(m *testing.M){
boot.Register(factories.Init)
boottest.Boot()
defer boottest.Shutdown()
m.Run()
// ...
}
到此,基本上达成了写脚本、写单测时不用操心框架启动的问题。
不过限于框架限制,依然有一些不爽的地方没有解决。
- Go很多人摒弃了面向接口编程,导致在写UT时无从mock。后面想办法靠工具对所有的类走一次DI容器
- 测试的问题,限于框架限制,不能全局开始事务