单元测试
什么是单元测试?
-
首先它是测试==》检测代码正确性
-
其次它是个单元==》粒度最小
-
例如,实现一个加法函数:
-
我们的几个单元测试可以这样进行:
// add.go package main func Add(x, y int) int { return x + y }
// add_test.go
package main
func TestAdd1(t *testing.T) {
x := 1
y := 1
want := 2
got := Add(x, y)
if got != want {
t.Errorf("%d+%d=%d, but %d expected", x, y, got, want)
}
}
为什么我们需要单元测试?
-
万一有bug就完蛋了,测边界值防止意外发生,保证正确性
-
万一你的函数要被重构了,新的函数怎么证明它的正确性,怎么debug
-
钝角
哪里需要单元测试?
-
我就加了一个if,怎么就出bug了(复杂+容易出错)
-
这个库整个公司都在使用,出问题就n+1了(基础代码&公共代码)
-
这个广告计费模块太重要了(重要性)
-
这个需求老变,每次一变我就要改好多单测【这种情况不推荐写单测,因为一改需求就要推倒重来】(基础&底层)
什么时候写单元测试
-
在写代码之前把所有的测试情况想好,测试点就过了就是写好了(难度较高,但是比较流行(TDD))(编码前)
-
写一段,来一个单测,再写一段,再来一个单测(编码中)
-
最后做一个全范围覆盖,补上所有的测试(编码后,大多数人会做的选择)
如何写单元测试
-
在当前package下新建文件add.go,并实现Add函数
// add.go
package main
func Add(x, y int) int {
return x + y
}
-
我们想测试Add函数,那么我们需要在add.go的package下创建add_test.go,并实现逻辑
// add_test.go
package main
func TestAdd1(t *testing.T) {
x := 1
y := 1
want := 2
got := Add(x, y)
if got != want {
t.Errorf("%d+%d=%d, but %d expected", x, y, got, want)
}
}
-
如果我们想并发跑很多case:
// add_test.go
func TestAdd2(t *testing.T) {
type tCase struct {
x int
y int
expected int
}
cases := make([]*tCase, 0, 0)
// test1
cases = append(cases, &tCase{
x: 1,
y: -1,
expected: 0,
})
// test2
cases = append(cases, &tCase{
x: 1,
y: 2,
expected: 3,
})
for i, c := range cases {
t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) {
got := Add(c.x, c.y)
if got != c.expected {
t.Errorf("%d+%d expected %d, but %d got", c.x, c.y, c.expected, got)
}
})
}
}
-
mock必不可少
// add_test.go
func init() {
rand.Seed(time.Now().UnixNano())
}
func AddWithRandom(x, y int) int {
return x + y + rand.Int()
}
-
mock
random.Int()
func TestAddWithRandom(t *testing.T) {
monkey.Patch(rand.Int, func() int { return 0 })
x := 1
y := 1
expected := 2
got := AddWithRandom(x, y)
if got != expected {
t.Errorf("%d+%d expected %d, but %d got", x, y, expected, got)
}
}
-
基准测试
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
for j := 0; j < 1e9; j++ {
Add(i, i+1)
}
}
}
func BenchmarkMul(b *testing.B) {
for i := 0; i < b.N; i++ {
for j := 0; j < 1e9; j++ {
Mul(i, i+1)
}
}
}
Gin框架
-
什么是框架?==》用库帮你做你不想写的部分,让你省出精力来写业务
-
框架:由多个库组成,形成针对某一部分的解决方案《==》不断对代码的抽象和复用
-
简单地理解
-
函数:代码复用
-
库:跨项目的代码复用
-
框架:本身也是一个库,可能由多个库组装而成,形成一个解决方案,开发者只需要关注业务逻辑,本质也是代码的复用
-
-
Gin框架:HTTP的Web框架:帮你处理web相关的事情:路由解析、TCP建立、HTTP协议的解析等等,都帮你搞定了
-
注:什么是协议:数据(传输的数据包)+约定(规则)
-
restful不属于一个协议,因为不是强制的,它只是一种良好的约束规则
-
未登录跳转到统一登录界面去登录:使用302(临时重定向)(301一般用于path全部永久改动)
-
一般浏览器遇到301会给你缓存,因为是永久改变的,不必再跳到原来界面,每次刷新都会直接跳到新界面,只有删除缓存再进入才有可能再经历一次301界面;而302是肯定不会给你缓存的
-
Gin框架的使用:
-
Path + Method -> Handler
func main() {
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
// By default it serves on :8080 unless a
// PORT environment variable was defined.
router.Run()
// router.Run(":3000") for a hard coded port
}
-
Validation -> Binding
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
func main() {
router := gin.Default()
// Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Example for binding XML (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>manu</user>
// <password>123</password>
// </root>)
router.POST("/loginXML", func(c *gin.Context) {
var xml Login
if err := c.ShouldBindXML(&xml); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if xml.User != "manu" || xml.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Example for binding a HTML form (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// This will infer what binder to use depending on the content-type header.
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
Gorm补充
查找树用B和B+,不用二叉查找树,是为了更好地适配磁盘,减少I/O,从而避免层数过多。
B/B+的查找复杂度:logN
计算方法:
-
如果只有一层,这层有无穷多个节点===》二分,logN;
-
如果一层只有两个,但是有很多层===》变成平衡二叉树,logN;
Gorm的使用
-
Declaring Models
type Teacher struct {
ID int
Name string
}
func (Teacher) TableName() string {
return "teacher"
}
type Course struct {
ID int
Name string
}
func (Course) TableName() string {
return "course"
}
type TeacherSchedule struct {
ID int
TeacherID int
CourseID int
}
func (TeacherSchedule) TableName() string {
return "teacher_schedule"
}
Connect
package main
import (
"fmt"
"testing"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:bytedancecamp@tcp(180.184.74.182:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic(fmt.Sprintf("open mysql failed, err is %s", err))
}
// do something with db
}
-
CURD(Create、Update、Read、Delete)
func main() {
dsn := "root:bytedancecamp@tcp(180.184.74.182:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn))
db = db.Debug()
if err != nil {
panic(fmt.Sprintf("open mysql failed, err is %s", err))
}
// create
teacher := &Teacher{
Name: "张三",
}
if err := db.Create(teacher).Error; err != nil {
panic(fmt.Sprintf("create failed, err is %s", err))
}
// update
teacher.Name = "李四"
if err := db.Where("name = ?", "张三").Updates(teacher).Error; err != nil {
panic(fmt.Sprintf("update failed, err is %s", err))
}
// query
teacher2 := new(Teacher)
if err := db.First(teacher2, "name = ?", "李四").Error; err != nil {
panic(fmt.Sprintf("query failed ,err is %s", err))
}
// delete
if err := db.Delete(&Teacher{}, "name = ?", "李四").Error; err != nil {
panic(fmt.Sprintf("delete failed, err is %s", err))
}
}
[176.107ms] [rows:1] INSERT INTO `teacher` (`name`) VALUES ('张三')
[173.249ms] [rows:1] UPDATE `teacher` SET `name`='李四' WHERE name = '张三' AND `id` = 2
[86.831ms] [rows:1] SELECT * FROM `teacher` WHERE name = '李四' ORDER BY `teacher`.`id` LIMIT 1
[173.992ms] [rows:1] DELETE FROM `teacher` WHERE name = '李四'
软删除:记录deleteAt,并不真的删除记录
微服务
什么是微服务
为什么需要微服务
-
效率分工相关,人越多,沟通成本越高、分工越难、效率越低,总而言之就是人越少越好
-
康威定律:公司的组织架构是怎样的,那么产品的结构就是怎么样的
-
领域驱动设计(DDD):以对象为基础进行设计,将业务分割成小的部门
-
单体架构部署起来会比微服务更困难和复杂。
服务治理
-
需要负责服务注册、发现的中间件
-
限流与熔断的流量解决方案
-
日志采集
-
链路追踪