顶点云(应用)传输、认证单元测试

编写传输和认证模块的单元测试。

Golang 的单元测试需要导入 testing 包,测试函数以 Test 开头,且第一个参数为 testing.T。通常我们设置一个对照列表,这个列表由二元组构成,分别是测试函数输入值和对应的正确输出值。我们编写 verify 函数比对要测试函数的输出和正确输出,如果发现不同则该测试函数失败,并通过 t.Errorf() 报告。

1
2
3
4
5
6
7
8
9
10
11
// authenticate_test.go
func verify(t *testing.T, testnum int, testcase string, input, output, expected []byte, err error) {
if string(expected) != string(output) || err != nil {
t.Errorf("%d. %s with input = %s: output %s != %s",
testnum,
testcase,
string(input),
string(output),
string(expected))
}
}

认证模块单元测试

  • 认证模块的函数耦合度不高,可以独立测试,为每个函数编写一个测试函数。在 authenticate 目录下新建 authenticate_test.go。以 AES CFB 加解密模块为例,下面这三组测试数据是在密钥为 AABCDEFGHIJKLMNOPBCDEFGHIJKLMNOP 情况下加密生成的,我们需要测试 AesEncodingAesDecoding 的正确性。
1
2
3
4
5
6
// authenticate_test.go
var testAESes = []testS{
{"An Apple A Day", "\xca\x35\x19\xc8\x90\x0a\xed\x4f\x69\x09\x3e\xb2\x56\x41"},
{"Keep Doctor Away .", "\xc0\x3e\x5c\xf9\xc0\x3e\xee\x49\x3d\x27\x6c\xd6\x76\x4f\xed\x74\xeb\x1a\xe4"},
{"+w-s*a/d%4", "\xa0\x2c\x14\xfa\xca\x1b\xae\x4e\x6c\x7c"},
}
  • 编写 TestAesDecoding 函数,注意测试列表中的二元组元素均为 string,为了符合 verifyAesDecoding 的参数类型,需要对某些位置使用 []bytestring 做类型转换。我们将测试列表中二元组的后者作为 AesDecoding 的输入,并将其输出和二元组的前者做比对。
1
2
3
4
5
6
7
func TestAesDecoding(t *testing.T) {
c := NewAesBlock([]byte("AABCDEFGHIJKLMNOPBCDEFGHIJKLMNOP"))
for i, item := range testAESes {
s, err := AesDecode([]byte(item.out), int64(len(item.in)), c)
verify(t, i, "AesDecoding 256bits", []byte(item.out), []byte(item.in), s, err)
}
}
  • 同样,将输入、输出参数位置调换即可写出 TestAesEncoding
1
2
3
4
5
6
7
8
// authenticate_test.go
func TestAesEncoding(t *testing.T) {
c := NewAesBlock([]byte("AABCDEFGHIJKLMNOPBCDEFGHIJKLMNOP"))
for i, item := range testAESes {
s := AesEncode([]byte(item.in), c)
verify(t, i, "AesEncoding 256bits", []byte(item.in), []byte(item.out), s, nil)
}
}
  • authenticate_test.go 其它部分代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// authenticate_test.go
package authenticate
import "testing"
type testS struct {
in string
out string
}

var testBase64s = []testS{
{"An Apple A Day", "QW4gQXBwbGUgQSBEYXk="},
{"Keep Doctor Away .", "S2VlcCBEb2N0b3IgQXdheSAgLg=="},
{"+w-s*a/d%4", "K3ctcyphL2QlNA=="},
}

func TestBase64Encoding(t *testing.T) {
for i, item := range testBase64s {
s := Base64Encode([]byte(item.in))
verify(t, i, "Base64Encoding", []byte(item.in), []byte(item.out), s, nil)
}
}

func TestBase64Decoding(t *testing.T) {
for i, item := range testBase64s {
s, err := Base64Decode([]byte(item.out))
verify(t, i, "Base64Decoding", []byte(item.out), []byte(item.in), s, err)
}
}

func TestNewAes(t *testing.T) {
testkey1 := "abcdefghijklmnop"
c := NewAesBlock([]byte(testkey1))
if c == nil {
t.Errorf("NewAesBlock returns nil with 128bits key %s", testkey1)
}
testkey2 := "abcdefghijklmnopabcdefgh"
c = NewAesBlock([]byte(testkey2))
if c == nil {
t.Errorf("NewAesBlock returns nil with 192bits key %s", testkey2)
}
testkey3 := "abcdefghijklmnopabcdefghijklmnop"
c = NewAesBlock([]byte(testkey3))
if c == nil {
t.Errorf("NewAesBlock returns nil with 256bits key %s", testkey3)
}
}

func TestGenerateToken(t *testing.T) {
if !(len(GenerateToken(1)) == 16 &&
len(GenerateToken(2)) == 24 &&
len(GenerateToken(3)) == 32 &&
len(GenerateToken(100)) == 32) {
t.Errorf("Generate Token Error.")
}
}
  • authenticate 目录下启动终端,执行 go test,输出结果为 OK, pass 表明单元测试通过。

传输模块单元测试

传输模块无法直接通过相互独立的测试函数验证,因此编写一个用于测试的简单服务器和客户端。在 transmit 目录下新建 transmit_test.go。测试流程如下:客户端向服务器发送连接请求,连接成功则接收服务器发送的一个文件,之后再接收一组字节。如无错误则调用 Destroy() 断开连接。之后测试函数验证客户端接收到的文件和源文件是否相同,接收到的字节流和源字节流是否相同,如二者相左则调用 t.Errorf() 发出错误报告。

客户端函数

  • 我们要测试的功能包括普通字节传输(RecvBytes/SendBytes)、文件传输(SendFromReader/RecvToWriter)以及 DestroyNewTransmitter 等接口中提供的函数,接口中未导出的函数也会在代码中涉及到。
  • 因为客户端和服务器运行在同一个测试程序中,代码中需要延时一秒以保证服务器启动完成(服务器进入 listen 状态后阻塞,因此客户端需先启动)。
  • 客户端测试函数代码如下:
    • 先启动一个 net.Conn 连接 127.0.0.1:10086 ,即测试服务器监听的端口;
    • 连接成功后创建一个以变量 test_out_filename 命名的文件(该变量是测试代码的全局变量,我们将在编写完测试函数后定义,它指定了测试客户端接收文件时存储的名称);
    • 创建成功后建立一个新的 transmitter,使用变量 pass 存储的内容作为双方加密的密钥(该变量也是测试代码的全局变量);
    • transmitter 创建成功后接收一个文件,再接收一个字符串;
    • 将该字符串和 test_string 比对,test_string 同样是可设置的全局变量;
    • 比对结束,无误退出,否则通过 t.Errorf() 报告错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// transmit_test.go
func client_test(t *testing.T) {
time.Sleep(time.Second)
cconn, err := net.Dial("tcp", "127.0.0.1:10086")
if err != nil {
fmt.Println("ERROR: Error dialing", err.Error())
return
}
defer cconn.Close()
file, err := os.OpenFile(test_out_filename,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("ERROR: Cannot Open TestOutFile")
return
}
defer file.Close()
fileWriter := bufio.NewWriter(file)
ts := NewTransmitter(cconn, BUFSIZE, []byte(pass))
ts.RecvToWriter(fileWriter)
recvB, err := ts.RecvBytes()
if err != nil {
t.Errorf("ERROR: Cannot receive bytes")
return
}
if string(recvB) != test_string {
t.Errorf("ERROR: Receive bytes error")
return
}
ts.Destroy()
return
}

服务器函数(测试函数主线程)

  • 真正的测试函数包含了服务器和客户端的启动,下面简单解释测试函数的流程:
    • 打开要传输的文件,通过函数 GetFileSize 获取文件长度;
    • 监听 127.0.0.1:10086 端口;
    • 启动客户端;
    • 服务器接受客户端请求;
    • 服务器向客户端发送此前打开的文件内容;
    • 服务器发送 test_string 的内容;
    • 至此传输结束,测试函数开始比对文件 test_in_filenametest_out_filename,如果无误则退出,否则调用 t.Errorf() 报告错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// transmit_test.go
func TestTransmission(t *testing.T) {
file, err := os.Open(test_in_filename)
if err != nil {
t.Errorf("Transmit: Cannot Open TestInFile")
return
}
defer file.Close()
fileReader := bufio.NewReader(file)
totalFileLength, err := GetFileSize(test_in_filename)
if err != nil {
t.Errorf("Transmit: GetFileSize function failed")
return
}

// test server
listener, err := net.Listen("tcp", "127.0.0.1:10086")
if err != nil {
fmt.Println("test server starting with an error, break down...")
return
}
defer listener.Close()
go client_test(t)
sconn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting", err.Error())
return
}
fmt.Println("Rececive connection request from",
sconn.RemoteAddr().String())

tr := NewTransmitter(sconn, BUFSIZE, []byte(pass))
tr.SendFromReader(fileReader, int64(totalFileLength))
tr.SendBytes([]byte(test_string))
time.Sleep(time.Second * 2)

// verify received
vfile, err := os.Open(test_out_filename)
if err != nil {
t.Errorf("Transmit: Cannot Open TestOutFile")
return
}
defer vfile.Close()
rfile, err := os.Open(test_in_filename)
if err != nil {
t.Errorf("Transmit: Cannot Open TestOutFile")
return
}
defer rfile.Close()

vfileReader := bufio.NewReader(vfile)
rfileReader := bufio.NewReader(rfile)

for {
rbyte, err1 := rfileReader.ReadByte()
vbyte, err2 := vfileReader.ReadByte()
if err1 != nil && err2 != nil {
break
} else if err != nil || err2 != nil || rbyte != vbyte {
t.Errorf("Transmit: Received File Is Not Same With Origin File")
break
}
}
}
  • 代码中用到的 GetFileSize 可以放到此前的 transmit.go 中,作为一个可复用的函数:
1
2
3
4
5
6
7
8
9
// transmit.go
func GetFileSize(path string) (size int64, err error) {
fileInfo, err := os.Stat(path)
if err != nil {
return -1, err
}
fileSize := fileInfo.Size()
return fileSize, nil
}

全局变量指定

上面代码中用到的几个全局变量和导入的包声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// transmit_test.go
package transmit
import (
"bufio"
"fmt"
"net"
"os"
"testing"
"time"
)

const BUFSIZE int64 = 4096 * 1024
const pass string = "1234567890123456"
const test_in_filename string = "test_in.exe"
const test_out_filename string = "test_out.exe"
const test_string string = "helloworld"

测试

transmit 目录下放置一个名为 test_in.exe 的文件用于测试传输,这里我放置了一个 293M 的 Idea 安装包(已重命名为 test_in.exe)作为测试文件。

transmit 目录下启动终端,执行 go test,输出结果为 OK, pass 表明测试成功,此时 transmit 目录下还会出现 test_out.exe, 该文件内容和之前放置的 test_in.exe 相同。


专栏目录:顶点云(应用)设计与实现
此专栏的上一篇文章:顶点云(应用)认证基础模块实现
此专栏的下一篇文章:顶点云(应用)数据用户设计

原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(http://blog.forec.cn/2016/11/16/zenith-cloud-4/) 、作者信息(Forec)和本声明。

分享到