New Upstream Snapshot - golang-github-issue9-assert

Ready changes

Summary

Merged new upstream version: 3.0.4 (was: 0.0~git20170908.0.ceac1aa).

Resulting package

Built on 2022-11-19T15:03 (took 3m58s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots golang-github-issue9-assert-dev

Lintian Result

Diff

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..d8720ad
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+
+# html
+[*.{htm,html,js,css}]
+indent_style = space
+indent_size = 4
+
+# 配置文件
+[*.{yml,yaml,json}]
+indent_style = space
+indent_size = 2
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 72ded60..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-language: go
-go:
-    - tip
-    - 1.9
-    - 1.8
-    - 1.7
-    - 1.6
-    - 1.5
-    - 1.4
-    - 1.3
diff --git a/README.md b/README.md
index 1e3828f..2c71d49 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,75 @@
-assert [![Build Status](https://travis-ci.org/issue9/assert.svg?branch=master)](https://travis-ci.org/issue9/assert)
+assert
 ======
 
+[![Go](https://github.com/issue9/assert/workflows/Go/badge.svg)](https://github.com/issue9/assert/actions?query=workflow%3AGo)
+[![codecov](https://codecov.io/gh/issue9/assert/branch/master/graph/badge.svg)](https://codecov.io/gh/issue9/assert)
+[![license](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](https://opensource.org/licenses/MIT)
+[![PkgGoDev](https://pkg.go.dev/badge/github.com/issue9/assert)](https://pkg.go.dev/github.com/issue9/assert/v3)
+[![Go version](https://img.shields.io/github/go-mod/go-version/issue9/assert)](https://golang.org)
+
 assert 包是对 testing 的一个简单扩展,提供的一系列的断言函数,
 方便在测试函数中使用:
+
 ```go
-func TestA(t testing.T) {
+func TestA(t *testing.T) {
     v := true
-    assert.True(v)
-
-    a := assert.New(t)
+    a := assert.New(t, false)
     a.True(v)
 }
 
-// 也可以对testing.B使用
+// 也可以对 testing.B 使用
 func Benchmark1(b *testing.B) {
-    a := assert.New(b)
+    a := assert.New(b, false)
     v := false
     a.True(v)
     for(i:=0; i<b.N; i++) {
         // do something
     }
 }
-```
 
-### 安装
+// 对 API 请求做测试,可以引用 assert/rest
+func TestHTTP( t *testing.T) {
+    a := assert.New(t, false)
+
+    srv := rest.NewServer(a, h, nil)
+    a.NotNil(srv)
+    defer srv.Close()
 
-```shell
-go get github.com/issue9/assert
+    srv.NewRequest(http.MethodGet, "/body").
+        Header("content-type", "application/json").
+        Query("page", "5").
+        JSONBody(&bodyTest{ID: 5}).
+        Do().
+        Status(http.StatusCreated).
+        Header("content-type", "application/json;charset=utf-8").
+        JSONBody(&bodyTest{ID: 6})
+}
 ```
 
+也可以直接对原始数据进行测试。
 
-### 文档
+```go
+// 请求数据
+req :=`POST /users HTTP/1.1
+Host: example.com
+Content-type: application/json
+
+{"username": "admin", "password":"123"}
+
+`
 
-[![Go Walker](https://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/issue9/assert)
-[![GoDoc](https://godoc.org/github.com/issue9/assert?status.svg)](https://godoc.org/github.com/issue9/assert)
+// 期望的返回数据
+resp :=`HTTP/1.1 201
+Location: https://example.com/users/1
+`
 
+func TestRaw(t *testing.T) {
+    a := assert.New(t, false)
+    rest.RawHTTP(a, nil,req, resp)
+}
+```
 
-### 版权
+版权
+----
 
 本项目采用 [MIT](https://opensource.org/licenses/MIT) 开源授权许可证,完整的授权说明可在 [LICENSE](LICENSE) 文件中找到。
diff --git a/assert.go b/assert.go
index b56ba43..83dcd97 100644
--- a/assert.go
+++ b/assert.go
@@ -1,263 +1,101 @@
-// Copyright 2014 by caixw, All rights reserved.
-// Use of this source code is governed by a MIT
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
 
+// Package assert 是对 testing 包的一些简单包装
+//
+//	func TestAssert(t *testing.T) {
+//	    var v interface{} = 5
+//
+//	    a := assert.New(t, false)
+//	    a.True(v==5, "v的值[%v]不等于5", v).
+//	        Equal(5, v, "v的值[%v]不等于5", v).
+//	        Nil(v).
+//	        TB().Log("success")
+//	}
+//
+//	// 也可以对 testing.B 使用
+//	func Benchmark1(b *testing.B) {
+//	    a := assert.New(b)
+//	    a.True(false)
+//	    for(i:=0; i<b.N; i++) {
+//	        // do something
+//	    }
+//	}
 package assert
 
 import (
 	"fmt"
-	"os"
-	"path"
-	"reflect"
-	"runtime"
-	"strconv"
+	"sort"
 	"strings"
-	"testing"
 )
 
-// 定位错误信息的触发函数。输出格式为:TestXxx(xxx_test.go:17)。
-func getCallerInfo() string {
-	for i := 0; ; i++ {
-		pc, file, line, ok := runtime.Caller(i)
-		if !ok {
-			break
-		}
-
-		basename := path.Base(file)
-
-		// 定位以 _test.go 结尾的文件。
-		// 8 == len("_test.go")
-		l := len(basename)
-		if l < 8 || (basename[l-8:l] != "_test.go") {
-			continue
-		}
-
-		// 定位函数名为 Test 开头的行。
-		// 为什么要定位到 TestXxx 函数,是因为考虑以下情况:
-		//  func isOK(val interface{}, t *testing.T) {
-		//      // do somthing
-		//      assert.True(t, val)  // (1
-		//  }
-		//
-		//  func TestOK(t *testing.T) {
-		//      isOK("123", t)       // (2
-		//      isOK(123, t)         // (3
-		//  }
-		// 以上这段代码,定位到 (2、(3 的位置比总是定位到 (1 的位置更直观!
-		funcName := runtime.FuncForPC(pc).Name()
-		index := strings.LastIndex(funcName, ".Test")
-		if -1 == index {
-			continue
-		}
-		funcName = funcName[index+1:]
-		if strings.IndexByte(funcName, '.') > -1 { // Go1.5 之后的匿名函数
-			continue
-		}
+var failureSprint FailureSprintFunc = DefaultFailureSprint
 
-		return funcName + "(" + basename + ":" + strconv.Itoa(line) + ")"
-	}
-
-	return "<无法获取调用者信息>"
+// Failure 在断言出错时输出的错误信息
+type Failure struct {
+	Action string                 // 操作名称,比如 Equal,NotEqual 等方法名称。
+	Values map[string]interface{} // 断言出错是返回的一些额外参数
+	User   string                 // 断言出错时用户反馈的额外信息
 }
 
-// 格式化错误提示信息。
-//
-// msg1 中的所有参数将依次被传递给 fmt.Sprintf() 函数,
-// 所以 msg1[0] 必须可以转换成 string(如:string, []byte, []rune, fmt.Stringer)
+// FailureSprintFunc 将 Failure 转换成文本的函数
 //
-// msg2 参数格式与 msg1 完全相同,在 msg1 为空的情况下,会使用 msg2 的内容,
-// 否则 msg2 不会启作用。
-func formatMessage(msg1 []interface{}, msg2 []interface{}) string {
-	if len(msg1) == 0 {
-		msg1 = msg2
-	}
+// NOTE: 可以使用此方法实现对错误信息的本地化。
+type FailureSprintFunc func(*Failure) string
 
-	if len(msg1) == 0 {
-		return "<未提供任何错误信息>"
-	}
-
-	format := ""
-	switch v := msg1[0].(type) {
-	case []byte:
-		format = string(v)
-	case []rune:
-		format = string(v)
-	case string:
-		format = v
-	case fmt.Stringer:
-		format = v.String()
-	default:
-		return "<无法正确转换错误提示信息>"
-	}
-
-	return fmt.Sprintf(format, msg1[1:]...)
-}
-
-// 当 expr 条件不成立时,输出错误信息。
-//
-// expr 返回结果值为bool类型的表达式;
-// msg1,msg2 输出的错误信息,之所以提供两组信息,是方便在用户没有提供的情况下,
-// 可以使用系统内部提供的信息,优先使用 msg1 中的信息,若不存在,则使用 msg2 的内容。
-func assert(t testing.TB, expr bool, msg1 []interface{}, msg2 []interface{}) {
-	if !expr {
-		t.Error(formatMessage(msg1, msg2) + "@" + getCallerInfo())
-	}
-}
-
-// True 断言表达式 expr 为 true,否则输出错误信息。
+// SetFailureSprintFunc 设置一个全局的转换方法
 //
-// args 对应 fmt.Printf() 函数中的参数,其中 args[0] 对应第一个参数 format,依次类推,
-// 具体可参数 formatMessage() 函数的介绍。其它断言函数的 args 参数,功能与此相同。
-func True(t testing.TB, expr bool, args ...interface{}) {
-	assert(t, expr, args, []interface{}{"True失败,实际值为[%T:%[1]v]", expr})
-}
-
-// False 断言表达式 expr 为 false,否则输出错误信息
-func False(t testing.TB, expr bool, args ...interface{}) {
-	assert(t, !expr, args, []interface{}{"False失败,实际值为[%T:%[1]v]", expr})
-}
-
-// Nil 断言表达式 expr 为 nil,否则输出错误信息
-func Nil(t testing.TB, expr interface{}, args ...interface{}) {
-	assert(t, IsNil(expr), args, []interface{}{"Nil失败,实际值为[%T:%[1]v]", expr})
-}
-
-// NotNil 断言表达式 expr 为非 nil 值,否则输出错误信息
-func NotNil(t testing.TB, expr interface{}, args ...interface{}) {
-	assert(t, !IsNil(expr), args, []interface{}{"NotNil失败,实际值为[%T:%[1]v]", expr})
-}
-
-// Equal 断言 v1 与 v2 两个值相等,否则输出错误信息
-func Equal(t testing.TB, v1, v2 interface{}, args ...interface{}) {
-	assert(t, IsEqual(v1, v2), args, []interface{}{"Equal失败,实际值为v1=[%T:%[1]v];v2=[%T:%[2]v]", v1, v2})
-}
-
-// NotEqual 断言 v1 与 v2 两个值不相等,否则输出错误信息
-func NotEqual(t testing.TB, v1, v2 interface{}, args ...interface{}) {
-	assert(t, !IsEqual(v1, v2), args, []interface{}{"NotEqual失败,实际值为v1=[%T:%[1]v];v2=[%T:%[2]v]", v1, v2})
-}
-
-// Empty 断言 expr 的值为空(nil,"",0,false),否则输出错误信息
-func Empty(t testing.TB, expr interface{}, args ...interface{}) {
-	assert(t, IsEmpty(expr), args, []interface{}{"Empty失败,实际值为[%T:%[1]v]", expr})
-}
-
-// NotEmpty 断言 expr 的值为非空(除 nil,"",0,false之外),否则输出错误信息
-func NotEmpty(t testing.TB, expr interface{}, args ...interface{}) {
-	assert(t, !IsEmpty(expr), args, []interface{}{"NotEmpty失败,实际值为[%T:%[1]v]", expr})
-}
-
-// Error 断言有错误发生,否则输出错误信息。
-// 传递未初始化的 error 值(var err error = nil),将断言失败
-func Error(t testing.TB, expr interface{}, args ...interface{}) {
-	if IsNil(expr) { // 空值,必定没有错误
-		assert(t, false, args, []interface{}{"Error失败,未初始化的类型[%T]", expr})
-		return
-	}
-
-	_, ok := expr.(error)
-	assert(t, ok, args, []interface{}{"Error失败,实际类型为[%T]", expr})
-}
-
-// ErrorString 断言有错误发生,且错误信息中包含指定的字符串 str。
-// 传递未初始化的 error 值(var err error = nil),将断言失败
-func ErrorString(t testing.TB, expr interface{}, str string, args ...interface{}) {
-	if IsNil(expr) { // 空值,必定没有错误
-		assert(t, false, args, []interface{}{"ErrorString失败,未初始化的类型[%T]", expr})
-		return
-	}
-
-	if err, ok := expr.(error); ok {
-		index := strings.Index(err.Error(), str)
-		assert(t, index >= 0, args, []interface{}{"Error失败,实际类型为[%T]", expr})
-	}
-}
-
-// ErrorType 断言有错误发生,且错误的类型与 typ 的类型相同。
-// 传递未初始化的 error 值(var err error = nil),将断言失败
-func ErrorType(t testing.TB, expr interface{}, typ error, args ...interface{}) {
-	if IsNil(expr) { // 空值,必定没有错误
-		assert(t, false, args, []interface{}{"ErrorType失败,未初始化的类型[%T]", expr})
-		return
-	}
-
-	if _, ok := expr.(error); !ok {
-		assert(t, false, args, []interface{}{"ErrorType失败,实际类型为[%T],且无法转换成error接口", expr})
-		return
-	}
-
-	t1 := reflect.TypeOf(expr)
-	t2 := reflect.TypeOf(typ)
-	assert(t, t1 == t2, args, []interface{}{"ErrorType失败,v1[%v]为一个错误类型,但与v2[%v]的类型不相同", t1, t2})
-}
-
-// NotError 断言没有错误发生,否则输出错误信息
-func NotError(t testing.TB, expr interface{}, args ...interface{}) {
-	if IsNil(expr) { // 空值必定没有错误
-		assert(t, true, args, []interface{}{"NotError失败,实际类型为[%T]", expr})
-		return
+// [New] 方法在默认情况下继承由此方法设置的值。
+func SetFailureSprintFunc(f FailureSprintFunc) { failureSprint = f }
+
+// DefaultFailureSprint 默认的 [FailureSprintFunc] 实现
+func DefaultFailureSprint(f *Failure) string {
+	s := strings.Builder{}
+	s.WriteString(f.Action)
+	s.WriteString(" 断言失败!")
+
+	if len(f.Values) > 0 {
+		keys := make([]string, 0, len(f.Values))
+		for k := range f.Values {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+
+		s.WriteString("反馈以下参数:\n")
+		for _, k := range keys {
+			s.WriteString(k)
+			s.WriteByte('=')
+			s.WriteString(fmt.Sprint(f.Values[k]))
+			s.WriteByte('\n')
+		}
 	}
-	err, ok := expr.(error)
-	assert(t, !ok, args, []interface{}{"NotError失败,错误信息为[%v]", err})
-}
 
-// FileExists 断言文件存在,否则输出错误信息
-func FileExists(t testing.TB, path string, args ...interface{}) {
-	_, err := os.Stat(path)
-
-	if err != nil && !os.IsExist(err) {
-		assert(t, false, args, []interface{}{"FileExists发生以下错误:%v", err.Error()})
+	if f.User != "" {
+		s.WriteString("用户反馈信息:")
+		s.WriteString(f.User)
 	}
-}
 
-// FileNotExists 断言文件不存在,否则输出错误信息
-func FileNotExists(t testing.TB, path string, args ...interface{}) {
-	_, err := os.Stat(path)
-	assert(t, os.IsNotExist(err), args, []interface{}{"FileExists发生以下错误:%v", err.Error()})
+	return s.String()
 }
 
-// Panic 断言函数会发生 panic,否则输出错误信息。
-func Panic(t testing.TB, fn func(), args ...interface{}) {
-	has, _ := HasPanic(fn)
-	assert(t, has, args, []interface{}{"并未发生panic"})
-}
-
-// PanicString 断言函数会发生 panic,且 panic 信息中包含指定的字符串内容,否则输出错误信息。
-func PanicString(t testing.TB, fn func(), str string, args ...interface{}) {
-	if has, msg := HasPanic(fn); has {
-		index := strings.Index(fmt.Sprint(msg), str)
-		assert(t, index >= 0, args, []interface{}{"并未发生panic"})
+// NewFailure 声明 Failure 对象
+//
+// user 表示用户提交的反馈,。其第一个元素如果是 string,那么将调用 fmt.Sprintf(user[0], user[1:]...)
+// 对数据进行格式化,否则采用 fmt.Sprint(user...) 格式化数据;
+// kv 表示当前错误返回的数据;
+func NewFailure(action string, user []interface{}, kv map[string]interface{}) *Failure {
+	var u string
+	if len(user) > 0 {
+		switch v := user[0].(type) {
+		case string:
+			u = fmt.Sprintf(v, user[1:]...)
+		default:
+			u = fmt.Sprint(user...)
+		}
 	}
-}
 
-// PanicType 断言函数会发生 panic,且 panic 返回的类型与 typ 的类型相同。
-func PanicType(t testing.TB, fn func(), typ interface{}, args ...interface{}) {
-	has, msg := HasPanic(fn)
-	if !has {
-		return
+	return &Failure{
+		Action: action,
+		User:   u,
+		Values: kv,
 	}
-
-	t1 := reflect.TypeOf(msg)
-	t2 := reflect.TypeOf(typ)
-	assert(t, t1 == t2, args, []interface{}{"PanicType失败,v1[%v]的类型与v2[%v]的类型不相同", t1, t2})
-
-}
-
-// NotPanic 断言函数会发生 panic,否则输出错误信息。
-func NotPanic(t testing.TB, fn func(), args ...interface{}) {
-	has, msg := HasPanic(fn)
-	assert(t, !has, args, []interface{}{"发生了panic,其信息为[%v]", msg})
-}
-
-// Contains 断言 container 包含 item 的或是包含 item 中的所有项
-// 具体函数说明可参考 IsContains()
-func Contains(t testing.TB, container, item interface{}, args ...interface{}) {
-	assert(t, IsContains(container, item), args,
-		[]interface{}{"container:[%v]并未包含item[%v]", container, item})
-}
-
-// NotContains 断言 container 不包含 item 的或是不包含 item 中的所有项
-func NotContains(t testing.TB, container, item interface{}, args ...interface{}) {
-	assert(t, !IsContains(container, item), args,
-		[]interface{}{"container:[%v]包含item[%v]", container, item})
 }
diff --git a/assert_test.go b/assert_test.go
index a3c27cb..f12c428 100644
--- a/assert_test.go
+++ b/assert_test.go
@@ -1,233 +1,42 @@
-// Copyright 2014 by caixw, All rights reserved.
-// Use of this source code is governed by a MIT
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
 
 package assert
 
-import (
-	"errors"
-	"testing"
-)
+import "testing"
 
-func TestGetCallerInfo(t *testing.T) {
-	str := getCallerInfo()
-	// NOTE:注意这里涉及到调用函数的行号信息
-	if str != "TestGetCallerInfo(assert_test.go:13)" {
-		t.Errorf("getCallerInfo返回的信息不正确,其返回值为:%v", str)
+func TestDefaultFailureSprint(t *testing.T) {
+	f := NewFailure("A", nil, nil)
+	if f.Action != "A" || f.User != "" || len(f.Values) != 0 {
+		t.Error("err1")
 	}
-
-	// 嵌套调用,第二个参数为当前的行号
-	testGetCallerInfo(t, "20")
-	testGetCallerInfo(t, "21")
-
-	// 闭合函数,line为调用所在的行号。
-	f := func(line string) {
-		str := getCallerInfo()
-		if str != "TestGetCallerInfo(assert_test.go:"+line+")" {
-			t.Errorf("getCallerInfo返回的信息不正确,其返回值为:%v", str)
-		}
+	if s := DefaultFailureSprint(f); s != "A 断言失败!" {
+		t.Error("err2")
 	}
-	f("30") // 参数为当前等号
-	f("31")
-}
 
-// 参数line,为调用此函数所在的行号。
-func testGetCallerInfo(t *testing.T, line string) {
-	str := getCallerInfo()
-	if str != "TestGetCallerInfo(assert_test.go:"+line+")" {
-		t.Errorf("getCallerInfo返回的信息不正确,其返回值为:%v", str)
+	// 带 user
+	f = NewFailure("AB", []interface{}{1, 2}, nil)
+	if f.Action != "AB" || f.User != "1 2" || len(f.Values) != 0 {
+		t.Error("err3")
 	}
-
-}
-
-func TestFormatMsg(t *testing.T) {
-	msg1 := []interface{}{}
-	msg2 := []interface{}{[]rune("msg:%v"), 2}
-	msg3 := []interface{}{"msg:%v", 3}
-	msg4 := []interface{}{123, 456}
-
-	str := formatMessage(msg1, msg2)
-	if str != "msg:2" {
-		t.Errorf("formatMessage(msg1,msg2)返回信息错误:[%v]", str)
+	if s := DefaultFailureSprint(f); s != "AB 断言失败!用户反馈信息:1 2" {
+		t.Error("err4", s)
 	}
 
-	str = formatMessage(nil, msg2)
-	if str != "msg:2" {
-		t.Errorf("formatMessage(msg1,msg2)返回信息错误:[%v]", str)
+	// 带 values
+	f = NewFailure("AB", nil, map[string]interface{}{"k1": "v1", "k2": 2})
+	if f.Action != "AB" || f.User != "" || len(f.Values) != 2 {
+		t.Error("err5")
 	}
-
-	str = formatMessage(msg2, msg3)
-	if str != "msg:2" {
-		t.Errorf("formatMessage(msg2,msg3)返回信息错误:[%v]", str)
+	if s := DefaultFailureSprint(f); s != "AB 断言失败!反馈以下参数:\nk1=v1\nk2=2\n" {
+		t.Error("err6", s)
 	}
 
-	str = formatMessage(nil, nil)
-	if str != "<未提供任何错误信息>" {
-		t.Errorf("formatMessage(nil,nil)返回信息错误:[%v]", str)
+	// 带 user,values
+	f = NewFailure("AB", []interface{}{1, 2}, map[string]interface{}{"k1": "v1", "k2": 2})
+	if f.Action != "AB" || f.User == "" || len(f.Values) != 2 {
+		t.Error("err7")
 	}
-
-	str = formatMessage(nil, msg4)
-	if str != "<无法正确转换错误提示信息>" {
-		t.Errorf("formatMessage(nil,nil)返回信息错误:[%v]", str)
+	if s := DefaultFailureSprint(f); s != "AB 断言失败!反馈以下参数:\nk1=v1\nk2=2\n用户反馈信息:1 2" {
+		t.Error("err8", s)
 	}
 }
-
-func TestTrue(t *testing.T) {
-	True(t, true)
-	True(t, 1 == 1, "True(1==1) falid")
-}
-
-func TestFalse(t *testing.T) {
-	False(t, false, "False falid")
-	False(t, 1 == 2, "False(1==2) falid")
-}
-
-func TestNil(t *testing.T) {
-	Nil(t, nil, "Nil falid")
-
-	var v interface{}
-	Nil(t, v, "Nil(v) falid")
-}
-
-func TestNotNil(t *testing.T) {
-	NotNil(t, 5, "NotNil falid")
-
-	var v interface{} = 5
-	NotNil(t, v, "NotNil falid")
-}
-
-func TestEqual(t *testing.T) {
-	Equal(t, 5, 5, "Equal(5,5) falid")
-
-	var v1, v2 interface{}
-	v1 = 5
-	v2 = 5
-
-	Equal(t, 5, v1)
-	Equal(t, v1, v2, "Equal(v1,v2) falid")
-	Equal(t, int8(126), 126)
-	Equal(t, int64(126), int8(126))
-	Equal(t, uint(7), int(7))
-}
-
-func TestNotEqual(t *testing.T) {
-	NotEqual(t, 5, 6, "NotEqual(5,6) falid")
-
-	var v1, v2 interface{} = 5, 6
-
-	NotEqual(t, 5, v2, "NotEqual(5,v2) falid")
-	NotEqual(t, v1, v2, "NotEqual(v1,v2) falid")
-	NotEqual(t, 128, int8(127))
-}
-
-func TestEmpty(t *testing.T) {
-	Empty(t, 0, "Empty(0) falid")
-	Empty(t, "", "Empty(``) falid")
-	Empty(t, false, "Empty(false) falid")
-	Empty(t, []string{}, "Empty(slice{}) falid")
-	Empty(t, []int{}, "Empty(slice{}) falid")
-}
-
-func TestNotEmpty(t *testing.T) {
-	NotEmpty(t, 1, "NotEmpty(1) falid")
-	NotEmpty(t, true, "NotEmpty(true) falid")
-	NotEmpty(t, []string{"ab"}, "NotEmpty(slice(abc)) falid")
-}
-
-type ErrorImpl struct {
-	msg string
-}
-
-func (err *ErrorImpl) Error() string {
-	return err.msg
-}
-
-func TestError(t *testing.T) {
-	err1 := errors.New("test")
-	Error(t, err1, "Error(err) falid")
-
-	err2 := &ErrorImpl{msg: "msg"}
-	Error(t, err2, "Error(ErrorImpl) falid")
-}
-
-func TestErrorString(t *testing.T) {
-	err1 := errors.New("test")
-	ErrorString(t, err1, "test", "Error(err1) falid")
-
-	err2 := &ErrorImpl{msg: "msg"}
-	Error(t, err2, "msg", "Error(ErrorImpl) falid")
-}
-
-func TestErrorType(t *testing.T) {
-	ErrorType(t, errors.New("abc"), errors.New("def"), "ErrorType:errors.New(abc) != errors.New(def)")
-
-	ErrorType(t, &ErrorImpl{msg: "abc"}, &ErrorImpl{}, "ErrorType:&ErrorImpl{} != &ErrorImpl{}")
-}
-
-func TestNotError(t *testing.T) {
-	NotError(t, "123", "NotError(123) falid")
-
-	var err1 error
-	NotError(t, err1, "var err1 error falid")
-
-	err2 := &ErrorImpl{msg: "msg"}
-	Error(t, err2, "Error(ErrorImpl) falid")
-}
-
-func TestFileExists(t *testing.T) {
-	FileExists(t, "./assert.go", "FileExists() falid")
-	FileExists(t, "./", "FileExists() falid")
-}
-
-func TestFileNotExists(t *testing.T) {
-	FileNotExists(t, "c:/win", "FileNotExists() falid")
-	FileNotExists(t, "./abcefg/", "FileNotExists() falid")
-}
-
-func TestPanic(t *testing.T) {
-	f1 := func() {
-		panic("panic")
-	}
-
-	Panic(t, f1)
-}
-
-func TestPanicString(t *testing.T) {
-	f1 := func() {
-		panic("panic")
-	}
-
-	PanicString(t, f1, "pani")
-}
-
-func TestPanicType(t *testing.T) {
-	f1 := func() {
-		panic("panic")
-	}
-	PanicType(t, f1, "abc")
-
-	f1 = func() {
-		panic(errors.New("panic"))
-	}
-	PanicType(t, f1, errors.New("abc"))
-
-	f1 = func() {
-		panic(&ErrorImpl{msg: "panic"})
-	}
-	PanicType(t, f1, &ErrorImpl{msg: "abc"})
-}
-
-func TestNotPanic(t *testing.T) {
-	f1 := func() {
-	}
-
-	NotPanic(t, f1)
-}
-
-func TestContains(t *testing.T) {
-	Contains(t, []int{1, 2, 3}, []int8{1, 2})
-}
-
-func TestNotContains(t *testing.T) {
-	NotContains(t, []int{1, 2, 3}, []int8{1, 3})
-}
diff --git a/assertion.go b/assertion.go
index 54629b0..cd94fb1 100644
--- a/assertion.go
+++ b/assertion.go
@@ -1,143 +1,345 @@
-// Copyright 2014 by caixw, All rights reserved.
-// Use of this source code is governed by a MIT
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
 
 package assert
 
-import "testing"
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"os"
+	"reflect"
+	"strings"
+	"testing"
+)
 
-// Assertion 是对 testing.TB 进行了简单的封装。
-// 可以以对象的方式调用包中的各个断言函数。
+// Assertion 可以以对象的方式调用包中的各个断言函数
 type Assertion struct {
-	t testing.TB
+	tb testing.TB
+
+	fatal bool
+	print func(...interface{})
+	f     FailureSprintFunc
 }
 
-// New 返回 Assertion 对象。
-func New(t testing.TB) *Assertion {
-	return &Assertion{t: t}
+// New 返回 Assertion 对象
+//
+// fatal 决定在出错时是调用 tb.Error 还是 tb.Fatal;
+func New(tb testing.TB, fatal bool) *Assertion {
+	p := tb.Error
+	if fatal {
+		p = tb.Fatal
+	}
+
+	return &Assertion{
+		tb: tb,
+
+		fatal: fatal,
+		print: p,
+		f:     failureSprint,
+	}
 }
 
-// TB 返回 testing.TB 接口
-func (a *Assertion) TB() testing.TB {
-	return a.t
+// NewWithEnv 以指定的环境变量初始化 *Assertion 对象
+//
+// env 是以 t.Setenv 的形式调用。
+func NewWithEnv(tb testing.TB, fatal bool, env map[string]string) *Assertion {
+	for k, v := range env {
+		tb.Setenv(k, v)
+	}
+	return New(tb, fatal)
 }
 
-// True 参照 assert.True() 函数
-func (a *Assertion) True(expr bool, msg ...interface{}) *Assertion {
-	True(a.t, expr, msg...)
+// Assert 断言 expr 条件成立
+//
+// f 表示在断言失败时输出的信息
+//
+// 普通用户直接使用 True 效果是一样的,此函数主要供库调用。
+func (a *Assertion) Assert(expr bool, f *Failure) *Assertion {
+	if !expr {
+		a.TB().Helper()
+		a.print(a.f(f))
+	}
 	return a
 }
 
-// False 参照 assert.False() 函数
+// TB 返回 [testing.TB] 接口
+func (a *Assertion) TB() testing.TB { return a.tb }
+
+// True 断言表达式 expr 为真
+//
+// args 对应 fmt.Printf() 函数中的参数,其中 args[0] 对应第一个参数 format,依次类推,
+// 具体可参数 Assert 方法的介绍。其它断言函数的 args 参数,功能与此相同。
+func (a *Assertion) True(expr bool, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(expr, NewFailure("True", msg, nil))
+}
+
 func (a *Assertion) False(expr bool, msg ...interface{}) *Assertion {
-	False(a.t, expr, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(!expr, NewFailure("False", msg, nil))
 }
 
-// Nil 参照 assert.Nil() 函数
 func (a *Assertion) Nil(expr interface{}, msg ...interface{}) *Assertion {
-	Nil(a.t, expr, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(isNil(expr), NewFailure("Nil", msg, map[string]interface{}{"v": expr}))
 }
 
-// NotNil 参照 assert.NotNil() 函数
 func (a *Assertion) NotNil(expr interface{}, msg ...interface{}) *Assertion {
-	NotNil(a.t, expr, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(!isNil(expr), NewFailure("NotNil", msg, map[string]interface{}{"v": expr}))
 }
 
-// Equal 参照 assert.Equal() 函数
 func (a *Assertion) Equal(v1, v2 interface{}, msg ...interface{}) *Assertion {
-	Equal(a.t, v1, v2, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(isEqual(v1, v2), NewFailure("Equal", msg, map[string]interface{}{"v1": v1, "v2": v2}))
 }
 
-// NotEqual 参照 assert.NotEqual() 函数
 func (a *Assertion) NotEqual(v1, v2 interface{}, msg ...interface{}) *Assertion {
-	NotEqual(a.t, v1, v2, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(!isEqual(v1, v2), NewFailure("NotEqual", msg, map[string]interface{}{"v1": v1, "v2": v2}))
 }
 
-// Empty 参照 assert.Empty() 函数
 func (a *Assertion) Empty(expr interface{}, msg ...interface{}) *Assertion {
-	Empty(a.t, expr, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(isEmpty(expr), NewFailure("Empty", msg, map[string]interface{}{"v": expr}))
 }
 
-// NotEmpty 参照 assert.NotEmpty() 函数
 func (a *Assertion) NotEmpty(expr interface{}, msg ...interface{}) *Assertion {
-	NotEmpty(a.t, expr, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(!isEmpty(expr), NewFailure("NotEmpty", msg, map[string]interface{}{"v": expr}))
 }
 
-// Error 参照 assert.Error() 函数
-func (a *Assertion) Error(expr interface{}, msg ...interface{}) *Assertion {
-	Error(a.t, expr, msg...)
-	return a
+// Error 断言有错误发生
+//
+// 传递未初始化的 error 值(var err error = nil),将断言失败
+//
+// NotNil 的特化版本,限定了类型为 error。
+func (a *Assertion) Error(expr error, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(!isNil(expr), NewFailure("Error", msg, map[string]interface{}{"v": expr}))
 }
 
-// ErrorString 参照 assert.ErrorString() 函数
-func (a *Assertion) ErrorString(expr interface{}, str string, msg ...interface{}) *Assertion {
-	ErrorString(a.t, expr, str, msg...)
-	return a
+// ErrorString 断言有错误发生且错误信息中包含指定的字符串 str
+//
+// 传递未初始化的 error 值(var err error = nil),将断言失败
+func (a *Assertion) ErrorString(expr error, str string, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+
+	if isNil(expr) { // 空值,必定没有错误
+		return a.Assert(false, NewFailure("ErrorString", msg, map[string]interface{}{"v": expr}))
+	}
+	return a.Assert(strings.Contains(expr.Error(), str), NewFailure("ErrorString", msg, map[string]interface{}{"v": expr}))
 }
 
-// ErrorType 参照 assert.ErrorType() 函数
-func (a *Assertion) ErrorType(expr interface{}, typ error, msg ...interface{}) *Assertion {
-	ErrorType(a.t, expr, typ, msg...)
-	return a
+// ErrorIs 断言 expr 为 target 类型
+//
+// 相当于 a.True(errors.Is(expr, target))
+func (a *Assertion) ErrorIs(expr, target error, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(errors.Is(expr, target), NewFailure("ErrorIs", msg, map[string]interface{}{"err": expr}))
 }
 
-// NotError 参照 assert.NotError() 函数
-func (a *Assertion) NotError(expr interface{}, msg ...interface{}) *Assertion {
-	NotError(a.t, expr, msg...)
-	return a
+// NotError 断言没有错误
+//
+// Nil 的特化版本,限定了类型为 error。
+func (a *Assertion) NotError(expr error, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(isNil(expr), NewFailure("NotError", msg, map[string]interface{}{"v": expr}))
 }
 
-// FileExists 参照 assert.FileExists() 函数
 func (a *Assertion) FileExists(path string, msg ...interface{}) *Assertion {
-	FileExists(a.t, path, msg...)
+	a.TB().Helper()
+
+	if _, err := os.Stat(path); err != nil && !errors.Is(err, fs.ErrExist) {
+		return a.Assert(false, NewFailure("FileExists", msg, map[string]interface{}{"err": err}))
+	}
 	return a
 }
 
-// FileNotExists 参照 assert.FileNotExists() 函数
 func (a *Assertion) FileNotExists(path string, msg ...interface{}) *Assertion {
-	FileNotExists(a.t, path, msg...)
+	a.TB().Helper()
+
+	_, err := os.Stat(path)
+	if err == nil {
+		return a.Assert(false, NewFailure("FileNotExists", msg, nil))
+	}
+	if errors.Is(err, fs.ErrExist) {
+		return a.Assert(false, NewFailure("FileNotExists", msg, map[string]interface{}{"err": err}))
+	}
+
 	return a
 }
 
-// Panic 参照 assert.Panic() 函数
-func (a *Assertion) Panic(fn func(), msg ...interface{}) *Assertion {
-	Panic(a.t, fn, msg...)
+func (a *Assertion) FileExistsFS(fsys fs.FS, path string, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+
+	if _, err := fs.Stat(fsys, path); err != nil && !errors.Is(err, fs.ErrExist) {
+		return a.Assert(false, NewFailure("FileExistsFS", msg, map[string]interface{}{"err": err}))
+	}
+
 	return a
 }
 
-// PanicString 参照 assert.PanicString() 函数
-func (a *Assertion) PanicString(fn func(), str string, msg ...interface{}) *Assertion {
-	PanicString(a.t, fn, str, msg...)
+func (a *Assertion) FileNotExistsFS(fsys fs.FS, path string, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+
+	_, err := fs.Stat(fsys, path)
+	if err == nil {
+		return a.Assert(false, NewFailure("FileNotExistsFS", msg, nil))
+	}
+	if errors.Is(err, fs.ErrExist) {
+		return a.Assert(false, NewFailure("FileNotExistsFS", msg, map[string]interface{}{"err": err}))
+	}
+
 	return a
 }
 
-// PanicType 参照 assert.PanicType() 函数
+func (a *Assertion) Panic(fn func(), msg ...interface{}) *Assertion {
+	a.TB().Helper()
+
+	has, _ := hasPanic(fn)
+	return a.Assert(has, NewFailure("Panic", msg, nil))
+}
+
+// PanicString 断言函数会发生 panic 且 panic 信息中包含指定的字符串内容
+func (a *Assertion) PanicString(fn func(), str string, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+
+	if has, m := hasPanic(fn); has {
+		return a.Assert(strings.Contains(fmt.Sprint(m), str), NewFailure("PanicString", msg, map[string]interface{}{"msg": m}))
+	}
+	return a.Assert(false, NewFailure("PanicString", msg, nil))
+}
+
+// PanicType 断言函数会发生 panic 且抛出指定的类型
 func (a *Assertion) PanicType(fn func(), typ interface{}, msg ...interface{}) *Assertion {
-	PanicType(a.t, fn, typ, msg...)
-	return a
+	a.TB().Helper()
+
+	if has, m := hasPanic(fn); has {
+		t1, t2 := getType(true, m, typ)
+		return a.Assert(t1 == t2, NewFailure("PanicType", msg, map[string]interface{}{"v1": t1, "v2": t2}))
+	}
+	return a.Assert(false, NewFailure("PanicType", msg, nil))
 }
 
-// NotPanic 参照 assert.NotPanic() 函数
 func (a *Assertion) NotPanic(fn func(), msg ...interface{}) *Assertion {
-	NotPanic(a.t, fn, msg...)
-	return a
+	a.TB().Helper()
+
+	has, m := hasPanic(fn)
+	return a.Assert(!has, NewFailure("NotPanic", msg, map[string]interface{}{"err": m}))
 }
 
-// Contains 参照 assert.Contains() 函数
+// Contains 断言 container 包含 item 或是包含 item 中的所有项
+//
+// 若 container 是字符串(string、[]byte 和 []rune,不包含 fmt.Stringer 接口),
+// 都将会以字符串的形式判断其是否包含 item。
+// 若 container 是个列表(array、slice、map)则判断其元素中是否包含 item 中的
+// 的所有项,或是 item 本身就是 container 中的一个元素。
 func (a *Assertion) Contains(container, item interface{}, msg ...interface{}) *Assertion {
-	Contains(a.t, container, item, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(isContains(container, item), NewFailure("Contains", msg, map[string]interface{}{"container": container, "item": item}))
 }
 
-// NotContains 参照 assert.NotContains() 函数
+// NotContains 断言 container 不包含 item 或是不包含 item 中的所有项
 func (a *Assertion) NotContains(container, item interface{}, msg ...interface{}) *Assertion {
-	NotContains(a.t, container, item, msg...)
-	return a
+	a.TB().Helper()
+	return a.Assert(!isContains(container, item), NewFailure("NotContains", msg, map[string]interface{}{"container": container, "item": item}))
+}
+
+// Zero 断言是否为零值
+//
+// 最终调用的是 reflect.Value.IsZero 进行判断
+func (a *Assertion) Zero(v interface{}, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(isZero(v), NewFailure("Zero", msg, map[string]interface{}{"v": v}))
+}
+
+// NotZero 断言是否为非零值
+//
+// 最终调用的是 reflect.Value.IsZero 进行判断
+func (a *Assertion) NotZero(v interface{}, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(!isZero(v), NewFailure("NotZero", msg, map[string]interface{}{"v": v}))
+}
+
+// Length 断言长度是否为指定的值
+//
+// v 可以是以下类型:
+//   - map
+//   - string
+//   - slice
+//   - array
+func (a *Assertion) Length(v interface{}, l int, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+
+	rl, err := getLen(v)
+	if err != "" {
+		a.Assert(false, NewFailure("Length", msg, map[string]interface{}{"err": err}))
+	}
+	return a.Assert(rl == l, NewFailure("Length", msg, map[string]interface{}{"l1": rl, "l2": l}))
+}
+
+// NotLength 断言长度不是指定的值
+//
+// v 可以是以下类型:
+//   - map
+//   - string
+//   - slice
+//   - array
+func (a *Assertion) NotLength(v interface{}, l int, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+
+	rl, err := getLen(v)
+	if err != "" {
+		a.Assert(false, NewFailure("NotLength", msg, map[string]interface{}{"err": err}))
+	}
+	return a.Assert(rl != l, NewFailure("NotLength", msg, map[string]interface{}{"l": rl}))
+}
+
+// TypeEqual 断言两个值的类型是否相同
+//
+// ptr 如果为 true,则会在对象为指针时,查找其指向的对象。
+func (a *Assertion) TypeEqual(ptr bool, v1, v2 interface{}, msg ...interface{}) *Assertion {
+	if v1 == v2 {
+		return a
+	}
+
+	a.TB().Helper()
+
+	t1, t2 := getType(ptr, v1, v2)
+	return a.Assert(t1 == t2, NewFailure("TypeEquaal", msg, map[string]interface{}{"v1": t1, "v2": t2}))
+}
+
+// Same 断言为同一个对象
+func (a *Assertion) Same(v1, v2 interface{}, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(isSame(v1, v2), NewFailure("Same", msg, nil))
+}
+
+// NotSame 断言为不是同一个对象
+func (a *Assertion) NotSame(v1, v2 interface{}, msg ...interface{}) *Assertion {
+	a.TB().Helper()
+	return a.Assert(!isSame(v1, v2), NewFailure("NotSame", msg, nil))
+}
+
+func isSame(v1, v2 interface{}) bool {
+	rv1 := reflect.ValueOf(v1)
+	if !canPointer(rv1.Kind()) {
+		return false
+	}
+	rv2 := reflect.ValueOf(v2)
+	if !canPointer(rv2.Kind()) {
+		return false
+	}
+
+	return rv1.Pointer() == rv2.Pointer()
+}
+
+func canPointer(k reflect.Kind) bool {
+	switch k {
+	case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Slice, reflect.UnsafePointer, reflect.Func:
+		return true
+	default:
+		return false
+	}
 }
diff --git a/assertion_test.go b/assertion_test.go
index 94caeed..bbd895f 100644
--- a/assertion_test.go
+++ b/assertion_test.go
@@ -1,59 +1,207 @@
-// Copyright 2014 by caixw, All rights reserved.
-// Use of this source code is governed by a MIT
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
 
 package assert
 
 import (
+	"database/sql"
 	"errors"
+	"fmt"
+	"os"
 	"testing"
+	"time"
 )
 
-func TestAssertion(t *testing.T) {
-	a := New(t)
+type errorImpl struct {
+	msg string
+}
+
+func (err *errorImpl) Error() string {
+	return err.msg
+}
+
+func TestAssertion_True_False(t *testing.T) {
+	a := New(t, true)
 
 	if t != a.TB() {
 		t.Error("a.T与t不相等")
 	}
 
 	a.True(true)
-	a.True(5 == 5, "a.True(5==5 falid")
+	a.True(true, "a.True(5==5 failed")
+
+	a.False(false, "a.False(false) failed")
+	a.False(false, "a.False(4==5) failed")
+}
 
-	a.False(false, "a.False(false) falid")
-	a.False(4 == 5, "a.False(4==5) falid")
+func TestAssertion_Equal_NotEqual_Nil_NotNil(t *testing.T) {
+	a := New(t, false)
 
 	v1 := 4
 	v2 := 4
 	v3 := 5
 	v4 := "5"
 
-	a.Equal(4, 4, "a.Equal(4,4) falid")
-	a.Equal(v1, v2, "a.Equal(v1,v2) falid")
+	a.Equal(4, 4, "a.Equal(4,4) failed")
+	a.Equal(v1, v2, "a.Equal(v1,v2) failed")
 
-	a.NotEqual(4, 5, "a.NotEqual(4,5) falid").
-		NotEqual(v1, v3, "a.NotEqual(v1,v3) falid").
-		NotEqual(v3, v4, "a.NotEqual(v3,v4) falid")
+	a.NotEqual(4, 5, "a.NotEqual(4,5) failed").
+		NotEqual(v1, v3, "a.NotEqual(v1,v3) failed").
+		NotEqual(v3, v4, "a.NotEqual(v3,v4) failed")
 
 	var v5 interface{}
 	v6 := 0
 	v7 := []int{}
 
-	a.Empty(v5, "a.Empty falid").
-		Empty(v6, "a.Empty(0) falid").
-		Empty(v7, "a.Empty(v7) falid")
+	a.Empty(v5, "a.Empty failed").
+		Empty(v6, "a.Empty(0) failed").
+		Empty(v7, "a.Empty(v7) failed")
 
-	a.NotEmpty(1, "a.NotEmpty(1) falid")
+	a.NotEmpty(1, "a.NotEmpty(1) failed")
 
 	a.Nil(v5)
 
-	a.NotNil(v7, "a.Nil(v7) falid").
-		NotNil(v6, "a.NotNil(v6) falid")
+	a.NotNil(v7, "a.Nil(v7) failed").
+		NotNil(v6, "a.NotNil(v6) failed")
+}
+
+func TestAssertion_Error(t *testing.T) {
+	a := New(t, false)
+
+	err := errors.New("test")
+	a.Error(err, "a.Error(err) failed")
+	a.ErrorString(err, "test", "ErrorString(err) failed")
+
+	err2 := &errorImpl{msg: "msg"}
+	a.Error(err2, "ErrorString(errorImpl) failed")
+	a.ErrorString(err2, "msg", "ErrorString(errorImpl) failed")
+
+	var err3 error
+	a.NotError(err3, "var err1 error failed")
+
+	err4 := errors.New("err4")
+	err5 := fmt.Errorf("err5 with %w", err4)
+	a.ErrorIs(err5, err4)
+}
+
+func TestAssertion_FileExists_FileNotExists(t *testing.T) {
+	a := New(t, false)
+
+	a.FileExists("./assert.go", "a.FileExists(c:/windows) failed").
+		FileNotExists("c:/win", "a.FileNotExists(c:/win) failed")
+
+	a.FileExistsFS(os.DirFS("./"), "assert.go", "a.FileExistsFS(c:/windows) failed").
+		FileNotExistsFS(os.DirFS("c:/"), "win", "a.FileNotExistsFS(c:/win) failed")
+}
+
+func TestAssertion_Panic(t *testing.T) {
+	a := New(t, false)
+
+	f1 := func() {
+		panic("panic message")
+	}
+
+	a.Panic(f1)
+	a.PanicString(f1, "panic message")
+	a.PanicType(f1, "abc")
+
+	f1 = func() {
+		panic(errors.New("panic"))
+	}
+	a.PanicType(f1, errors.New("abc"))
+
+	f1 = func() {
+		panic(&errorImpl{msg: "panic"})
+	}
+	a.PanicType(f1, &errorImpl{msg: "abc"})
+
+	f1 = func() {}
+	a.NotPanic(f1)
+}
+
+func TestAssertion_Zero_NotZero(t *testing.T) {
+	a := New(t, false)
+
+	var v interface{}
+	a.Zero(0)
+	a.Zero(nil)
+	a.Zero(time.Time{})
+	a.Zero(v)
+	a.Zero([2]int{0, 0})
+	a.Zero([0]int{})
+	a.Zero(&time.Time{})
+	a.Zero(sql.NullTime{})
+
+	a.NotZero([]int{0, 0})
+	a.NotZero([]int{})
+}
+
+func TestAssertion_Length_NotLength(t *testing.T) {
+	a := New(t, false)
+
+	a.Length(nil, 0)
+	a.Length([]int{1, 2}, 2)
+	a.Length([3]int{1, 2, 3}, 3)
+	a.NotLength([3]int{1, 2, 3}, 2)
+	a.Length(map[string]string{"1": "1", "2": "2"}, 2)
+	a.NotLength(map[string]string{"1": "1", "2": "2"}, 3)
+	slices := []rune{'a', 'b', 'c'}
+	ps := &slices
+	pps := &ps
+	a.Length(pps, 3)
+	a.NotLength(pps, 2)
+	a.Length("string", 6)
+	a.NotLength("string", 4)
+}
+
+func TestAssertion_Contains(t *testing.T) {
+	a := New(t, false)
+
+	a.Contains([]int{1, 2, 3}, []int8{1, 2}).
+		NotContains([]int{1, 2, 3}, []int8{1, 3})
+}
+
+func TestAssertion_TypeEqual(t *testing.T) {
+	a := New(t, true)
+
+	a.TypeEqual(false, 1, 2)
+	a.TypeEqual(false, 1, 1)
+	a.TypeEqual(false, 1.0, 2.0)
+
+	v1 := 5
+	pv1 := &v1
+	a.TypeEqual(false, 1, v1)
+	a.TypeEqual(true, 1, &pv1)
+
+	v2 := &errorImpl{}
+	v3 := errorImpl{}
+	a.TypeEqual(false, v2, v2)
+	a.TypeEqual(true, v2, v3)
+	a.TypeEqual(true, v2, &v3)
+	a.TypeEqual(true, &v2, &v3)
+}
+
+func TestAssertion_Same(t *testing.T) {
+	a := New(t, false)
+
+	a.NotSame(5, 5)
+	a.NotSame(struct{}{}, struct{}{})
+	a.NotSame(func() {}, func() {})
+
+	i := 5
+	a.NotSame(i, i)
 
-	v9 := errors.New("test")
-	a.Error(v9, "a.Error(v9) falid")
+	empty := struct{}{}
+	empty2 := empty
+	a.NotSame(empty, empty)
+	a.NotSame(empty, empty2)
+	a.Same(&empty, &empty)
+	a.Same(&empty, &empty2)
 
-	a.NotError("abc", "a.NotError falid")
+	f := func() {}
+	f2 := f
+	a.Same(f, f)
+	a.Same(f, f2)
 
-	a.FileExists("./assert.go", "a.FileExists(c:/windows) falid").
-		FileNotExists("c:/win", "a.FileNotExists(c:/win) falid")
+	a.NotSame(5, 5)
+	a.NotSame(f, 5)
 }
diff --git a/debian/changelog b/debian/changelog
index 9c4863b..dce605b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,9 @@
-golang-github-issue9-assert (0.0~git20170908.0.ceac1aa-6) UNRELEASED; urgency=medium
+golang-github-issue9-assert (3.0.4-1) UNRELEASED; urgency=medium
 
   * Set upstream metadata fields: Repository-Browse.
+  * New upstream release.
 
- -- Debian Janitor <janitor@jelmer.uk>  Thu, 10 Nov 2022 02:44:38 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 19 Nov 2022 15:00:42 -0000
 
 golang-github-issue9-assert (0.0~git20170908.0.ceac1aa-5) unstable; urgency=medium
 
diff --git a/doc.go b/doc.go
deleted file mode 100644
index 0ceefbf..0000000
--- a/doc.go
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2014 by caixw, All rights reserved.
-// Use of this source code is governed by a MIT
-// license that can be found in the LICENSE file.
-
-// Package assert 是对 testing 包的一些简单包装。
-// 方便在测试包里少写一点代码。
-//
-// 提供了两种操作方式:直接调用包函数;或是使用 Assertion 对象。
-// 两种方式完全等价,可以根据自己需要,选择一种。
-//  func TestAssert(t *testing.T) {
-//      var v interface{} = 5
-//
-//      // 直接调用包函数
-//      assert.True(t, v == 5, "v的值[%v]不等于5", v)
-//      assert.Equal(t, 5, v, "v的值[%v]不等于5", v)
-//      assert.Nil(t, v)
-//
-//      // 以 Assertion 对象方式使用
-//      a := assert.New(t)
-//      a.True(v==5, "v的值[%v]不等于5", v)
-//      a.Equal(5, v, "v的值[%v]不等于5", v)
-//      a.Nil(v)
-//      a.TB().Log("success")
-//
-//      // 以函数链的形式调用 Assertion 对象的方法
-//      a.True(false).Equal(5,6)
-//  }
-//  // 也可以对 testing.B 使用
-//  func Benchmark1(b *testing.B) {
-//      a := assert.New(b)
-//      a.True(false)
-//      for(i:=0; i<b.N; i++) {
-//          // do something
-//      }
-//  }
-package assert
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..ec2ef52
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/issue9/assert/v3
+
+go 1.17
diff --git a/rest/request.go b/rest/request.go
new file mode 100644
index 0000000..cfc2f48
--- /dev/null
+++ b/rest/request.go
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: MIT
+
+package rest
+
+import (
+	"bytes"
+	"encoding/json"
+	"encoding/xml"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/issue9/assert/v3"
+)
+
+// Request 请求的参数封装
+type Request struct {
+	path    string
+	method  string
+	body    io.Reader
+	queries url.Values
+	params  map[string]string
+	headers map[string]string
+	a       *assert.Assertion
+	client  *http.Client
+}
+
+// NewRequest 获取一条请求的结果
+//
+// method 表示请求方法
+// path 表示请求的路径,域名部分无须填定。可以通过 {} 指定参数,比如:
+//  r := NewRequest(http.MethodGet, "/users/{id}")
+// 之后就可以使用 Params 指定 id 的具体值,达到复用的目的:
+//  resp1 := r.Param("id", "1").Do()
+//  resp2 := r.Param("id", "2").Do()
+func (srv *Server) NewRequest(method, path string) *Request {
+	return NewRequest(srv.a, method, srv.URL()+path).Client(srv.client)
+}
+
+func (srv *Server) Get(path string) *Request {
+	return srv.NewRequest(http.MethodGet, path)
+}
+
+func (srv *Server) Put(path string, body []byte) *Request {
+	return srv.NewRequest(http.MethodPut, path).Body(body)
+}
+
+func (srv *Server) Post(path string, body []byte) *Request {
+	return srv.NewRequest(http.MethodPost, path).Body(body)
+}
+
+func (srv *Server) Patch(path string, body []byte) *Request {
+	return srv.NewRequest(http.MethodPatch, path).Body(body)
+}
+
+func (srv *Server) Delete(path string) *Request {
+	return srv.NewRequest(http.MethodDelete, path)
+}
+
+// NewRequest 以调用链的方式构建一个访问请求对象
+func NewRequest(a *assert.Assertion, method, path string) *Request {
+	return &Request{
+		a:      a,
+		method: method,
+		path:   path,
+	}
+}
+
+func Get(a *assert.Assertion, path string) *Request {
+	return NewRequest(a, http.MethodGet, path)
+}
+
+func Delete(a *assert.Assertion, path string) *Request {
+	return NewRequest(a, http.MethodDelete, path)
+}
+
+func Post(a *assert.Assertion, path string, body []byte) *Request {
+	return NewRequest(a, http.MethodPost, path).Body(body)
+}
+
+func Put(a *assert.Assertion, path string, body []byte) *Request {
+	return NewRequest(a, http.MethodPut, path).Body(body)
+}
+
+func Patch(a *assert.Assertion, path string, body []byte) *Request {
+	return NewRequest(a, http.MethodPatch, path).Body(body)
+}
+
+// Client 指定采用的客户端实例
+//
+// 可以为空,如果为空,那么在 Do 函数中的参数必不能为空。
+func (req *Request) Client(c *http.Client) *Request {
+	req.client = c
+	return req
+}
+
+// Query 添加一个请求参数
+func (req *Request) Query(key, val string) *Request {
+	if req.queries == nil {
+		req.queries = url.Values{}
+	}
+
+	req.queries.Add(key, val)
+
+	return req
+}
+
+// Header 指定请求时的报头
+func (req *Request) Header(key, val string) *Request {
+	if req.headers == nil {
+		req.headers = make(map[string]string, 5)
+	}
+
+	req.headers[key] = val
+
+	return req
+}
+
+// Param 替换参数
+func (req *Request) Param(key, val string) *Request {
+	if req.params == nil {
+		req.params = make(map[string]string, 5)
+	}
+
+	req.params[key] = val
+
+	return req
+}
+
+// Body 指定提交的内容
+func (req *Request) Body(body []byte) *Request {
+	req.body = bytes.NewReader(body)
+	return req
+}
+
+func (req *Request) StringBody(body string) *Request {
+	req.body = bytes.NewBufferString(body)
+	return req
+}
+
+// BodyFunc 指定一个未编码的对象
+//
+// marshal 对 obj 的编码函数,比如 json.Marshal 等。
+func (req *Request) BodyFunc(obj interface{}, marshal func(interface{}) ([]byte, error)) *Request {
+	req.a.TB().Helper()
+
+	data, err := marshal(obj)
+	req.a.NotError(err).NotNil(data)
+	return req.Body(data)
+}
+
+// JSONBody 指定一个 JSON 格式的 body
+//
+// NOTE: 此函并不会设置 content-type 报头。
+func (req *Request) JSONBody(obj interface{}) *Request {
+	return req.BodyFunc(obj, json.Marshal)
+}
+
+// XMLBody 指定一个 XML 格式的 body
+//
+// NOTE: 此函并不会设置 content-type 报头。
+func (req *Request) XMLBody(obj interface{}) *Request {
+	return req.BodyFunc(obj, xml.Marshal)
+}
+
+func (req *Request) buildPath() string {
+	path := req.path
+
+	for key, val := range req.params {
+		key = "{" + key + "}"
+		path = strings.Replace(path, key, val, -1)
+	}
+
+	if len(req.queries) > 0 {
+		path += ("?" + req.queries.Encode())
+	}
+
+	return path
+}
+
+// Request 返回标准库的 http.Request 实例
+func (req *Request) Request() *http.Request {
+	req.a.TB().Helper()
+
+	r, err := http.NewRequest(req.method, req.buildPath(), req.body)
+	req.a.NotError(err).NotNil(r)
+	r.Close = true
+
+	for k, v := range req.headers {
+		r.Header.Add(k, v)
+	}
+
+	return r
+}
diff --git a/rest/request_test.go b/rest/request_test.go
new file mode 100644
index 0000000..efaaaa2
--- /dev/null
+++ b/rest/request_test.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: MIT
+
+package rest
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/issue9/assert/v3"
+)
+
+func TestRequest_buildPath(t *testing.T) {
+	srv := NewServer(assert.New(t, false), h, nil)
+	a := srv.Assertion()
+	a.NotNil(srv)
+
+	req := srv.NewRequest(http.MethodGet, "/get")
+	a.NotNil(req)
+	a.Equal(req.buildPath(), srv.URL()+"/get")
+
+	req.Param("id", "1").Query("page", "5")
+	a.Equal(req.buildPath(), srv.URL()+"/get?page=5")
+
+	req = srv.NewRequest(http.MethodGet, "/users/{id}/orders/{oid}")
+	a.NotNil(req)
+	a.Equal(req.buildPath(), srv.URL()+"/users/{id}/orders/{oid}")
+	req.Param("id", "1").Param("oid", "2").Query("page", "5")
+	a.Equal(req.buildPath(), srv.URL()+"/users/1/orders/2?page=5")
+}
diff --git a/rest/response.go b/rest/response.go
new file mode 100644
index 0000000..dbf1dfa
--- /dev/null
+++ b/rest/response.go
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: MIT
+
+package rest
+
+import (
+	"bytes"
+	"encoding/json"
+	"encoding/xml"
+	"errors"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+
+	"github.com/issue9/assert/v3"
+)
+
+// Response 测试请求的返回结构
+type Response struct {
+	resp *http.Response
+	a    *assert.Assertion
+	body []byte
+}
+
+// Do 执行请求操作
+//
+// h 默认为空,如果不为空,则表示当前请求忽略 http.Client,而是访问 h.ServeHTTP 的内容。
+func (req *Request) Do(h http.Handler) *Response {
+	if req.client == nil && h == nil {
+		panic("h 不能为空")
+	}
+
+	req.a.TB().Helper()
+
+	r := req.Request()
+	var err error
+	var resp *http.Response
+	if h != nil {
+		w := httptest.NewRecorder()
+		h.ServeHTTP(w, r)
+		resp = w.Result()
+	} else {
+		resp, err = req.client.Do(r)
+		req.a.NotError(err).NotNil(resp)
+	}
+
+	var bs []byte
+	if resp.Body != nil {
+		bs, err = io.ReadAll(resp.Body)
+		if err != io.EOF {
+			req.a.NotError(err)
+		}
+		req.a.NotError(resp.Body.Close())
+	}
+
+	return &Response{
+		a:    req.a,
+		resp: resp,
+		body: bs,
+	}
+}
+
+// Resp 返回 http.Response 实例
+//
+// NOTE: http.Response.Body 内容已经被读取且关闭。
+func (resp *Response) Resp() *http.Response { return resp.resp }
+
+func (resp *Response) assert(expr bool, f *assert.Failure) *Response {
+	resp.a.TB().Helper()
+	resp.a.Assert(expr, f)
+	return resp
+}
+
+// Success 状态码是否在 100-399 之间
+func (resp *Response) Success(msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	succ := resp.resp.StatusCode >= 100 && resp.resp.StatusCode < 400
+	return resp.assert(succ, assert.NewFailure("Success", msg, map[string]interface{}{"status": resp.resp.StatusCode}))
+}
+
+// Fail 状态码是否大于 399
+func (resp *Response) Fail(msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	fail := resp.resp.StatusCode >= 400
+	return resp.assert(fail, assert.NewFailure("Fail", msg, map[string]interface{}{"status": resp.resp.StatusCode}))
+}
+
+// Status 判断状态码是否与 status 相等
+func (resp *Response) Status(status int, msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	eq := resp.resp.StatusCode == status
+	return resp.assert(eq, assert.NewFailure("Status", msg, map[string]interface{}{"status1": resp.resp.StatusCode, "status2": status}))
+}
+
+// NotStatus 判断状态码是否与 status 不相等
+func (resp *Response) NotStatus(status int, msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	neq := resp.resp.StatusCode != status
+	return resp.assert(neq, assert.NewFailure("NotStatus", msg, map[string]interface{}{"status": resp.resp.StatusCode}))
+}
+
+// Header 判断指定的报头是否与 val 相同
+//
+// msg 可以为空,会返回一个默认的错误提示信息
+func (resp *Response) Header(key string, val string, msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	h := resp.resp.Header.Get(key)
+	return resp.assert(h == val, assert.NewFailure("Header", msg, map[string]interface{}{"header": key, "v1": h, "v2": val}))
+}
+
+// NotHeader 指定的报头必定不与 val 相同。
+func (resp *Response) NotHeader(key string, val string, msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	h := resp.resp.Header.Get(key)
+	return resp.assert(h != val, assert.NewFailure("NotHeader", msg, map[string]interface{}{"header": key, "v": h}))
+}
+
+// Body 断言内容与 val 相同
+func (resp *Response) Body(val []byte, msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	return resp.assert(bytes.Equal(resp.body, val), assert.NewFailure("Body", msg, map[string]interface{}{"v1": string(resp.body), "v2": string(val)}))
+}
+
+// StringBody 断言内容与 val 相同
+func (resp *Response) StringBody(val string, msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	b := string(resp.body)
+	return resp.assert(b == val, assert.NewFailure("StringBody", msg, map[string]interface{}{"v1": b, "v2": val}))
+}
+
+// BodyNotEmpty 报文内容是否不为空
+func (resp *Response) BodyNotEmpty(msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	return resp.assert(len(resp.body) > 0, assert.NewFailure("BodyNotEmpty", msg, nil))
+}
+
+// BodyEmpty 报文内容是否为空
+func (resp *Response) BodyEmpty(msg ...interface{}) *Response {
+	resp.a.TB().Helper()
+	return resp.assert(len(resp.body) == 0, assert.NewFailure("BodyEmpty", msg, map[string]interface{}{"v": resp.body}))
+}
+
+// JSONBody body 转换成 JSON 对象之后是否等价于 val
+func (resp *Response) JSONBody(val interface{}) *Response {
+	resp.a.TB().Helper()
+	return resp.BodyFunc(func(a *assert.Assertion, body []byte) {
+		a.TB().Helper()
+
+		// NOTE: 应当始终将 body 转换 val 相同的类型,然后再比较对象,
+		// 因为 val 转换成字符串,可能因为空格缩进等原因,未必会与 body 是相同的。
+		b, err := UnmarshalObject(body, val, json.Unmarshal)
+		a.NotError(err).Equal(b, val)
+	})
+}
+
+// XMLBody body 转换成 XML 对象之后是否等价于 val
+func (resp *Response) XMLBody(val interface{}) *Response {
+	resp.a.TB().Helper()
+	return resp.BodyFunc(func(a *assert.Assertion, body []byte) {
+		a.TB().Helper()
+
+		// NOTE: 应当始终将 body 转换 val 相同的类型,然后再比较对象,
+		// 因为 val 转换成字符串,可能因为空格缩进等原因,未必会与 body 是相同的。
+		b, err := UnmarshalObject(body, val, xml.Unmarshal)
+		a.NotError(err).Equal(b, val)
+	})
+}
+
+// BodyFunc 指定对 body 内容的断言方式
+func (resp *Response) BodyFunc(f func(a *assert.Assertion, body []byte)) *Response {
+	resp.a.TB().Helper()
+
+	b := make([]byte, len(resp.body))
+	copy(b, resp.body)
+	f(resp.a, b)
+
+	return resp
+}
+
+// UnmarshalObject 将 data 以 u 作为转换方式转换成与 val 相同的类型
+//
+// 如果 val 是指针,则会转换成其指向的类型,返回的对象是指针类型。
+func UnmarshalObject(data []byte, val interface{}, u func([]byte, interface{}) error) (interface{}, error) {
+	t := reflect.TypeOf(val)
+	for t.Kind() == reflect.Ptr {
+		t = t.Elem()
+	}
+	bv := reflect.New(t)
+
+	if err := u(data, bv.Interface()); err != nil && !errors.Is(err, io.EOF) {
+		return nil, err
+	}
+	return bv.Interface(), nil
+}
diff --git a/rest/response_test.go b/rest/response_test.go
new file mode 100644
index 0000000..6e76b8c
--- /dev/null
+++ b/rest/response_test.go
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: MIT
+
+package rest
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/issue9/assert/v3"
+)
+
+func TestRequest_Do(t *testing.T) {
+	a := assert.New(t, false)
+	srv := NewServer(a, h, nil)
+
+	srv.Get("/get").
+		Do(nil).
+		Success().
+		Status(201)
+
+	srv.NewRequest(http.MethodGet, "/not-exists").
+		Do(nil).
+		Fail()
+
+	srv.NewRequest(http.MethodGet, "/get").
+		Do(BuildHandler(a, 202, "", nil)).
+		Status(202)
+
+	r := Get(a, "/get")
+	r.Do(BuildHandler(a, 202, "", nil)).Status(202)
+	r.Do(BuildHandler(a, 203, "", nil)).Status(203)
+	a.Panic(func() {
+		r.Do(nil)
+	})
+}
+
+func TestResponse(t *testing.T) {
+	srv := NewServer(assert.New(t, false), h, nil)
+
+	srv.NewRequest(http.MethodGet, "/body").
+		Header("content-type", "application/json").
+		Query("page", "5").
+		JSONBody(&bodyTest{ID: 5}).
+		Do(nil).
+		Status(http.StatusCreated).
+		NotStatus(http.StatusNotFound).
+		Header("content-type", "application/json;charset=utf-8").
+		NotHeader("content-type", "invalid value").
+		JSONBody(&bodyTest{ID: 6}).
+		Body([]byte(`{"id":6}`)).
+		StringBody(`{"id":6}`).
+		BodyNotEmpty()
+
+	srv.NewRequest(http.MethodGet, "/get").
+		Query("page", "5").
+		Do(nil).
+		Status(http.StatusCreated).
+		NotHeader("content-type", "invalid value").
+		BodyEmpty()
+
+	// xml
+
+	srv.NewRequest(http.MethodGet, "/body").
+		Header("content-type", "application/xml").
+		XMLBody(&bodyTest{ID: 5}).
+		Do(nil).
+		Success(http.StatusCreated).
+		Header("content-type", "application/xml;charset=utf-8").
+		XMLBody(&bodyTest{ID: 6})
+}
diff --git a/rest/rest.go b/rest/rest.go
new file mode 100644
index 0000000..b8312d0
--- /dev/null
+++ b/rest/rest.go
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: MIT
+
+// Package rest 简单的 API 测试库
+package rest
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+
+	"github.com/issue9/assert/v3"
+)
+
+// BuildHandler 生成用于测试的 http.Handler 对象
+//
+// 仅是简单地按以下步骤输出内容:
+//   - 输出状态码 code;
+//   - 输出报头 headers,以 Add 方式,而不是 set,不会覆盖原来的数据;
+//   - 输出 body,如果为空字符串,则不会输出;
+func BuildHandler(a *assert.Assertion, code int, body string, headers map[string]string) http.Handler {
+	return http.HandlerFunc(BuildHandlerFunc(a, code, body, headers))
+}
+
+func BuildHandlerFunc(a *assert.Assertion, code int, body string, headers map[string]string) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		a.TB().Helper()
+
+		for k, v := range headers {
+			w.Header().Add(k, v)
+		}
+		w.WriteHeader(code)
+
+		if body != "" {
+			_, err := w.Write([]byte(body))
+			a.NotError(err)
+		}
+	}
+}
+
+func (srv *Server) RawHTTP(req, resp string) *Server {
+	srv.Assertion().TB().Helper()
+	RawHTTP(srv.Assertion(), srv.client, req, resp)
+	return srv
+}
+
+// RawHTTP 通过原始数据进行比较请求和返回数据是符合要求
+//
+// reqRaw 表示原始的请求数据;
+// respRaw 表示返回之后的原始数据;
+//
+// NOTE: 仅判断状态码、报头和实际内容是否相同,而不是直接比较两个 http.Response 的值。
+func RawHTTP(a *assert.Assertion, client *http.Client, reqRaw, respRaw string) {
+	if client == nil {
+		client = &http.Client{}
+	}
+	a.TB().Helper()
+
+	r, resp := readRaw(a, reqRaw, respRaw)
+	if r == nil {
+		return
+	}
+
+	ret, err := client.Do(r)
+	a.NotError(err).NotNil(ret)
+
+	compare(a, resp, ret.StatusCode, ret.Header, ret.Body)
+	a.NotError(ret.Body.Close())
+}
+
+// RawHandler 通过原始数据进行比较请求和返回数据是符合要求
+//
+// 功能上与 RawHTTP 相似,处理方式从 http.Client 变成了 http.Handler。
+func RawHandler(a *assert.Assertion, h http.Handler, reqRaw, respRaw string) {
+	if h == nil {
+		panic("h 不能为空")
+	}
+	a.TB().Helper()
+
+	r, resp := readRaw(a, reqRaw, respRaw)
+	if r == nil {
+		return
+	}
+
+	ret := httptest.NewRecorder()
+	h.ServeHTTP(ret, r)
+
+	compare(a, resp, ret.Code, ret.Header(), ret.Body)
+}
+
+func readRaw(a *assert.Assertion, reqRaw, respRaw string) (*http.Request, *http.Response) {
+	a.TB().Helper()
+
+	resp, err := http.ReadResponse(bufio.NewReader(bytes.NewBufferString(respRaw)), nil)
+	a.NotError(err).NotNil(resp)
+
+	r, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(reqRaw)))
+	a.NotError(err).NotNil(r)
+	u, err := url.Parse(r.Host + r.URL.String())
+	a.NotError(err).NotNil(u)
+	r.RequestURI = ""
+	r.URL = u
+
+	return r, resp
+}
+
+func compare(a *assert.Assertion, resp *http.Response, status int, header http.Header, body io.Reader) {
+	a.Equal(resp.StatusCode, status, "compare 断言失败,状态码的期望值 %d 与实际值 %d 不同", resp.StatusCode, status)
+
+	for k := range resp.Header {
+		respV := resp.Header.Get(k)
+		retV := header.Get(k)
+		a.Equal(respV, retV, "compare 断言失败,报头 %s 的期望值 %s 与实际值 %s 不相同", k, respV, retV)
+	}
+
+	retB, err := io.ReadAll(body)
+	a.NotError(err).NotNil(retB)
+	respB, err := io.ReadAll(resp.Body)
+	a.NotError(err).NotNil(respB)
+	retB = bytes.TrimSpace(retB)
+	respB = bytes.TrimSpace(respB)
+	a.Equal(respB, retB, "compare 断言失败,内容的期望值与实际值不相同\n%s\n\n%s\n", respB, retB)
+}
diff --git a/rest/rest_test.go b/rest/rest_test.go
new file mode 100644
index 0000000..ba1e431
--- /dev/null
+++ b/rest/rest_test.go
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: MIT
+
+package rest
+
+import (
+	"encoding/json"
+	"encoding/xml"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/issue9/assert/v3"
+)
+
+type bodyTest struct {
+	XMLName struct{} `json:"-" xml:"root"`
+	ID      int      `json:"id" xml:"id"`
+}
+
+var h = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	if r.URL.Path == "/get" {
+		w.WriteHeader(http.StatusCreated)
+		return
+	}
+
+	if r.URL.Path == "/body" {
+		if r.Header.Get("content-type") == "application/json" {
+			b := &bodyTest{}
+			bs, err := io.ReadAll(r.Body)
+			if err != nil {
+				fmt.Println(err)
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+
+			if err := json.Unmarshal(bs, b); err != nil {
+				fmt.Println(err)
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+
+			b.ID++
+			bs, err = json.Marshal(b)
+			if err != nil {
+				fmt.Println(err)
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+			w.Header().Add("content-Type", "application/json;charset=utf-8")
+			w.WriteHeader(http.StatusCreated)
+			w.Write(bs)
+			return
+		}
+
+		if r.Header.Get("content-type") == "application/xml" {
+			b := &bodyTest{}
+			bs, err := io.ReadAll(r.Body)
+			if err != nil {
+				fmt.Println(err)
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+
+			if err := xml.Unmarshal(bs, b); err != nil {
+				fmt.Println(err)
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+
+			b.ID++
+			bs, err = xml.Marshal(b)
+			if err != nil {
+				fmt.Println(err)
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+			w.Header().Add("content-Type", "application/xml;charset=utf-8")
+			w.WriteHeader(http.StatusCreated)
+			w.Write(bs)
+			return
+		}
+
+		w.WriteHeader(http.StatusUnsupportedMediaType)
+		return
+	}
+
+	w.WriteHeader(http.StatusNotFound)
+})
+
+func TestBuildHandler(t *testing.T) {
+	a := assert.New(t, false)
+
+	h := BuildHandler(a, 201, "body", map[string]string{"k1": "v1"})
+	w := httptest.NewRecorder()
+	r, err := http.NewRequest(http.MethodGet, "/", nil)
+	a.NotError(err).NotNil(r)
+	h.ServeHTTP(w, r)
+	a.Equal(w.Code, 201).
+		Equal(w.Header().Get("k1"), "v1")
+}
+
+var raw = []*struct {
+	req, resp string
+}{
+	{
+		req: `GET /get HTTP/1.1
+Host: {host}
+
+`,
+		resp: `HTTP/1.1 201
+
+`,
+	},
+	{
+		req: `POST /body HTTP/1.1
+Host: {host}
+Content-Type: application/json
+Content-Length: 8
+
+{"id":5}
+
+`,
+		resp: `HTTP/1.1 201
+Content-Type: application/json;charset=utf-8
+
+{"id":6}
+
+`,
+	},
+	{
+		req: `DELETE /body?page=5 HTTP/1.0
+Host: {host}
+Content-Type: application/xml
+Content-Length: 23
+
+<root><id>6</id></root>
+
+`,
+		resp: `HTTP/1.0 201
+Content-Type: application/xml;charset=utf-8
+
+<root><id>7</id></root>
+
+`,
+	},
+}
+
+func TestServer_RawHTTP(t *testing.T) {
+	a := assert.New(t, true)
+	s := NewServer(a, h, nil)
+
+	for _, item := range raw {
+		req := strings.Replace(item.req, "{host}", s.URL(), 1)
+		s.RawHTTP(req, item.resp)
+	}
+}
+
+func TestRawHandler(t *testing.T) {
+	a := assert.New(t, true)
+	host := "http://localhost:88"
+
+	for _, item := range raw {
+		req := strings.Replace(item.req, "{host}", host, 1)
+		RawHandler(a, h, req, item.resp)
+	}
+}
diff --git a/rest/server.go b/rest/server.go
new file mode 100644
index 0000000..fbb3f22
--- /dev/null
+++ b/rest/server.go
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+
+package rest
+
+import (
+	"net/http"
+	"net/http/httptest"
+
+	"github.com/issue9/assert/v3"
+)
+
+// Server 测试服务
+type Server struct {
+	a      *assert.Assertion
+	server *httptest.Server
+	client *http.Client
+	closed bool
+}
+
+// NewServer 声明新的测试服务
+//
+// 如果 client 为 nil,则会采用 &http.Client{} 作为默认值
+func NewServer(a *assert.Assertion, h http.Handler, client *http.Client) *Server {
+	return newServer(a, httptest.NewServer(h), client)
+}
+
+// NewTLSServer 声明新的测试服务
+//
+// 如果 client 为 nil,则会采用 &http.Client{} 作为默认值
+func NewTLSServer(a *assert.Assertion, h http.Handler, client *http.Client) *Server {
+	return newServer(a, httptest.NewTLSServer(h), client)
+}
+
+func newServer(a *assert.Assertion, srv *httptest.Server, client *http.Client) *Server {
+	if client == nil {
+		client = &http.Client{}
+	}
+
+	s := &Server{
+		a:      a,
+		server: srv,
+		client: client,
+	}
+
+	a.TB().Cleanup(func() {
+		s.Close()
+	})
+
+	return s
+}
+
+func (srv *Server) URL() string { return srv.server.URL }
+
+func (srv *Server) Assertion() *assert.Assertion { return srv.a }
+
+// Close 关闭服务
+//
+// 如果未手动调用,则在 testing.TB.Cleanup 中自动调用。
+func (srv *Server) Close() {
+	if srv.closed {
+		return
+	}
+
+	srv.server.Close()
+	srv.closed = true
+}
diff --git a/rest/server_test.go b/rest/server_test.go
new file mode 100644
index 0000000..38e7edb
--- /dev/null
+++ b/rest/server_test.go
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: MIT
+
+package rest
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/issue9/assert/v3"
+)
+
+func TestNew(t *testing.T) {
+	a := assert.New(t, false)
+
+	srv := NewTLSServer(a, nil, nil)
+	a.NotNil(srv)
+	a.Equal(srv.client, &http.Client{})
+	a.True(len(srv.server.URL) > 0)
+
+	client := &http.Client{}
+	srv = NewServer(a, nil, client)
+	a.NotNil(srv)
+	a.Equal(srv.client, client)
+
+	srv.Close()
+	a.True(srv.closed)
+	srv.Close()
+}
diff --git a/util.go b/util.go
index 9335ac8..ad7f99b 100644
--- a/util.go
+++ b/util.go
@@ -1,76 +1,48 @@
-// Copyright 2014 by caixw, All rights reserved.
-// Use of this source code is governed by a MIT
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
 
 package assert
 
 import (
 	"bytes"
+	"fmt"
 	"reflect"
 	"strings"
-	"time"
 )
 
-// IsEmpty 判断一个值是否为空(0, "", false, 空数组等)。
+// 判断一个值是否为空(0, "", false, 空数组等)。
 // []string{""}空数组里套一个空字符串,不会被判断为空。
-func IsEmpty(expr interface{}) bool {
-	if expr == nil {
+func isEmpty(expr interface{}) bool {
+	if isZero(expr) {
 		return true
 	}
 
-	switch v := expr.(type) {
-	case bool:
-		return !v
-	case int:
-		return 0 == v
-	case int8:
-		return 0 == v
-	case int16:
-		return 0 == v
-	case int32:
-		return 0 == v
-	case int64:
-		return 0 == v
-	case uint:
-		return 0 == v
-	case uint8:
-		return 0 == v
-	case uint16:
-		return 0 == v
-	case uint32:
-		return 0 == v
-	case uint64:
-		return 0 == v
-	case string:
-		return len(v) == 0
-	case float32:
-		return 0 == v
-	case float64:
-		return 0 == v
-	case time.Time:
-		return v.IsZero()
-	case *time.Time:
-		return v.IsZero()
+	rv := reflect.ValueOf(expr)
+	for rv.Kind() == reflect.Ptr {
+		rv = rv.Elem()
+	}
+	switch rv.Kind() {
+	case reflect.Slice, reflect.Map, reflect.Array, reflect.Chan: // 长度为 0 的数组也是 empty
+		return rv.Len() == 0
+	default:
+		return false
 	}
+}
 
-	// 符合IsNil条件的,都为Empty
-	if IsNil(expr) {
+func isZero(v interface{}) bool {
+	if isNil(v) || reflect.ValueOf(v).IsZero() {
 		return true
 	}
 
-	// 长度为0的数组也是empty
-	v := reflect.ValueOf(expr)
-	switch v.Kind() {
-	case reflect.Slice, reflect.Map, reflect.Chan:
-		return 0 == v.Len()
+	rv := reflect.ValueOf(v)
+	for rv.Kind() == reflect.Ptr {
+		rv = rv.Elem()
 	}
-
-	return false
+	return rv.IsZero()
 }
 
-// IsNil 判断一个值是否为 nil。
+// isNil 判断一个值是否为 nil。
 // 当特定类型的变量,已经声明,但还未赋值时,也将返回 true
-func IsNil(expr interface{}) bool {
+func isNil(expr interface{}) bool {
 	if nil == expr {
 		return true
 	}
@@ -81,19 +53,20 @@ func IsNil(expr interface{}) bool {
 	return k >= reflect.Chan && k <= reflect.Slice && v.IsNil()
 }
 
-// IsEqual 判断两个值是否相等。
+// 判断两个值是否相等
 //
 // 除了通过 reflect.DeepEqual() 判断值是否相等之外,一些类似
 // 可转换的数值也能正确判断,比如以下值也将会被判断为相等:
-//  int8(5)                     == int(5)
-//  []int{1,2}                  == []int8{1,2}
-//  []int{1,2}                  == [2]int8{1,2}
-//  []int{1,2}                  == []float32{1,2}
-//  map[string]int{"1":"2":2}   == map[string]int8{"1":1,"2":2}
 //
-//  // map的键值不同,即使可相互转换也判断不相等。
-//  map[int]int{1:1,2:2}        != map[int8]int{1:1,2:2}
-func IsEqual(v1, v2 interface{}) bool {
+//	int8(5)                     == int(5)
+//	[]int{1,2}                  == []int8{1,2}
+//	[]int{1,2}                  == [2]int8{1,2}
+//	[]int{1,2}                  == []float32{1,2}
+//	map[string]int{"1":"2":2}   == map[string]int8{"1":1,"2":2}
+//
+//	// map 的键值不同,即使可相互转换也判断不相等。
+//	map[int]int{1:1,2:2}        != map[int8]int{1:1,2:2}
+func isEqual(v1, v2 interface{}) bool {
 	if reflect.DeepEqual(v1, v2) {
 		return true
 	}
@@ -101,9 +74,8 @@ func IsEqual(v1, v2 interface{}) bool {
 	vv1 := reflect.ValueOf(v1)
 	vv2 := reflect.ValueOf(v2)
 
-	// NOTE: 这里返回false,而不是true
 	if !vv1.IsValid() || !vv2.IsValid() {
-		return false
+		return vv1.IsValid() == vv2.IsValid()
 	}
 
 	if vv1 == vv2 {
@@ -113,32 +85,36 @@ func IsEqual(v1, v2 interface{}) bool {
 	vv1Type := vv1.Type()
 	vv2Type := vv2.Type()
 
-	// 过滤掉已经在reflect.DeepEqual()进行处理的类型
+	if vv1Type.Comparable() && vv2Type.Comparable() && v1 == v2 {
+		return true
+	}
+
+	// 过滤掉已经在 reflect.DeepEqual() 进行处理的类型
 	switch vv1Type.Kind() {
 	case reflect.Struct, reflect.Ptr, reflect.Func, reflect.Interface:
 		return false
 	case reflect.Slice, reflect.Array:
-		// vv2.Kind()与vv1的不相同
+		// vv2.Kind() 与 vv1 的不相同
 		if vv2.Kind() != reflect.Slice && vv2.Kind() != reflect.Array {
-			// 虽然类型不同,但可以相互转换成vv1的,如:vv2是string,vv2是[]byte,
+			// 虽然类型不同,但可以相互转换成 vv1 的,如:vv2 是 string,vv2 是 []byte,
 			if vv2Type.ConvertibleTo(vv1Type) {
-				return IsEqual(vv1.Interface(), vv2.Convert(vv1Type).Interface())
+				return isEqual(vv1.Interface(), vv2.Convert(vv1Type).Interface())
 			}
 			return false
 		}
 
-		// reflect.DeepEqual()未考虑类型不同但是类型可转换的情况,比如:
-		// []int{8,9} == []int8{8,9},此处重新对slice和array做比较处理。
+		// reflect.DeepEqual() 未考虑类型不同但是类型可转换的情况,比如:
+		// []int{8,9} == []int8{8,9},此处重新对 slice 和 array 做比较处理。
 		if vv1.Len() != vv2.Len() {
 			return false
 		}
 
 		for i := 0; i < vv1.Len(); i++ {
-			if !IsEqual(vv1.Index(i).Interface(), vv2.Index(i).Interface()) {
+			if !isEqual(vv1.Index(i).Interface(), vv2.Index(i).Interface()) {
 				return false
 			}
 		}
-		return true // for中所有的值比较都相等,返回true
+		return true // for 中所有的值比较都相等,返回 true
 	case reflect.Map:
 		if vv2.Kind() != reflect.Map {
 			return false
@@ -154,7 +130,7 @@ func IsEqual(v1, v2 interface{}) bool {
 			return true
 		}
 
-		// 两个map的键名类型不同
+		// 两个 map 的键名类型不同
 		if vv2Type.Key().Kind() != vv1Type.Key().Kind() {
 			return false
 		}
@@ -165,17 +141,17 @@ func IsEqual(v1, v2 interface{}) bool {
 				return false
 			}
 
-			if !IsEqual(vv1.MapIndex(index).Interface(), vv2Index.Interface()) {
+			if !isEqual(vv1.MapIndex(index).Interface(), vv2Index.Interface()) {
 				return false
 			}
 		}
-		return true // for中所有的值比较都相等,返回true
+		return true // for 中所有的值比较都相等,返回 true
 	case reflect.String:
 		if vv2.Kind() == reflect.String {
 			return vv1.String() == vv2.String()
 		}
-		if vv2Type.ConvertibleTo(vv1Type) { // 考虑v1是string,v2是[]byte的情况
-			return IsEqual(vv1.Interface(), vv2.Convert(vv1Type).Interface())
+		if vv2Type.ConvertibleTo(vv1Type) { // 考虑 v1 是 string,v2 是 []byte 的情况
+			return isEqual(vv1.Interface(), vv2.Convert(vv1Type).Interface())
 		}
 
 		return false
@@ -190,9 +166,9 @@ func IsEqual(v1, v2 interface{}) bool {
 	return false
 }
 
-// HasPanic 判断 fn 函数是否会发生 panic
+// hasPanic 判断 fn 函数是否会发生 panic
 // 若发生了 panic,将把 msg 一起返回。
-func HasPanic(fn func()) (has bool, msg interface{}) {
+func hasPanic(fn func()) (has bool, msg interface{}) {
 	defer func() {
 		if msg = recover(); msg != nil {
 			has = true
@@ -203,14 +179,8 @@ func HasPanic(fn func()) (has bool, msg interface{}) {
 	return
 }
 
-// IsContains 判断 container 是否包含了 item 的内容。若是指针,会判断指针指向的内容,
-// 但是不支持多重指针。
-//
-// 若 container 是字符串(string、[]byte和[]rune,不包含 fmt.Stringer 接口),
-// 都将会以字符串的形式判断其是否包含 item。
-// 若 container是个列表(array、slice、map)则判断其元素中是否包含 item 中的
-// 的所有项,或是 item 本身就是 container 中的一个元素。
-func IsContains(container, item interface{}) bool {
+// isContains 判断 container 是否包含了 item 的内容。若是指针,会判断指针指向的内容
+func isContains(container, item interface{}) bool {
 	if container == nil { // nil不包含任何东西
 		return false
 	}
@@ -218,15 +188,15 @@ func IsContains(container, item interface{}) bool {
 	cv := reflect.ValueOf(container)
 	iv := reflect.ValueOf(item)
 
-	if cv.Kind() == reflect.Ptr {
+	for cv.Kind() == reflect.Ptr {
 		cv = cv.Elem()
 	}
 
-	if iv.Kind() == reflect.Ptr {
+	for iv.Kind() == reflect.Ptr {
 		iv = iv.Elem()
 	}
 
-	if IsEqual(container, item) {
+	if isEqual(container, item) {
 		return true
 	}
 
@@ -243,7 +213,7 @@ func IsContains(container, item interface{}) bool {
 		case byte:
 			return bytes.IndexByte([]byte(c), i) != -1
 		case rune:
-			return bytes.IndexRune([]byte(c), i) != -1
+			return bytes.ContainsRune([]byte(c), i)
 		}
 	case []byte:
 		switch i := iv.Interface().(type) {
@@ -256,12 +226,12 @@ func IsContains(container, item interface{}) bool {
 		case byte:
 			return bytes.IndexByte(c, i) != -1
 		case rune:
-			return bytes.IndexRune(c, i) != -1
+			return bytes.ContainsRune(c, i)
 		}
 	case []rune:
 		switch i := iv.Interface().(type) {
 		case string:
-			return strings.Contains(string(c), string(i))
+			return strings.Contains(string(c), i)
 		case []byte:
 			return strings.Contains(string(c), string(i))
 		case []rune:
@@ -269,7 +239,7 @@ func IsContains(container, item interface{}) bool {
 		case byte:
 			return strings.IndexByte(string(c), i) != -1
 		case rune:
-			return strings.IndexRune(string(c), i) != -1
+			return strings.ContainsRune(string(c), i)
 		}
 	}
 
@@ -284,7 +254,7 @@ func IsContains(container, item interface{}) bool {
 
 		// item 是 container 的一个元素
 		for i := 0; i < cv.Len(); i++ {
-			if IsEqual(cv.Index(i).Interface(), iv.Interface()) {
+			if isEqual(cv.Index(i).Interface(), iv.Interface()) {
 				return true
 			}
 		}
@@ -296,7 +266,7 @@ func IsContains(container, item interface{}) bool {
 			return false
 		}
 
-		// item 的元素比 container 的元素多,必须在判断完 item 不是 container 中的一个元素之
+		// item 的元素比 container 的元素多
 		if iv.Len() > cv.Len() {
 			return false
 		}
@@ -304,7 +274,7 @@ func IsContains(container, item interface{}) bool {
 		// 依次比较 item 的各个子元素是否都存在于 container,且下标都相同
 		ivIndex := 0
 		for i := 0; i < cv.Len(); i++ {
-			if IsEqual(cv.Index(i).Interface(), iv.Index(ivIndex).Interface()) {
+			if isEqual(cv.Index(i).Interface(), iv.Index(ivIndex).Interface()) {
 				if (ivIndex == 0) && (i+iv.Len() > cv.Len()) {
 					return false
 				}
@@ -334,11 +304,11 @@ func IsContains(container, item interface{}) bool {
 
 		// 判断所有 item 的项都存在于 container 中
 		for _, key := range iv.MapKeys() {
-			cvItem := iv.MapIndex(key)
+			cvItem := cv.MapIndex(key)
 			if !cvItem.IsValid() { // container 中不包含该值。
 				return false
 			}
-			if !IsEqual(cvItem.Interface(), iv.MapIndex(key).Interface()) {
+			if !isEqual(cvItem.Interface(), iv.MapIndex(key).Interface()) {
 				return false
 			}
 		}
@@ -348,3 +318,36 @@ func IsContains(container, item interface{}) bool {
 
 	return false
 }
+
+func getLen(v interface{}) (l int, msg string) {
+	r := reflect.ValueOf(v)
+	for r.Kind() == reflect.Ptr {
+		r = r.Elem()
+	}
+
+	if v == nil {
+		return 0, ""
+	}
+
+	switch r.Kind() {
+	case reflect.Array, reflect.String, reflect.Slice, reflect.Map:
+		return r.Len(), ""
+	}
+	return 0, fmt.Sprintf("无法获取 %s 类型的长度信息", r.Kind())
+}
+
+func getType(ptr bool, v1, v2 interface{}) (t1, t2 reflect.Type) {
+	t1 = reflect.TypeOf(v1)
+	t2 = reflect.TypeOf(v2)
+
+	if ptr {
+		for t1.Kind() == reflect.Ptr {
+			t1 = t1.Elem()
+		}
+		for t2.Kind() == reflect.Ptr {
+			t2 = t2.Elem()
+		}
+	}
+
+	return
+}
diff --git a/util_test.go b/util_test.go
index c35600b..e56a9c3 100644
--- a/util_test.go
+++ b/util_test.go
@@ -1,22 +1,37 @@
-// Copyright 2014 by caixw, All rights reserved.
-// Use of this source code is governed by a MIT
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
 
 package assert
 
 import (
 	"testing"
+	"time"
 )
 
+func TestIsZero(t *testing.T) {
+	zero := func(v interface{}) {
+		t.Helper()
+		if !isZero(v) {
+			t.Errorf("zero: %v", v)
+		}
+	}
+
+	zero(nil)
+	zero(struct{}{})
+	zero(time.Time{})
+	zero(&time.Time{})
+}
+
 func TestIsEqual(t *testing.T) {
 	eq := func(v1, v2 interface{}) {
-		if !IsEqual(v1, v2) {
+		t.Helper()
+		if !isEqual(v1, v2) {
 			t.Errorf("eq:[%v]!=[%v]", v1, v2)
 		}
 	}
 
 	neq := func(v1, v2 interface{}) {
-		if IsEqual(v1, v2) {
+		t.Helper()
+		if isEqual(v1, v2) {
 			t.Errorf("eq:[%v]==[%v]", v1, v2)
 		}
 	}
@@ -42,29 +57,29 @@ func TestIsEqual(t *testing.T) {
 	// 比较两个元素类型可相互转换的数组
 	eq(
 		[][]int{
-			[]int{1, 2},
-			[]int{3, 4},
+			{1, 2},
+			{3, 4},
 		},
 		[][]int8{
-			[]int8{1, 2},
-			[]int8{3, 4},
+			{1, 2},
+			{3, 4},
 		},
 	)
 
-	// 比较两个元素类型可转换的map
+	// 比较两个元素类型可转换的 map
 	eq(
 		[]map[int]int{
-			map[int]int{1: 1, 2: 2},
-			map[int]int{3: 3, 4: 4},
+			{1: 1, 2: 2},
+			{3: 3, 4: 4},
 		},
 		[]map[int]int8{
-			map[int]int8{1: 1, 2: 2},
-			map[int]int8{3: 3, 4: 4},
+			{1: 1, 2: 2},
+			{3: 3, 4: 4},
 		},
 	)
 	eq(map[string]int{"1": 1, "2": 2}, map[string]int8{"1": 1, "2": 2})
 
-	// 比较两个元素类型可转换的map
+	// 比较两个元素类型可转换的 map
 	eq(
 		map[int]string{
 			1: "1",
@@ -88,61 +103,99 @@ func TestIsEqual(t *testing.T) {
 	neq(true, "true")
 	neq(true, 1)
 	neq(true, "1")
-	// 判断包含不同键名的两个map
+	// 判断包含不同键名的两个 map
 	neq(map[int]int{1: 1, 2: 2}, map[int]int{5: 5, 6: 6})
+
+	// time
+	loc := time.FixedZone("utf+8", 8*3600)
+	now := time.Now()
+	eq(time.Time{}, time.Time{})
+	neq(now.In(loc), now.In(time.UTC)) // 时区不同
+	n1 := time.Now()
+	n2 := n1.Add(0)
+	eq(n1, n2)
+
+	// 指针
+	v1 := 5
+	v2 := 5
+	p1 := &v1
+	p2 := &v1
+	eq(p1, p2) // 指针相等
+	p2 = &v2
+	eq(p1, p2) // 指向内容相等
 }
 
 func TestIsEmpty(t *testing.T) {
-	if IsEmpty([]string{""}) {
-		t.Error("IsEmpty([]string{\"\"})")
+	if isEmpty([]string{""}) {
+		t.Error("isEmpty([]string{\"\"})")
+	}
+
+	if !isEmpty([]string{}) {
+		t.Error("isEmpty([]string{})")
+	}
+
+	if !isEmpty([]int{}) {
+		t.Error("isEmpty([]int{})")
+	}
+
+	if !isEmpty(map[string]int{}) {
+		t.Error("isEmpty(map[string]int{})")
+	}
+
+	if !isEmpty(0) {
+		t.Error("isEmpty(0)")
+	}
+
+	if !isEmpty(int64(0)) {
+		t.Error("isEmpty(int64(0))")
 	}
 
-	if !IsEmpty([]string{}) {
-		t.Error("IsEmpty([]string{})")
+	if !isEmpty(uint64(0)) {
+		t.Error("isEmpty(uint64(0))")
 	}
 
-	if !IsEmpty([]int{}) {
-		t.Error("IsEmpty([]int{})")
+	if !isEmpty(0.0) {
+		t.Error("isEmpty(0.0)")
 	}
 
-	if !IsEmpty(map[string]int{}) {
-		t.Error("IsEmpty(map[string]int{})")
+	if !isEmpty(float32(0)) {
+		t.Error("isEmpty(0.0)")
 	}
 
-	if !IsEmpty(0) {
-		t.Error("IsEmpty(0)")
+	if !isEmpty("") {
+		t.Error("isEmpty(``)")
 	}
 
-	if !IsEmpty(uint64(0)) {
-		t.Error("IsEmpty(uint64(0))")
+	if !isEmpty([0]int{}) {
+		t.Error("isEmpty([0]int{})")
 	}
 
-	if !IsEmpty(0.0) {
-		t.Error("IsEmpty(0.0)")
+	if !isEmpty(time.Time{}) {
+		t.Error("isEmpty(time.Time{})")
 	}
 
-	if !IsEmpty("") {
-		t.Error("IsEmpty(``)")
+	if !isEmpty(&time.Time{}) {
+		t.Error("isEmpty(&time.Time{})")
 	}
 
-	if IsEmpty("  ") {
-		t.Error("IsEmpty(\"  \")")
+	if isEmpty("  ") {
+		t.Error("isEmpty(\"  \")")
 	}
 }
 
 func TestIsNil(t *testing.T) {
-	if !IsNil(nil) {
-		t.Error("IsNil(nil)")
+	if !isNil(nil) {
+		t.Error("isNil(nil)")
 	}
 
 	var v1 []int
-	if !IsNil(v1) {
-		t.Error("IsNil(v1)")
+	if !isNil(v1) {
+		t.Error("isNil(v1)")
 	}
 
 	var v2 map[string]string
-	if !IsNil(v2) {
-		t.Error("IsNil(v2)")
+	if !isNil(v2) {
+		t.Error("isNil(v2)")
 	}
 }
 
@@ -151,7 +204,7 @@ func TestHasPanic(t *testing.T) {
 		panic("panic")
 	}
 
-	if has, _ := HasPanic(f1); !has {
+	if has, _ := hasPanic(f1); !has {
 		t.Error("f1未发生panic")
 	}
 
@@ -159,7 +212,7 @@ func TestHasPanic(t *testing.T) {
 		f1()
 	}
 
-	if has, msg := HasPanic(f2); !has {
+	if has, msg := hasPanic(f2); !has {
 		t.Error("f2未发生panic")
 	} else if msg != "panic" {
 		t.Errorf("f2发生了panic,但返回信息不正确,应为[panic],但其实返回了%v", msg)
@@ -175,7 +228,7 @@ func TestHasPanic(t *testing.T) {
 		f1()
 	}
 
-	if has, msg := HasPanic(f3); has {
+	if has, msg := hasPanic(f3); has {
 		t.Errorf("f3发生了panic,其信息为:[%v]", msg)
 	}
 
@@ -183,25 +236,44 @@ func TestHasPanic(t *testing.T) {
 		//todo
 	}
 
-	if has, msg := HasPanic(f4); has {
+	if has, msg := hasPanic(f4); has {
 		t.Errorf("f4发生panic,其信息为[%v]", msg)
 	}
 }
 
 func TestIsContains(t *testing.T) {
 	fn := func(result bool, container, item interface{}) {
-		if result != IsContains(container, item) {
-			t.Errorf("%v == (IsContains%v, %v)出错\n", result, container, item)
+		t.Helper()
+		if result != isContains(container, item) {
+			t.Errorf("%v == (isContains(%v, %v))出错\n", result, container, item)
 		}
 	}
 
 	fn(false, nil, nil)
 
 	fn(true, "abc", "a")
-	fn(true, "abc", 'a')       // string vs byte
-	fn(true, "abc", rune('a')) // string vs rune
 	fn(true, "abc", "c")
 	fn(true, "abc", "bc")
+	fn(true, "abc", byte('a'))    // string vs byte
+	fn(true, "abc", rune('a'))    // string vs rune
+	fn(true, "abc", []byte("ab")) // string vs []byte
+	fn(true, "abc", []rune("ab")) // string vs []rune
+
+	fn(true, []byte("abc"), "a")
+	fn(true, []byte("abc"), "c")
+	fn(true, []byte("abc"), "bc")
+	fn(true, []byte("abc"), byte('a'))
+	fn(true, []byte("abc"), rune('a'))
+	fn(true, []byte("abc"), []byte("ab"))
+	fn(true, []byte("abc"), []rune("ab"))
+
+	fn(true, []rune("abc"), "a")
+	fn(true, []rune("abc"), "c")
+	fn(true, []rune("abc"), "bc")
+	fn(true, []rune("abc"), byte('a'))
+	fn(true, []rune("abc"), rune('a'))
+	fn(true, []rune("abc"), []byte("ab"))
+	fn(true, []rune("abc"), []rune("ab"))
 
 	fn(true, "中文a", "中")
 	fn(true, "中文a", "a")
@@ -216,17 +288,21 @@ func TestIsContains(t *testing.T) {
 	fn(true, map[string]int{"1": 1, "2": 2}, map[string]int8{"1": 1})
 	fn(true,
 		map[string][]int{
-			"1": []int{1, 2, 3},
-			"2": []int{4, 5, 6},
+			"1": {1, 2, 3},
+			"2": {4, 5, 6},
 		},
 		map[string][]int8{
-			"1": []int8{1, 2, 3},
-			"2": []int8{4, 5, 6},
+			"1": {1, 2, 3},
+			"2": {4, 5, 6},
 		},
 	)
 
 	fn(false, map[string]int{}, nil)
 	fn(false, map[string]int{"1": 1, "2": 2}, map[string]int8{})
+	fn(false, map[string]int{"1": 1, "2": 2}, map[string]int8{"1": 110}) // 同键名,不同值
+	fn(false, map[string]int{"1": 1, "2": 2}, map[string]int8{"5": 5})
 	fn(false, []int{1, 2, 3}, nil)
 	fn(false, []int{1, 2, 3}, []int8{1, 3})
+	fn(false, []int{1, 2, 3}, []int{1, 2, 3, 4})
+	fn(false, []int{}, []int{1}) // 空数组
 }

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/go.mod
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/request.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/request_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/response.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/response_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/rest.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/rest_test.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/server.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/rest/server_test.go

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/issue9/assert/doc.go

No differences were encountered in the control files

More details

Full run details