GO单元测试
发布日期:2021-11-18 19:17:14 浏览次数:8 分类:技术文章

本文共 16364 字,大约阅读时间需要 54 分钟。

GO单元测试

什么是单元测试

首先我们谈一谈什么是测试,它是验证某种事物是否按预期工作的流程,虽然测试这一概念普遍用于软件行业,但他并不局限于此。

比如你买了一台二手电脑,在交易之前,一定会去试着链接屏幕看显示屏是否正常工作,HDMI,VGA端口,或者USB,AUDIO等接口是否正常输入输出,这个过程其实就是测试。

在程序的世界中,通常需要被测试的内容复杂,测试流程繁琐,往往需要我们编写更多的代码来完成自动化的测试。测试代码需要手动完成,但它可以更快的执行,并且可以让更多的开发人员来共享他们。

下面我们用一个简单的例子来了解一下GO的单元测试

我们有一个Sum方法,它的作用是遍历数组求和。

package mainimport "fmt"func Sum(set []int) int {
var result int for num := range set {
result += num } return result}

也许细心的你已经发现了其中的问题。我们在mian函数中对他进行简单的测试。

func main() {
set := []int{
17, 23, 100, 76, 55} sum := Sum(set) fmt.Printf("sum is : %d", sum)}

结果

sum is : 10

这个答案好像不太符合我们的预期。没错,经过测试我们发现了在循环中忽略了元素的序号。我们对代码稍作修改:

func Sum(set []int) int {
var result int for _, num := range set {
result += num } return result}

再次测试:

sum is : 271

正确

虽然上面所示的方法可能适用于小型项目,但要在main中来测试所有我们想要检测的内容会变得非常麻烦。幸运的是,在testing包,Go为我们提供一些很好的功能,我们可以在不需要太多学习的情况下使用它们。

接下来在同一个包中,创建一个名为 sum_test.go 的文件,并将下面的代码添加到其中。

package mainimport "testing"func TestSum(t *testing.T) {
set := []int{
17, 23, 100, 76, 55} expected := 271 actual := Sum(set) if actual != expected {
t.Errorf("Expect %d, but got %d!", expected, actual) }}

现在我们要运行我们的测试,所以在终端中切换到main包所在目录,并使用下面的命令运行测试。

得到结果:

➜  awesomeProject go test -v=== RUN   TestSum--- PASS: TestSum (0.00s)PASSok      awesomeProject  0.004s

恭喜!你刚刚使用 Go 内置的 testing 编写了第一个测试。现在,让我们深入了解实际发生的事情。

首先,是我们的文件名。Go 要求所有的测试都在以 _test.go 结尾的文件中。这使得我们在检查另一个 package 包的源代码时,确定哪些文件是测试和哪些文件实现功能非常容易。

在看了文件名之后,我们可以直接跳转到代码中,将测试包导入。它为我们提供了一些类型 (如testing.T) ,这些类型提供常见功能,比如在测试失败时设置错误消息。

接下来,是函数 TestSum()。所有的测试都应该以 func TestXxx(*testing.T) 的格式来编写。其中 Xxx 可以是任何字符或数字,而第一个字符需要是大写字符或数字。(译注:一般,Xxx 就是被测试的函数名)

最后,如上所述,我们使用了 TestSum 函数中的参数 *tesing.T。如果我们没有得到预期的结果,我们使用它来设置一个错误,当我们运行测试时,该错误将显示在终端上。我们回到刚才的例子中,请将测试代码中的 expected 更新为 10,然后使用 go test -v 运行测试。您应该会看到显示如下所示错误信息的输出:

➜  awesomeProject go test -v=== RUN   TestSum--- FAIL: TestSum (0.00s)    main_test.go:11: Expect 10, but got 271!FAILexit status 1FAIL    awesomeProject  0.004s

我们的例子只是一个简单的方法单。单元测试是针对程序的最小单元来进行正确性检验的测试工作,一个单元可能是单个程序、类、对象、方法等。 ——维基百科

为什么要写单元测试

减少bug

一个机器,由各种细小的零件组成,如果其中某件零件坏了,机器运行故障。必须保证每个零件都按设计图要求的规格,机器才能正常运行。

一个可单元测试的工程,会把业务、功能分割成规模更小、有独立的逻辑部件,称为单元。单元测试的目标,就是保证各个单元的逻辑正确性。单元测试保障工程各个“零件”按“规格”(需求)执行,从而保证整个“机器”(项目)运行正确,最大限度减少bug。

提高代码质量

由于每个单元有独立的逻辑,做单元测试时需要隔离外部依赖,确保这些依赖不影响验证逻辑。因为要把各种依赖分离,单元测试会促进工程进行组件拆分,整理工程依赖关系,更大程度减少代码耦合。这样写出来的代码,更好维护,更好扩展,从而提高代码质量。

如果程序有bug,我们运行一次全部单元测试,找到不通过的测试,可以很快地定位对应的执行代码。修复代码后,运行对应的单元测试;如还不通过,继续修改,运行测试…直到测试通过。有时,写那个功能模块的员工已离职,项目运行出错(逻辑错误,非pannic),你根本就不知道调试哪个类。如果离职的员工之前写了单元测试,运行一下立马就找到问题点了。单元测试大大减少调试时间,从而达到节约时间成本的效果。

放心重构

重构,每个开发者都会经历,重构后把代码改坏了的情况并不少见。以往,写完一个框架,运行APP,没什么问题,完事。由于最初的框架并不是你写的,可谓牵一发动全身,你改1个方法导致整个框架运行失败…

如果你有单元测试,情况大不相同。写完一个功能,把单元测试写了,确保这个功能逻辑正确;写第二个,单元测试…,道理一样,每个功能做到第一点“保证逻辑正确性”,100个功能块拼在一起肯定不出问题。你大可以放心一边重构,一边运行项目;而不是整体重构完,提心吊胆地run。

什么时候写单元测试

  1. 一是在具体实现代码之前,这是测试驱动开发(TDD)所提倡的。

Test-Driven Development, 测试驱动开发, 是敏捷开发的一项核心实践和技术,也是一种设计方法论。TDD原理是开发功能代码之前,先编写测试用例代码,然后针对测试用例编写功能代码,使其能够通过。由于TDD对开发人员要求非常高,跟传统开发思维不一样,因此实施起来相当困难。

测试驱动开发有好处也有坏处。因为每个测试用例都是根据需求来的,或者说把一个大需求分解成若干小需求编写测试用例,所以测试用例写出来后,开发者写的执行代码,必须满足测试用例。如果测试不通过,则修改执行代码,直到测试用例通过。
好处,通过测试的执行代码,肯定满足需求,而且有助于接口编程,降低代码耦合,也极大降低bug出现几率(如果是极限编程,几乎是不可能有bug)。
坏处,1.投入开发资源(时间和精力);2.由于测试用例在未进行代码设计前写;很有可能限制开发者对代码整体设计;3.可能引起开发人员不满情绪,我觉得这点很严重,毕竟不是人人都喜欢单元测试,尽管单元测试会带给我们相当多的好处。

  1. 二是与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。

  2. 三是编写完功能代码再写单元测试。我的实践经验告诉我,事后编写的单元测试“粒度”都比较粗。对同样的功能代码,采取前两种方案的结果可能是用10个“小”的单测来覆盖,每个单测比较简单易懂,可读性可维护性都比较好(重构时单测的改动不大);而第三种方案写的单测,往往是用1个“大”的单测来覆盖,这个单测逻辑就比较复杂,因为它要测的东西很多,可读性可维护性就比较差。

建议:我个人是比较推荐单元测试与具体实现代码同步进行这个方案的。只有对需求有一定的理解后才能知道什么是代码的正确性,才能写出有效的单元测试来验证正确性,而能写出一些功能代码则说明对需求有一定理解了。

单元测试的标准

单元测试覆盖率

测试覆盖率是统计通过运行程序包的测试多少代码得到执行。 如果执行测试套件导致80%的语句得到了运行,则测试覆盖率为80%。

在一些企业,用单元测试覆盖率常备拿来当做单元测试是否合格的指标,比如代码覆盖率必须达到90%以上。于是一些人员费尽心思提高单元测试的代码覆盖率,然而这种做法有利也有弊,我们将在以后的文章中讨论这点。

go test中如何得到单元测试覆盖率

让我们再次回到开头的例子中

main.go

package mainfunc Sum(set []int) int {
var result int for _, num := range set {
result += num } return result}

main_test.go

package mainimport "testing"func TestSum(t *testing.T) {
set := []int{
17, 23, 100, 76, 55} expected := 271 actual := Sum(set) if actual != expected {
t.Errorf("Expect %d, but got %d!", expected, actual) }}

我们在该文件的目录下运行

go test -cover

得到main包的单元测试覆盖率

➜  awesomeProject go test -coverPASScoverage: 100.0% of statementsok      awesomeProject  0.004s➜  awesomeProject

或者生成html格式的覆盖率报告

➜  awesomeProject  go test -v -coverprofile cover.out main_test.go mian.go === RUN   TestSum--- PASS: TestSum (0.00s)PASScoverage: 100.0% of statementsok      command-line-arguments  0.004s  coverage: 100.0% of statements➜  awesomeProject  go tool cover -html=cover.out -o cover.html

会在当前目录下生成一个cover.out和cover.html文件。用浏览器打开生成的html,绿色表示运行到的代码,红色表示没运行到的代码。

(例子中的代码全部运行到了)
在这里插入图片描述

优雅的 Golang 单元测试

编写测试友好的代码

假设我们需要测试一个 TCP 端口的响应是否符合预期,我们编写了这样的一个函数:

package example  import (    "fmt"    "io/ioutil"    "net"    "strings")  // GatherTCP : connect and check response  func GatherTCP(host string, port int, excepted string) (bool,error) {
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", host, port)) if err != nil {
return false, err } conn, err := net.DialTCP("tcp", nil, addr) // ! if err != nil {
return false, err } defer conn.Close() data, err := ioutil.ReadAll(conn) if err != nil {
return false, err } return strings.EqualFold(string(data), excepted), nil }

函数功能很简单, 但是这段代码不是一段对测试友好的代码。 我们看下叹号(!)注释的地方,这里创建了一个 TCP 连接( net.TCPConn ),但是因

为 net.DialTCP 函数不能被 mock,因此这个函数必须要和外部进行交互。 所以这个函数的单元测试函数不得不适配这种要求,除了编写单元测试的工作外,需要额
外处理以下情况:

  • 需要开启一个独立的 TCP 服务做 mock;
  • 处理监听端口冲突的情况;
  • 合理终结和回收 TCP 服务;

作为一个有追求的新社会青年,我们应该在设计的时候就考虑好如何编写测试用例,我们 用一个模块内的全局变量保存 net.DialTCP 的函数指针,利用这个变量完成单元测试的 mock 处理:

package example  import (    "fmt"    "io/ioutil"    "net"    "strings")  // DialTCP : net.DialTCP wrapper  var DialTCP = func(network string, laddr, raddr *net.TCPAddr)(net.Conn, error) {
return net.DialTCP(network, laddr, raddr) // # } // GatherTCP : connect and check response func GatherTCP(host string, port int, excepted string) (bool,error) {
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d",host, port)) if err != nil {
return false, err } conn, er := DialTCP("tcp", nil, addr) // # if err != nil {
return false, err } defer conn.Close() data, err := ioutil.ReadAll(conn) if err != nil {
return false, err } return strings.EqualFold(string(data), excepted), nil }

井号(#)注释的地方就是修改点,需要注意:

  • 将 net.DialTCP 返回的 *net.TCPConn 对象转换成 net.Conn 接口;
  • 全局变量 DialTCP 需要公开以便被修改;

使用 gomock 来 mock 外部依赖

mock(模拟)对象能够模拟实际依赖对象的功能,同时又不需要非常复杂的准备工作,你需要做的,仅仅就是定义对象接口,然后实现它,再交给测试对象去使用。

在这里插入图片描述

使用 gomock 来生成实现 net.Conn 的测试对象:

$ mockgen -package example_test -destination conn_mock_test.go net Conn

命令执行完后会在当前目录生成一个专用于测试的 MockConn 对象,无需修改 conn_mock_test.go文件。

使用示例:

var (    mockCtrl  *gomock.Controller    mockConn  *MockConn)mockCtrl = gomock.NewController(t) // 创建控制器mockConn = NewMockConn(mockCtrl) // 创建 mock 对象 mockConn.EXPECT().Close().Return(nil) // 声明对应方法期望的响应 mockConn.Close() // 执行并返回上面期望的响应

注入 mock 对象

现在我们可以通过替换掉全局变量 DialTCP 来注入 mock 对象了:

mockCtrl := gomock.NewController(t)  mockConn := NewMockConn(mockCtrl)  dialTCP := example.DialTCP  example.DialTCP = func(network string, laddr, raddr*net.TCPAddr) (net.Conn, error) {
return mockConn, nil } ok, err := example.GatherTCP("", 0, "") // call function example.DialTCP = dialTCP // reset

或者我们可以使用 来辅助注入:

mockCtrl := gomock.NewController(t)  mockConn := NewMockConn(mockCtrl)  stubs := gostub.Stub(&example.DialTCP, func(network string,laddr, raddr *net.TCPAddr) (net.Conn, error) {
return mockConn, nil }) defer stubs.Reset() ok, err := example.GatherTCP("", 0, "") // call function

通过传递 example.DialTCP 变量的地址,gostub 帮我们封装了替换重置过程。

使用 gomock 来编写简单的单元测试

结合刚才准备的工具,我们可以开始编写单元测试了,主要分为以下几个阶段:

  1. setup:装载测试环境(准备测试脚手架,注入 mock 连接,构造测试数据) 2. call:使用测试用例调用测试方法
  2. check:检查方法行为是否符合预期
  3. teardown:卸载测试环境
package example_test  import (    "example"    "io"    "net"    "testing"    "github.com/golang/mock/gomock")  func TestGatherTCPWithResponse(t *testing.T) {
// setup mockCtrl := gomock.NewController(t) mockConn := NewMockConn(mockCtrl) dialTCP := example.DialTCP example.DialTCP = func(network string, laddr, raddr*net.TCPAddr) (net.Conn, error) {
return mockConn, nil } response := "awesome" excepted := "awesome" pass := true mockConn.EXPECT().Close().Return(nil)mockConn.EXPECT().Read(gomock.Any()).AnyTimes().DoAndReturn(func(data []byte) (int, error) {
n := copy(data[:], response) return n, io.EOF })// call ok, err := example.GatherTCP("", 0, excepted)// check if ok != pass {
t.Fatalf("result not equal") } if err != nil {
t.Errorf("test error: %v", err) }// teardown mockCtrl.Finish() example.DialTCP = dialTCP }

简单的表驱动的单元测试

golang 提供了一种定义匿名结构数组的方式:

tests := []struct {
response, excepted string pass bool}{
{
"", "", true},{
"awesome", "awesome", true}, {
"这是中文", "这是中文", true}, {
"fail", "nooo", false},{
"这是中文", "这也是中文", false}, }

上面数组定义了5个元素,每个元素都是一个有 response , excepted 和 pass 的结构, 因此我们可以利用这种方式来定义我们的测试用例:

func TestGatherTCP(t *testing.T) {
// setup mockCtrl := gomock.NewController(t) mockConn := NewMockConn(mockCtrl) dialTCP := example.DialTCP example.DialTCP = func(network string, laddr, raddr*net.TCPAddr) (net.Conn, error) {
return mockConn, nil}// teardown defer func() {
mockCtrl.Finish() example.DialTCP = dialTCP}() tests := []struct {
response, excepted string pass bool }{
{
"", "", true},{
"awesome", "awesome", true}, {
"这是中文", "这是中文", true}, {
"fail", "nooo", false},{
"这是中文", "这也是中文", false}, } for _, test := range tests {
mockConn.EXPECT().Close().Return(nil)mockConn.EXPECT().Read(gomock.Any()).AnyTimes().DoAndReturn( func(data []byte) (int, error) {
n := copy(data[:], test.response) return n, io.EOF },) ok, err := example.GatherTCP("", 0, test.excepted) if ok != test.pass {
t.Fatalf("result not equal") } if err != nil {
t.Errorf("test error: %v", err) } } }

使用 testify 来编写单元测试

提供了断言和 mock 等一些比标准库更好用的测试工具,配合其提供的 suite 包,

可以编写类似 Python unittest.TestCase 风格的测试用例。
testify suite 要求声明一个内嵌了 suite.Suite 的结构,该结构可以声明 SetupTest 和 TearDownTest 两个方法以在每个测试用例运行时装载和卸载测试环境,如:

type exampleTestSuite struct {
suite.Suite dialTCP func(network string, laddr, raddr *net.TCPAddr)(net.Conn, error) } func (s *exampleTestSuite) SetupTest() {
) s.dialTCP = example.DialTCP example.DialTCP = MockDialTCP // replace DialTCP} func (s *exampleTestSuite) TearDownTest() {
example.DialTCP = s.dialTCP // reset DialTCP}

同时,suite 提供了断言相关的方法,使用方式类似 包下类似的方法:

func (s *ExampleTestSuite) TestExample() {
assert.NotNil(s.T(), nil, "must fail") // 等同于 s.NotNil(nil, "must fail")}

为了兼容 go test 命令,使用 suite 包的时候,需要配置测试入口:

func TestExampleTestSuite(t *testing.T) {
suite.Run(t, &exampleTestSuite{
})}

使用 testify 改写表驱动的单元测试

改动点主要有:

  1. 把之前每个函数都需要主动处理的 setup 和 teardown 过程放到独立的函数由框架 处理;
  2. 把测试逻辑改写成 suite 的方法形式;
package example_testimport (  "example"  "io"  "net"  "testing"  "github.com/golang/mock/gomock"  "github.com/stretchr/testify/suite")type exampleTestSuite struct {
suite.Suite mockCtrl *gomock.Controller mockConn *MockConn dialTCP func(network string, laddr, raddr *net.TCPAddr)(net.Conn, error)} func (s *exampleTestSuite) SetupTest() {
s.mockCtrl = gomock.NewController(s.T()) s.mockConn = NewMockConn(s.mockCtrl) s.dialTCP = example.DialTCP example.DialTCP = func(network string, laddr, raddr*net.TCPAddr) (net.Conn, error) {
return s.mockConn, nil} } func (s *exampleTestSuite) TearDownTest() {
s.mockCtrl.Finish() s.mockConn = nil s.mockCtrl = nil example.DialTCP = s.dialTCP } func (s *exampleTestSuite) TestConnected() {
tests := []struct {
response, excepted stringpass bool }{
{
"", "", true},{
"awesome", "awesome", true}, {
"这是中文", "这是中文", true}, {
"fail", "nooo", false},{
"这是中文", "这也是中文", false}, } for _, test := range tests {
s.mockConn.EXPECT().Close().Return(nil)s.mockConn.EXPECT().Read(gomock.Any()).AnyTimes().DoAndReturn(func(data []byte) (int, error) {
n := copy(data[:], test.response) return n, io.EOF }) ok, err := example.GatherTCP("", 0, test.excepted) s.Equal(test.pass, ok) s.Nil(err) } } func (s *exampleTestSuite) TestConnectError() {
tests := []struct {
readErr error }{
{
&net.OpError{
}}, } for _, test := range tests {
s.mockConn.EXPECT().Close().Return(nil)s.mockConn.EXPECT().Read(gomock.Any()).AnyTimes().DoAndReturn(func(data []byte) (int, error) {
return 0, test.readErr }) ok, err := example.GatherTCP("", 0, "") s.Equal(false, ok) s.NotNil(err) }} // TestExampleTestSuite : func TestExampleTestSuite(t *testing.T) {
suite.Run(t, &exampleTestSuite{
})}

使用 ginkgo 来编写单元测试

是一个 BDD 风格的测试框架,能够很好地提升单元测试的表达能力。

准备工作

$ ginkgo bootstrap

这个命令生成 example_suite_test.go文件,用于引导 ginkgo 的启动,无需修改。 添加一个单元测试:

$ ginkgo generate example$ cat example_test.gopackage example_testimport (  . "github.com/onsi/ginkgo"  . "github.com/onsi/gomega". "example" )var _ = Describe("Exampleas", func() {
})

这个命令生成了example_test.go文件,这是主要编写单元测试的地方。

测试环境的加载和卸载

ginkgo 提供了方便的机制来加载测试环境和卸载测试环境,比如我们需要替换 example.DialTCP ,可以这样操作:

var _ = Describe("Example", func() {
var ( dialTCP = example.DialTCP mockCtrl *gomock.Controller mockConn *MockConn connError error = nil)BeforeEach(func() {
// 加载测试环境mockCtrl = gomock.NewController(GinkgoT()) mockConn = NewMockConn(mockCtrl) example.DialTCP = func(network string, laddr, raddr*net.TCPAddr) (net.Conn, error) {
return mockConn, connError }})AfterEach(func() {
// 卸载测试环境 connError = nil example.DialTCP = dialTCP mockCtrl.Finish() })})

这样会在每个单元测试之前替换掉 example.DialTCP 并生成一个 MockConn 进行测试。

使用 ginkgo 改写表驱动形式的单元测试

为了简化单元测试的编写,提倡使用 ginkgo 提供的针对表驱动形式的单元测试 DSL:

package example_testimport ( "io""net"    . "github.com/onsi/ginkgo"    . "github.com/onsi/ginkgo/extensions/table"    . "github.com/onsi/gomega"    "example"    "github.com/golang/mock/gomock")  var _ = Describe("Example", func() {
var ( dialTCP = example.DialTCP mockCtrl *gomock.Controller mockConn *MockConn) BeforeEach(func() {
mockCtrl = gomock.NewController(GinkgoT()) mockConn = NewMockConn(mockCtrl) example.DialTCP = func(network string, laddr, raddr *net.TCPAddr) (net.Conn, error) {
return mockConn, nil }}) AfterEach(func() {
example.DialTCP = dialTCP mockCtrl.Finish()}) DescribeTable( "connected", func(response string, excepted string,pass bool) {
// setup mock mockConn.EXPECT().Close().Return(nil)mockConn.EXPECT().Read(gomock.Any()).AnyTimes().DoAndReturn(func(data []byte) (int, error) {
n := copy(data[:], response) return n, io.EOF }) ok, err := example.GatherTCP("", 0, excepted) Expect(err).Should(BeNil()) Expect(ok).Should(Equal(pass)) }, Entry("emtry ok", "", "", true), Entry("match ok", "awesome", "awesome", true), Entry("chinese ok", "这是中文", "这是中文", true), Entry("match fail", "fail", "nooo", false), Entry("chinese fail", "这是中文", "这也是中文", false), ) DescribeTable( "read error", func(readErr error) {
// setup mock mockConn.EXPECT().Close().Return(nil)mockConn.EXPECT().Read(gomock.Any()).AnyTimes().DoAndReturn(func(data []byte) (int, error) {
return 0, readErr }) ok, err := example.GatherTCP("", 0, "") Expect(err == nil).Should(BeFalse()) Expect(ok).Should(BeFalse()) }, Entry("connect error", &net.OpError{
}), )})

每个 Entry 声明了一个测试用例和对应的响应,不需要重复性地进行函数调用和结果检 查,可以极大简化单元测试编写难度。

转载地址:https://blog.csdn.net/weixin_39172380/article/details/88666479 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:nodejs + vue3.0开发环境搭建
下一篇:GO开发规范

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2024年03月15日 17时45分29秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

matlab 图中的legend,matlab中legend加图示命令的使用 2021-06-24
PHP exec xargs 不执行,Linux中的xargs命令及示例 2021-06-24
php 枚举cookie内容,php设置和获取cookie 2021-06-24
单防区扩展模块怎么用_AB罗克韦尔自动化Micro800 扩展 I/O模块型号及功能介绍 2021-06-24
java矩阵类_Java泛型——泛型矩阵类 2021-06-24
java车牌正则表达式_车牌正则表达式 2021-06-24
wordpress4.9.4 mysql_WordPress 将不再支持 PHP4 和 MySQL 4 2021-06-24
安卓是用java语言写的吗_android开发是用java语言吗? 2021-06-24
java 符号 t_java – 运算符”不能应用于’T’,’T’表示有界泛型类型 2019-04-21
用matlab写出信源熵,计算离散信源的熵matlab实现 2019-04-21
php表单yii2,Yii2创建表单(ActiveForm)方法详解 2019-04-21
php 程序授权机制,授权认证详细说明 2019-04-21
java 命令提示符,如何使用Java打开命令提示符并插入命令? 2019-04-21
IP/tzgm.php,LianjiaSpider/在售数量.ipynb at master · BerSerK/LianjiaSpider · GitHub 2019-04-21
linux移动文件的脚本,使用Linux脚本移动文件 2019-04-21
linux查看系统所有变量,Linux系统各指标命令 2019-04-21
linux打印机守护程序,linux下怎么编写守护程序呢? 2019-04-21
嵌入式linux 设置时间,time_clock控件应用,使用命令date -s 12:00:00手动设置时间后,时间就停住不走了,我在Ubuntu和嵌入式Linux平台都测试到了... 2019-04-21
linux 8086下编译,Ubuntu18.04/Linux下安装DosBox进行8086汇编 2019-04-21
linux监控windows,zabbix监控之linux及windows客户端安装配置 2019-04-21