您的位置:首页 > 其它

测试覆盖率

2016-05-17 00:00 337 查看

PHP覆盖率

phpunit-单元测试框架

先准备好一些东西:
1、待测试的代码
2、已安装好了PHPUnit(最好将phpunit设置成环境变量)
3、测试需要用到的工具包

下面就开始进行测试:
D:\\xampp\\htdocs\\Study\\CodeIgniter2.1.0\\tests>phpunit
PHPUnit 3.5.14 by Sebastian Bergmann.
...........
Time: 2 seconds, Memory: 7.50Mb
<-[30;42m<-[2KOK <11 tests, 7 assertions>
<-[0m<-2K
第二行表示进行单元测试时的执行结果,“.”表是断言成功,出现的还有可能是其它的符号,比如“F”、“E”、“S”等(更多信息请自行查看PHPUnit相关资料);
第三行表示代码的执行时间以及内存的使用情况;
第四行表示执行了11个测试,7个断言;
-------------------------------------------------------------------------------
所有东西安装成功后就开始做单元测试。下面以项目中的welcome.php为例:

[code=plain]<?php
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Welcome extends CI_Controller {
public function index()
{
$this->load->view('welcome_message');
}

public function test($a)
{
if($a >= 0)
{
return true;
}
else
{
return false;
}
}
}

然后在tests目录下新建一个WelcomeTest.php文件:

[code=plain]<?php
class WelcomeTest extends CIUnit_TestCase
{
public function setUp()
{
// Set the tested controller
$this->CI = set_controller('welcome');
}
public function testIndex()
{
// Call the controllers method
$this->CI->index();

// Fetch the buffered output
$out = output();

// Check if the content is OK
$this->assertSame(0, preg_match('/(error|notice)/i', $out));
}
public function testTest()
{
$result = $this->CI->test(5);
$this->assertEquals(true,$result);
}
public function testTest1()
{
$result = $this->CI->test(0);
$this->assertEquals(true,$result);
}
public function testTest2()
{
$result = $this->CI->test(-5);
$this->assertEquals(false,$result);
}
}

在WelcomeTest.php中写了四个测试函数,分别对welcome.php控制器中的index()和test()函数进行了测试。
接下来在cmd.exe下执行命令phpunit(注意是在tests目录下执行):
D:\\xampp\\htdocs\\Study\\CodeIgniter2.1.0\\tests>phpunit
显示结果如下:
PHPUnit 3.5.14 by Sebastian Bergmann.
....
Time: 0 seconds, Memory: 6.00Mb
<-[30;42m<-[2KOK <4 tests, 4 assertions>
<-[0m<-2K
说明:这里有四个测试和断言,结果都是正确。
至此,完成了一个控制器的测试工作,如果想对models或是libraries中的类进行测试,方法与上面类似,也可以查看工具包中给出的示例。
-------------------------------------------------------------------------------
接下来就查看一下单元测试的覆盖率问ti。
其实完成单元测试,也就完成了覆盖率。以刚才的例子来说,将覆盖率以html形式生成并放在coverage_report目录下则只需要在 cmd.exe中执行phpunit --coverage-html(如果对phpunit --coverage-html命令不熟悉,请查阅相关资料)即可:
D:\\xampp\\htdocs\\Study\\CodeIgniter2.1.0\\tests>phpunit --coverage-html coerage_report
在tests目录下会有coerage_report文件夹,里面的内容便是单元测试的覆盖率。

----------

PHPUnit参数详解:

Runs the tests that are provided by the class UnitTest. This class is expected to be declared in the UnitTest.php sourcefile.
UnitTest must be either a class that inherits from PHPUnit_Framework_TestCase or a class that provides a public static suite() method which returns an PHPUnit_Framework_Test object, for example an instance of the PHPUnit_Framework_TestSuite class.
运行类UnitTest提供的所有测试,这个类需要定义在UnitTest.php这个文件中.
类UnitTest需要从PHPUnit_Framework_TestCase继承或者提供一个公开的静态方法suite()返回一个PHPUnit_Framework_Test对象的实例.

phpunit UnitTest Test.php
Runs the tests that are provided by the class UnitTest. This class is expected to be declared in the specified sourcefile.
运行UnitTest提供的所有测试,这个类应该在定义在Test.php中

--log-junit
Generates a logfile in JUnit XML format for the tests run.
生成JUnit XML格式的日志文件

--log-tap
Generates a logfile using the Test Anything Protocol (TAP) format for the tests run.
生成TAP格式的日志文件

--log-dbus
Log test execution using DBUS.
使用DBUS记录测试的执行情况

--log-json
Generates a logfile using the JSON format.
生成JSON格式的日志文件

--coverage-html
Generates a code coverage report in HTML format.
生成html格式的代码覆盖报告
请注意这个功能只能在tokenizer和Xdebug安装后才能使用

--coverage-clover
Generates a logfile in XML format with the code coverage information for the tests run.
生成xml格式的代码覆盖报告
请注意这个功能只能在tokenizer和Xdebug安装后才能使用

--testdox-html and --testdox-text
Generates agile documentation in HTML or plain text format for the tests that are run.
生成记录已运行测试的html或者纯文本格式的文件文档

--filter
Only runs tests whose name matches the given pattern. The pattern can be either the name of a single test or a regular expression that matches multiple test names.
只运行名字符合参数规定的格式的测试,参数可以是一个测试的名字或者一个匹配多个测试名字的正则表达式

--group
Only runs tests from the specified group(s). A test can be tagged as belonging to a group using the @group annotation.
只运行规定的测试组,一个测试可以使用@group注释来分组
The @author annotation is an alias for @group allowing to filter tests based on their authors.
@author注视是一个和@group关联的注释标签,用来根据作者来过滤测试

--exclude-group
Exclude tests from the specified group(s). A test can be tagged as belonging to a group using the @group annotation.
只包含规定的多个测试组,一个测试可以使用@group注释来分组

--list-groups
List available test groups.
列出可用的测试组

--loader
Specifies the PHPUnit_Runner_TestSuiteLoader implementation to use.
定义使用PHPUnit_Runner_TestSuiteLoader的接口

The standard test suite loader will look for the sourcefile in the current working directory and in each directory that is specified in PHP's include_path configuration directive. Following the PEAR Naming Conventions, a class name such as Project_Package_Class is mapped to the sourcefile name Project/Package/Class.php.
标准的standard test suite loader会在当前的目录和php的include_path中根据PEAR的命名规则的类,一个叫做Project_Package_Class的类 会指向到文件Project/Package/Class.php

--repeat
Repeatedly runs the test(s) the specified number of times.
根据定义的数字重复运行测试

--tap
Reports the test progress using the Test Anything Protocol (TAP).
使用Test Anything Protocol格式报告测试进程

--testdox

Reports the test progress as agile documentation.
使用agile documentation格式报告测试进程

--colors

Use colors in output.
在输出结果中使用颜色

--stderr

Optionally print to STDERR instead of STDOUT.
使用STDERR替代STDOUT输出结果

--stop-on-error

Stop execution upon first error.
在遇到第一个错误时停止执行

--stop-on-failure

Stop execution upon first error or failure.
在遇到第一个失败时停止执行

--stop-on-skipped

Stop execution upon first skipped test.
在遇到第一个跳过的测试时停止执行

--stop-on-incomplete

Stop execution upon first incomplete test.
在遇到第一个未完成的测试时停止执行

--strict

Mark a test as incomplete if no assertions are made.
当一个测试没有定义任何断言时将其标记为未完成的测试

--verbose

Output more verbose information, for instance the names of tests that were incomplete or have been skipped.
输出例如未完成的测试的名字,跳过的测试的名字

--wait

Waits for a keystroke after each test. This is useful if you are running the tests in a window that stays open only as long as the test runner is active.
在每个测试开始之前等待用户按键,这个在你一个保持打开的窗口中运行很长的测试时很有帮助

--skeleton-class

Generates a skeleton class Unit (in Unit.php) from a test case class UnitTest (in UnitTest.php).
从一个测试类中生成一个概要测试类

--skeleton-test

Generates a skeleton test case class UnitTest (in UnitTest.php) for a class Unit (in Unit.php). See Chapter 17 for more details.
在Unit.php内为类Unit生成一个概要测试类UnitTest

--process-isolation

Run each test in a separate PHP process.
在多个php进程中运行所有测试

--no-globals-backup

Do not backup and restore $GLOBALS.
不备份和还原$GLOBALS变量

--static-backup

Backup and restore static attributes of user-defined classes.
备份和还原用户定义的类中的静态变量

--syntax-check

Enables the syntax check of test source files.
对测试的代码文件开启语法检查

--bootstrap

A "bootstrap" PHP file that is run before the tests.
定义测试前运行的bootstrap的php文件的路径

--configuration, -c

Read configuration from XML file. See Appendix C for more details.
从xml文件中读取配置,增加-c参数看更多的内容

If phpunit.xml or phpunit.xml.dist (in that order) exist in the current working directory and --configuration is not used, the configuration will be automatically read from that file.
如果phpunit.xml或phpunit.xml.dist(根据这个模式)在当前的目录中存在且--configuration参数没有使用的时候,配置信息会被自动读取

--no-configuration

Ignore phpunit.xml and phpunit.xml.dist from the current working directory.
自动跳过当前目录的phpunit.xml和phpunit.xml.dist配置文件

--include-path

Prepend PHP's include_path with given path(s).
在php的include_path增加路径

-d

Sets the value of the given PHP configuration option.
定义php的配置属性

--debug

Output debug information such as the name of a test when its execution starts.
输出调试信息如测试的名称及该测试什么时候开始执行

Note
提示
When the tested code contains PHP syntax errors, the TextUI test runner might exit without printing error information.
The standard test suite loader can optionally check the test suite sourcefile for PHP syntax errors, but not sourcefiles included by the test suite sourcefile.
当测试代码中含有php语法错误的时候,测试器会退出且不会打印任何错误信息,standard test suite loader可选择性检查测试文件源代码的PHP语法错误,但是不会检查测试文件中引入的其他的代码文件的语法错误

参kao:
https://phpunit.de/manual/3.7/en/code-coverage-analysis.html http://www.oschina.net/translate/php-unit-testing-with-phpunit

python覆盖率

Coverage3.3.1

背景
二进制程序可以使用BullseyeCoverage进行覆盖率分析,介绍一个针对python脚本的覆盖率分析工具。

Coverage3.3.1基本功能介绍
Coverage是一个用于统计python代码覆盖率的工具,支持HTML报告生成,最新版本支持对分支覆盖率进行统计
获取地址:http://pypi.python.org/pypi/coverage
官方站点:http://nedbatchelder.com/code/coverage/

安装
Coverage支持Python2.5/2.6/3.1,和其他python第三方module安装方式相同, 获获取后python setup.py install即可。

运行
Coverage支持两种运行方式:一种是命令行的方式,另一种方式是通过在脚本中调用Coverage的API来控制覆盖率的统计,下面一一介绍。

(1)命令行方式
这种方式比较简单,只需要指定被测脚本,不用对被测代码进行任何修改。
安装好Coverage后,会在python/bin/生成一个coverage脚本,通过调用coverage run <被测脚本>进行覆盖率分析。
Example:
coverage run test.py foo bar 其中run是coverage的参数,foo、bar是test.py的调用参数。
跑完后,会自动生成一个覆盖率统计结果文件(data file):.coverage。如果要修改这个默认的文件名也可以,只要设置COVERAGE_FILE环境变量

(2)API方式
除了命令行方式,coverage提供了更灵活的API调用方式来进行覆盖率分析。
API方式需要在被测代码前后嵌入,举一个最简单的例子:
from coverage import coverage
cov = coverage() #生成coverage对象
cov.start() #开始分析
function_to_be_tested() #待测函数
cov.stop() #分析结束
cov.save() #将覆盖率结果保存到数据文件

这样就通过在代码中调用API完成了对目标函数的覆盖率分析。
这种统计方式的好处在于可以灵活的控制覆盖率分析start和stop的代码段,可以满足不同的需求。
coverage的构造函数可以设置结果文件的名称等。有个函数容易弄错,就是use_cache,如果设置的use_cache(0),表示不在硬盘上读写结果文件。如果需要结果数据用来合并,一定要设置use_cache(1)。

查看覆盖率结果
通过(2)中介绍的方法完成覆盖率分析后,就可以查看覆盖率分析的结果了。Coverage提供了几种覆盖率结果的展现方式:
(1)简单文本方式
使用命令行coverage report,或者在代码中调用coverage对象的report方法可以获取简单文本格式的覆盖率结果:
Name Stmts Exec Branch BrExec Cover
--------------------------------------------------
Agent 261 101 104 29 35%
Async 497 247 114 39 46%
DNS 67 28 8 1 38%
FIFOLogMonitor 53 21 12 0 32%
MachineMonitor 108 81 28 10 66%
RPC 269 94 64 12 31%
Settings 30 22 4 3 73%
TaskCommon 69 24 24 0 25%
Thread 54 33 10 2 54%
WDBDLogMonitor 221 53 90 1 17%
common 323 77 108 9 19%
--------------------------------------------------
TOTAL 1952 781 566 106 35%

Name是被分析Module的名称
Stmts/Exec表示语句总数/执行到的语句数
Branch/BrExec表示分支数/执行到的分支数
Cover表示覆盖率Cover=(Exec+BrExec)/(Stmts+Branch)

(2)HTML格式
使用命令行coverage html,或者在代码中调用coverage对象的html_report方法,可以获取html格式的覆盖率分析报告,并且提供对源码详细覆盖情况的标注报告,报告范例可以在http://nedbatchelder.com/code/coverage/sample_html/查看。

(3)XML格式
类似的,通过coverage xml或者调用coverage.xml_report来生成。
--------------------------------------------------------------------------

总结:(1)命令行方式
运行:coverage run test.py foo bar
生成报告文档:
1)简单文本方式:coverage report
2)HTML格式:coverage html
3)XML格式:coverage xml 或者coverage.xml_report

用过代码覆盖率工具的都知道,多份结果的合并至关重要。执行合并操作很简单,只要把需要合并的覆盖率结果数据文件放在同一个目录里,然后执行:

[code=plain]coverage combine 合并
coverage report -m 直接终端显示报告
coverage html 生成html报告
coverage xml 生成xml报告
coverage erase 删除.coverage文件,保证不会影响下次统计的结果

其实对目录里的结果文件是有要求的,要求就是文件名的格式,需要合并的文件必须有同样的前缀,然后后面跟一个名称(通常是机器名),然后再跟一个数字(通常是进程ID),比如:

.coverage.CoderZh.1234
.coverage.Cnblogs.5678

为了方便执行结果的合并,我们在前面执行统计时,在run参数后面跟一个-p参数,会自动生成符合合并条件的结果文件。
$ coverage run -p my_program.py arg1 arg2
合并后,会再生成一个.coverage文件,然后再执行html查看合并后的报告吧。
其他几个erase annotate debug 参数就不介绍了。

coverage也提供一些很好用的函数,如:exclude(排除统计的代码)

参kao:
https://segmentfault.com/a/1190000003806169 https://segmentfault.com/a/1190000003805788 http://www.xuebuyuan.com/172617.html http://blog.chinaunix.net/uid-21998449-id-2681704.html http://www.cnblogs.com/coderzh/archive/2009/12/01/pythoncoverage.html

Go覆盖率

Go语言内置测试框架:内置的测试框架通过testing包以及go test命令来提供测试功能。
只要源码文件以_test.go结尾,就可以直接通过go test执行单元测试。同时还提供了代码测试覆盖率工具,可以很容易实施自动化测试。

golang测试用例使用原则:

文件名必须是_test.go结尾的,这样在执行go test的时候才会执行到相应的代码

你必须import testing这个包

所有的测试用例函数必须是Test开头

测试用例会按照源代码中写的顺序依次执行

测试函数TestXxx()的参数是testing.T,我们可以使用该类型来记录错误或者是测试状态

测试格式:func TestXxx (t *testing.T),Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如Testintdiv是错误的函数名。

函数中通过调用testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过,调用Log方法用来记录测试的信息。

如何运行测试用例?在项目目录下运行:go test

PASS

ok test/gotest 0.002s

显示详细测试通过和log信息:go test -v

=== RUN TestBubbleSort

--- PASS: TestBubbleSort (0.00s)

PASS

ok test/gotest 0.002s

查看测试覆盖率:go test -cover

$ go test -cover

PASS

coverage: 90.0% of statements

ok test/gotest 0.001s

1、测试Go代码

下面是一个完整的测试strings.Index函数的完整测试文件:

[code=plain]//strings_test.go
package strings_test

import (
"strings"
"testing"
)

func TestIndex(t *testing.T) {
const s, sep, want = "chicken", "ken", 4
got := strings.Index(s, sep)
if got != want {
t.Errorf("Index(%q,%q) = %v; want %v", s, sep, got, want)//注意原slide中的got和want写反了
}
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok command-line-arguments 0.007s

2、表驱动测试

Golang的struct字面值(struct literals)语法让我们可以轻松写出表驱动测试。

[code=plain]package strings_test

import (
"strings"
"testing"
)

func TestIndex(t *testing.T) {
var tests = []struct {
s   string
sep string
out int
}{
{"", "", 0},
{"", "a", -1},
{"fo", "foo", -1},
{"foo", "foo", 0},
{"oofofoofooo", "f", 2},
// etc
}
for _, test := range tests {
actual := strings.Index(test.s, test.sep)
if actual != test.out {
t.Errorf("Index(%q,%q) = %v; want %v",
test.s, test.sep, actual, test.out)
}
}
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok command-line-arguments 0.007s

3、T结构

*testing.T参数用于错误报告:

t.Errorf("got bar = %v, want %v", got, want)
t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)
t.Logf("iteration %v", i)

也可以用于enable并行测试(parallet test):
t.Parallel()

控制一个测试是否运行:

if runtime.GOARCH == "arm" {
t.Skip("this doesn't work on ARM")
}

4、运行测试

go test命令来运行特定包的测试。默认执行当前路径下包的测试代码。

$ go test
PASS

$ go test -v
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS

要运行工程下的所有测试,我们执行如下命令:

$ go test github.com/nf/…

标准库的测试:
$ go test std

注:假设strings_test.go的当前目录为testgo,在testgo目录下执行go test都是OK的。但如果我们切换到testgo的上一级目录执行go test,我们会得到什么结果呢?

$go test testgo
can't load package: package testgo: cannot find package "testgo" in any of:
/usr/local/go/src/pkg/testgo (from $GOROOT)
/Users/tony/Test/GoToolsProjects/src/testgo (from $GOPATH)

提示找不到testgo这个包,go test后面接着的应该是一个包名,go test会在GOROOT和GOPATH下查找这个包并执行包的测试。

5、测试覆盖率

go tool命令可以报告测试覆盖率统计。在testgo下执行go test -cover,结果如下:

go build _/Users/tony/Test/Go/testgo: no buildable Go source files in /Users/tony/Test/Go/testgo
FAIL _/Users/tony/Test/Go/testgo [build failed]

显然通过cover参数选项计算测试覆盖率不仅需要测试代码,还要有被测对象(一般是函数)的源码文件。

我们将目录切换到$GOROOT/src/pkg/strings下,执行go test -cover:

$go test -v -cover
=== RUN TestReader
— PASS: TestReader (0.00 seconds)
… …
=== RUN: ExampleTrimPrefix
— PASS: ExampleTrimPrefix (1.75us)
PASS
coverage: 96.9% of statements
ok strings 0.612s

go test可以生成覆盖率的profile文件,这个文件可以被go tool cover工具解析。

在$GOROOT/src/pkg/strings下面执行:

$ go test -coverprofile=cover.out

会在当前目录下生成cover.out文件。

查看cover.out文件,有两种方法:

a) cover -func=cover.out

$sudo go tool cover -func=cover.out
strings/reader.go:24: Len 66.7%
strings/reader.go:31: Read 100.0%
strings/reader.go:44: ReadAt 100.0%
strings/reader.go:59: ReadByte 100.0%
strings/reader.go:69: UnreadByte 100.0%
… …
strings/strings.go:638: Replace 100.0%
strings/strings.go:674: EqualFold 100.0%
total: (statements) 96.9%

b) 可视化查看

执行go tool cover -html=cover.out命令,会在/tmp目录下生成目录coverxxxxxxx,比如/tmp/cover404256298。目录下有一个 coverage.html文件。用浏览器打开coverage.html,即可以可视化的查看代码的测试覆盖情况。

关于go tool的cover命令,我的go version go1.3 darwin/amd64默认并不自带,需要通过go get下载。

$sudo GOPATH=/Users/tony/Test/GoToolsProjects go get code.google.com/p/go.tools/cmd/cover

下载后,cover安装在$GOROOT/pkg/tool/darwin_amd64下面。

二、高级测试技术

1、一个例子程序

outyet是一个web服务,用于宣告某个特定Go版本是否已经打标签发布了。其获取方法:

go get github.com/golang/example/outyet

注:
go get执行后,cd $GOPATH/src/github.com/golang/example/outyet下,执行go run main.go。然后用浏览器打开http://localhost:8080即可访问该Web服务了。

2、测试Http客户端和服务端

net/http/httptest包提供了许多帮助函数,用于测试那些发送或处理Http请求的代码。

3、httptest.Server

httptest.Server在本地回环网口的一个系统选择的端口上listen。它常用于端到端的HTTP测试。

type Server struct {
URL string // base URL of form http://ipaddr:port with no trailing slash
Listener net.Listener

// TLS is the optional TLS configuration, populated with a new config
// after TLS is started. If set on an unstarted server before StartTLS
// is called, existing fields are copied into the new config.
TLS *tls.Config

// Config may be changed after calling NewUnstartedServer and
// before Start or StartTLS.
Config *http.Server
}

func NewServer(handler http.Handler) *Server

func (*Server) Close() error

4、httptest.Server实战

下面代码创建了一个临时Http Server,返回简单的Hello应da:

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, client")
}))
defer ts.Close()

res, err := http.Get(ts.URL)
if err != nil {
log.Fatal(err)
}

greeting, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatal(err)
}

fmt.Printf("%s", greeting)

5、httptest.ResponseRecorder

httptest.ResponseRecorder是http.ResponseWriter的一个实现,用来记录变化,用在测试的后续检视中。

type ResponseRecorder struct {
Code int // the HTTP response code from WriteHeader
HeaderMap http.Header // the HTTP response headers
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
Flushed bool
}

6、httptest.ResponseRecorder实战

向一个HTTP handler中传入一个ResponseRecorder,通过它我们可以来检视生成的应da。

handler := func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "something failed", http.StatusInternalServerError)
}

req, err := http.NewRequest("GET", "http://example.com/foo", nil)
if err != nil {
log.Fatal(err)
}

w := httptest.NewRecorder()
handler(w, req)

fmt.Printf("%d – %s", w.Code, w.Body.String())

7、竞争检测(race detection)

当两个goroutine并发访问同一个变量,且至少一个goroutine对变量进行写操作时,就会发生数据竞争(data race)。

为了协助诊断这种bug,Go提供了一个内置的数据竞争检测工具。

通过传入-race选项,go tool就可以启动竞争检测。

$ go test -race mypkg // to test the package
$ go run -race mysrc.go // to run the source file
$ go build -race mycmd // to build the command
$ go install -race mypkg // to install the package

注:一个数据竞争检测的例子

例子代码:

//testrace.go

package main

import "fmt"
import "time"

func main() {
var i int = 0
go func() {
for {
i++
fmt.Println("subroutine: i = ", i)
time.Sleep(1 * time.Second)
}
}()

for {
i++
fmt.Println("mainroutine: i = ", i)
time.Sleep(1 * time.Second)
}
}

$go run -race testrace.go
mainroutine: i = 1
==================
WARNING: DATA RACE
Read by goroutine 5:
main.func·001()
/Users/tony/Test/Go/testrace.go:10 +0×49

Previous write by main goroutine:
main.main()
/Users/tony/Test/Go/testrace.go:17 +0xd5

Goroutine 5 (running) created at:
main.main()
/Users/tony/Test/Go/testrace.go:14 +0xaf
==================
subroutine: i = 2
mainroutine: i = 3
subroutine: i = 4
mainroutine: i = 5
subroutine: i = 6
mainroutine: i = 7
subroutine: i = 8

8、测试并发(testing with concurrency)

当测试并发代码时,总会有一种使用sleep的冲动。大多时间里,使用sleep既简单又有效。

但大多数时间不是”总是“。

我们可以使用Go的并发原语让那些奇怪不靠谱的sleep驱动的测试更加值得信赖。

9、使用静态分析工具vet查找错误

vet工具用于检测代码中程序员犯的常见错误:
– 错误的printf格式
– 错误的构建tag
– 在闭包中使用错误的range循环变量
– 无用的赋值操作
– 无法到达的代码
– 错误使用mutex
等等。

使用方法:
go vet [package]

10、从内部测试

golang中大多数测试代码都是被测试包的源码的一部分。这意味着测试代码可以访问包种未导出的符号以及内部逻辑。就像我们之前看到的那样。

注:比如$GOROOT/src/pkg/path/path_test.go与path.go都在path这个包下。

11、从外部测试

有些时候,你需要从被测包的外部对被测包进行测试,比如测试代码在package foo_test下,而不是在package foo下。

这样可以打破依赖循环,比如:

– testing包使用fmt
– fmt包的测试代码还必须导入testing包
– 于是,fmt包的测试代码放在fmt_test包下,这样既可以导入testing包,也可以同时导入fmt包。

12、Mocks和fakes

通过在代码中使用interface,Go可以避免使用mock和fake测试机制。

例如,如果你正在编写一个文件格式解析器,不要这样设计函数:

func Parser(f *os.File) error

作为替代,你可以编写一个接受interface类型的函数:

func Parser(r io.Reader) error

和bytes.Buffer、strings.Reader一样,*os.File也实现了io.Reader接口。

13、子进程测试

有些时候,你需要测试的是一个进程的行为,而不仅仅是一个函数。例如:

func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}

为了测试上面的代码,我们将测试程序本身作为一个子进程进行测试:

func TestCrasher(t *testing.T) {
if os.Getenv("BE_CRASHER") == "1" {
Crasher()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
cmd.Env = append(os.Environ(), "BE_CRASHER=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}

来自:http://tonybai.com/2014/10/22/golang-testing-techniques/

Java覆盖率

单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复改进重构之后的正确性。

一般来说,单元测试任务包括

接口功能测试:用来保证接口功能的正确性。

局部数据结构测试(不常用):用来保证接口中的数据结构是正确的

比如变量有无初始值

变量是否溢出

边界条件测试

变量没有赋值(即为NULL)

变量是数值(或字符)

主要边界:最小值,最大值,无穷大(对于DOUBLE等)

溢出边界(期望异常或拒绝服务):最小值-1,最大值+1

临近边界:最小值+1,最大值-1

变量是字符串

引用“字符变量”的边界

空字符串

对字符串长度应用“数值变量”的边界

变量是集合

空集合

对集合的大小应用“数值变量”的边界

调整次序:升序、降序

变量有规律

比如对于Math.sqrt,给出n^2-1,和n^2+1的边界

所有独立执行通路测试:保证每一条代码,每个分支都经过测试

代码覆盖率

语句覆盖:保证每一个语句都执行到了

判定覆盖(分支覆盖):保证每一个分支都执行到

条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)

路径覆盖:保证每一个路径都覆盖到

相关软件

Cobertura:语句覆盖

Emma: Eclipse插件Eclemma

各条错误处理通路测试:保证每一个异常都经过测试

JUNIT

JUnit是Java单元测试框架,已经在Eclipse中默认安装。目前主流的有JUnit3和JUnit4。
JUnit3中,测试用例需要继承
TestCase
类。
JUnit4中,测试用例无需继承
TestCase
类,只需要使用
@Test
等注解。

Junit3

先看一个Junit3的样例

// 测试java.lang.Math

// 必须继承TestCase

public class Junit3TestCase extends TestCase {

public Junit3TestCase() {

super();

}

// 传入测试用例名称

public Junit3TestCase(String name) {

super(name);

}

// 在每个Test运行之前运行

@Override

protected void setUp() throws Exception {

System.out.println("Set up");

}

// 测试方法。

// 方法名称必须以test开头,没有参数,无返回值,是公开的,可以抛出异常

// 也即类似public void testXXX() throws Exception {}

public void testMathPow() {

System.out.println("Test Math.pow");

Assert.assertEquals(4.0, Math.pow(2.0, 2.0));

}

public void testMathMin() {

System.out.println("Test Math.min");

Assert.assertEquals(2.0, Math.min(2.0, 4.0));

}

// 在每个Test运行之后运行

@Override

protected void tearDown() throws Exception {

System.out.println("Tear down");

}

}

如果采用默认的TestSuite,则测试方法必须是
public void testXXX() [throws Exception] {}
的形式,并且不能存在依赖关系,因为测试方法的调用顺序是不可预知的。
对于每个测试方法,调用的形式是:

testCase.setUp();

testCase.testXXX();

testCase.tearDown();

运行测试方法

在Eclipse中,可以直接在类名或测试方法上右击,在弹出的右击菜单中选择Run As -> JUnit Test。
在Mvn中,可以直接通过
mvn test
命令运行测试用例。
也可以通过Java方式调用,创建一个
TestCase
实例,然后重载
runTest()
方法,在其方法内调用测试方法(可以多个)。

TestCase test = new Junit3TestCase("mathPow") {

// 重载

protected void runTest() throws Throwable {

testMathPow();

};

};

test.run();

更加便捷地,可以在创建
TestCase
实例时直接传入测试方法名称,JUnit会自动调用此测试方法,如

TestCase test = new Junit3TestCase("testMathPow");

test.run();

Junit TestSuite

TestSuite是测试用例套件,能够运行过个测试方法。如果不指定TestSuite,会创建一个默认的TestSuite。默认TestSuite会扫描当前内中的所有测试方法,然后运行。
如果不想采用默认的TestSuite,则可以自定义TestSuite。在TestCase中,可以通过静态方法
suite()
返回自定义的suite。

import junit.framework.Assert;

import junit.framework.Test;

import junit.framework.TestCase;

import junit.framework.TestSuite;

public class Junit3TestCase extends TestCase {

public static Testsuite() {

System.out.println("create suite");

TestSuite suite = new TestSuite();

suite.addTest(new Junit3TestCase("testMathPow"));

return suite;

}

}

允许上述方法,控制台输出

create suite
Set up
Test Math.pow
Tear down

并且只运行了
testMathPow
测试方法,而没有运行
testMathMin
测试方法。通过显式指定测试方法,可以控制测试执行的顺序。

也可以通过Java的方式创建TestSuite,然后调用TestCase,如

// 先创建TestSuite,再添加测试方法

TestSuite testSuite = new TestSuite();

testSuite.addTest(new Junit3TestCase("testMathPow"));

// 或者 传入Class,TestSuite会扫描其中的测试方法。

TestSuite testSuite = new TestSuite(Junit3TestCase.class,Junit3TestCase2.class,Junit3TestCase3.class);

// 运行testSuite

TestResult testResult = new TestResult();

testSuite.run(testResult);

testResult中保存了很多测试数据,包括运行测试方法数目(
runCount
)等。

JUnit4

与JUnit3不同,JUnit4通过注解的方式来识别测试方法。目前支持的主要注解有:

@BeforeClass
全局只会执行一次,而且是第一个运行

@Before
在测试方法运行之前运行

@Test
测试方法

@After
在测试方法运行之后允许

@AfterClass
全局只会执行一次,而且是最后一个运行

@Ignore
忽略此方法

下面举一个样例:

import org.junit.After;

import org.junit.AfterClass;

import org.junit.Assert;

import org.junit.Before;

import org.junit.BeforeClass;

import org.junit.Ignore;

import org.junit.Test;

public class Junit4TestCase {

@BeforeClass

public static void setUpBeforeClass() {

System.out.println("Set up before class");

}

@Before

public void setUp() throws Exception {

System.out.println("Set up");

}

@Test

public void testMathPow() {

System.out.println("Test Math.pow");

Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);

}

@Test

public void testMathMin() {

System.out.println("Test Math.min");

Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);

}

// 期望此方法抛出NullPointerException异常

@Test(expected = NullPointerException.class)

public void testException() {

System.out.println("Test exception");

Object obj = null;

obj.toString();

}

@Ignore

@Test

public void testMathMax() {

Assert.fail("没有实现");

}

// 使用“假设”来忽略测试方法

@Test

public void testAssume(){

System.out.println("Test assume");

// 当假设失败时,则会停止运行,但这并不会意味测试方法失败。

Assume.assumeTrue(false);

Assert.fail("没有实现");

}

@After

public void tearDown() throws Exception {

System.out.println("Tear down");

}

@AfterClass

public static void tearDownAfterClass() {

System.out.println("Tear down After class");

}

}

如果细心的话,会发现Junit3的package是
junit.framework
,而Junit4是
org.junit
。执行输出:

Set up before class
Set up
Test Math.pow
Tear down
Set up
Test Math.min
Tear down
Set up
Test exception
Tear down
Set up
Test assume
Tear down
Tear down After class

可以看到,执行次序是
@BeforeClass
->
@Before
->
@Test
->
@After
->
@Before
->
@Test
->
@After
->
@AfterClass
@Ignore
会被忽略。

运行测试方法

与Junit3类似,可以在Eclipse中运行,也可以通过
mvn test
命令运行。

Assert

Junit3和Junit4都提供了一个Assert类(虽然package不同,但是大致差不多)。Assert类中定义了很多静态方法来进行断言。列表如下:

assertTrue(String message, boolean condition) 要求condition == true

assertFalse(String message, boolean condition) 要求condition == false

fail(String message) 必然失败,同样要求代码不可达

assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)

assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)

assertNotNull(String message, Object object) 要求object!=null

assertNull(String message, Object object) 要求object==null

assertSame(String message, Object expected, Object actual) 要求expected == actual

assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual

assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true

Mock/Stub

Mock和Stub是两种测试代码功能的方法。Mock测重于对功能的模拟。Stub测重于对功能的测试重现。比如对于List接口,Mock会直接对List进行模拟,而Stub会新建一个实现了List的TestList,在其中编写测试的代码。
强烈建议优先选择Mock方式,因为Mock方式下,模拟代码与测试代码放在一起,易读性好,而且扩展性、灵活性都比Stub好。
比较流行的Mock有:

JMock

EasyMock

Mockito

powermock

其中EasyMock和Mockito对于Java接口使用接口代理的方式来模拟,对于Java类使用继承的方式来模拟(也即会创建一个新的Class类)。Mockito支持spy方式,可以对实例进行模拟。但它们都不能对静态方法和final类进行模拟,powermock通过修改字节码来支持了此功能。

EasyMock

IBM上有几篇介绍EasyMock使用方法和原理的文章:EasyMock 使用方法与原理剖析使用 EasyMock 更轻松地进行测试
EasyMock把测试过程分为三步:录制、运行测试代码、验证期望。
录制过程大概就是:期望method(params)执行times次(默认一次),返回result(可选),抛出exception异常(可选)。
验证期望过程将会检查方法的调用次数。
一个简单的样例是:

@Test

public void testListInEasyMock() {

List list = EasyMock.createMock(List.class);

// 录制过程

// 期望方法list.set(0,1)执行2次,返回null,不抛出异常

expect1: EasyMock.expect(list.set(0, 1)).andReturn(null).times(2);

// 期望方法list.set(0,1)执行1次,返回null,不抛出异常

expect2: EasyMock.expect(list.set(0, 1)).andReturn(1);

// 执行测试代码

EasyMock.replay(list);

// 执行list.set(0,1),匹配expect1期望,会返回null

Assert.assertNull(list.set(0, 1));

// 执行list.set(0,1),匹配expect1(因为expect1期望执行此方法2次),会返回null

Assert.assertNull(list.set(0, 1));

// 执行list.set(0,1),匹配expect2,会返回1

Assert.assertEquals(1, list.set(0, 1));

// 验证期望

EasyMock.verify(list);

}

EasyMock还支持严格的检查,要求执行的方法次序与期望的完全一致。

Mockito

Mockito是Google Code上的一个开源项目,Api相对于EasyMock更好友好。与EasyMock不同的是,Mockito没有录制过程,只需要在“运行测试代码”之前对接口进行Stub,也即设置方法的返回值或抛出的异常,然后直接运行测试代码,运行期间调用Mock的方法,会返回预先设置的返回值或抛出异常,最后再对测试代码进行验证。可以查看此文章了解两者的不同。
官方提供了很多样例,基本上包括了所有功能,可以去看看
这里从官方样例中摘录几个典型的:

验证调用行为

import static org.mockito.Mockito.*;

//创建Mock

List mockedList = mock(List.class);

//使用Mock对象

mockedList.add("one");

mockedList.clear();

//验证行为

verify(mockedList).add("one");

verify(mockedList).clear();

对Mock对象进行Stub

//也可以Mock具体的类,而不仅仅是接口

LinkedList mockedList = mock(LinkedList.class);

//Stub

when(mockedList.get(0)).thenReturn("first"); // 设置返回值

when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出异常

//第一个会打印 "first"

System.out.println(mockedList.get(0));

//接下来会抛出runtime异常

System.out.println(mockedList.get(1));

//接下来会打印"null",这是因为没有stub get(999)

System.out.println(mockedList.get(999));

// 可以选择性地验证行为,比如只关心是否调用过get(0),而不关心是否调用过get(1)

verify(mockedList).get(0);

代码覆盖率

比较流行的工具是Emma和Jacoco,Ecliplse插件有eclemma。eclemma2.0之前采用的是Emma,之后采用的是Jacoco。这里主要介绍一下Jacoco。Eclmama由于是Eclipse插件,所以非常易用,就不多做介绍了。

Jacoco

Jacoco可以嵌入到Ant、Maven中,也可以使用Java Agent技术监控任意Java程序,也可以使用Java Api来定制功能。
Jacoco会监控JVM中的调用,生成监控结果(默认保存在jacoco.exec文件中),然后分析此结果,配合源代码生成覆盖率报告。需要注意的是:监控和分析这两步,必须使用相同的Class文件,否则由于Class不同,而无法定位到具体的方法,导致覆盖率均为0%。

Java Agent嵌入
首先,需要下载jacocoagent.jar文件,然后在Java程序启动参数后面加上
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
,具体的options可以在此页面找到。默认会在JVM关闭时(注意不能是
kill -9
),输出监控结果到jacoco.exec文件中,也可以通过socket来实时地输出监控报告(可以在Example代码中找到简单实现)。

Java Report
可以使用Ant、Mvn或Eclipse来分析jacoco.exec文件,也可以通过API来分析。

public void createReport() throws Exception {

// 读取监控结果

final FileInputStream fis = new FileInputStream(new File("jacoco.exec"));

final ExecutionDataReader executionDataReader = new ExecutionDataReader(fis);

// 执行数据信息

ExecutionDataStore executionDataStore = new ExecutionDataStore();

// 会话信息

SessionInfoStore sessionInfoStore = new SessionInfoStore();

executionDataReader.setExecutionDataVisitor(executionDataStore);

executionDataReader.setSessionInfoVisitor(sessionInfoStore);

while (executionDataReader.read()) {

}

fis.close();

// 分析结构

final CoverageBuilder coverageBuilder = new CoverageBuilder();

final Analyzer analyzer = new Analyzer(executionDataStore, coverageBuilder);

// 传入监控时的Class文件目录,注意必须与监控时的一样

File classesDirectory = new File("classes");

analyzer.analyzeAll(classesDirectory);

IBundleCoverage bundleCoverage = coverageBuilder.getBundle("Title");

// 输出报告

File reportDirectory = new File("report"); // 报告所在的目录

final HTMLFormatter htmlFormatter = new HTMLFormatter(); // HTML格式

final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory));

// 必须先调用visitInfo

visitor.visitInfo(sessionInfoStore.getInfos(), executionDataStore.getContents());

File sourceDirectory = new File("src"); // 源代码目录

// 遍历所有的源代码

// 如果不执行此过程,则在报告中只能看到方法名,但是无法查看具体的覆盖(因为没有源代码页面)

visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4));

// 执行完毕

visitor.visitEnd();

}

Maven单元测试报告及测试覆盖率

对junit单元[b]测试的报告:类似这样的结果[/b]

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.cn.qin.actionTest.UserActionTest
sdffsdfsdf
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.061 sec - in com.cn.qin.actionTest.UserActionTest

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] <<< cobertura-maven-plugin:2.5.1:cobertura (cobertura-report) < [cobertura]test @ Struts <<<
[INFO]
[INFO] --- cobertura-maven-plugin:2.5.1:cobertura (cobertura-report) @ Struts ---
[INFO] Cobertura 1.9.4.1 - GNU GPL License (NO WARRANTY) - See COPYRIGHT file
Cobertura: Loaded information on 3 classes.
Report time: 298ms

运行报告是junit自己的报告输出,和咱们在Eclipse运行的报告差不多。以上代表运行了3个用例,和预期效果不符的是0个,失败的用例是0个,忽略的用例数是0个。 如果需要跳过单元测试,则可以运行如下命令

1. mvn package -DskipTests

  大家可能要问,为何Maven能够自己寻找我们编写的测试类呢?其实还是那句约定大于配置。Maven自动去寻找src/test/java下面的类,当此文件夹下面的类符合以下规范,那么Maven默认认为他们是单元测试用例类。

  Test*.java:任何目录下以Test为开始的类
  *Test.java: 任何目录下以Test为结尾的类
  *TestCase.java: 任何目录下以TestCase为结尾的类。

如果想在一段时间内节省项目构建时间,暂时全部忽略单元测试。那么可以在pom.xml中配置如下:

[code=plain]<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>

等到项目完全开发完了,需要测试用例的时候将其注释掉即可。

本个模块有两个测试用例类,如果仅仅想运行一个测试用例该怎么办。
运行下面命令: test -Dtest=AccountImageServiceImplTest 这个是指定具体运行哪个测试用例。
当然需要将pom文件中忽略测试用例的配置注释掉。

也可以测试多个测试用例: mvn test -Dtest=AccountImageServiceImplTest,AccountImageUtilTest
也可以使用模糊匹配进行测试:mvn test -Dtest=*Test

[code=plain]<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
<excludes>
<exclude>**/AccountImageUtilTest.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

其中includes是需要测试的类,excludes是要排除之外测试用例。可以使用模糊匹配。**用来匹配任意件路经,*匹配任意类。
Junit的单元测试报告的pom.xml配置

[code=plain]<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.12.2</version>
<configuration>
<showSuccess>false</showSuccess>
</configuration>
</plugin>
</plugins>
</build>

这个默认生成的报告是txt,要生成html的报告需要使用命令mvn surefire-report:report. 这会在target/site下面生成html的报告, 后来经测试发现,

其实maven-surefire-plugin就已经可以生成txt和xml的测试结果,如果要html的报告才需要maven-surefire-report-plugin

4. 测试报告

基本的测试报告上面已经介绍过了,下面我们看看测试覆盖率的报告。运行如下命令:mvn cobertura:cobertura 其pom.xml配置

[code=plain]<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.5.1</version>
</plugin>
</plugins>
</build>

常用命令

  mvn cobertura:help 查看cobertura插件的帮助
  mvn cobertura:clean 清空cobertura插件运行结果
  mvn cobertura:check 运行cobertura的检查任务
  mvn cobertura:cobertura 运行cobertura的检查任务并生成报表,报表生成在target/site/cobertura目录下
  cobertura:dump-datafile Cobertura Datafile Dump Mojo
  mvn cobertura:instrument Instrument the compiled classes
  在target文件夹下出现了一个site目录,下面是一个静态站点,里面就是单元测试的覆盖率报告。
  详细配置还可参kao:http://zhanshenny.iteye.com/blog/1440571

5. 总结

这次我们介绍了Maven的测试,可以运行项目的单元测试用例,并生成报告。使用者可以根据自己的需要配置测试选项以满足项目的测试需求。

我自己测试的pom.xml配置插件

[code=plain]<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<!--设置包含的测试类 -->
<includes>
<include>******</include>
<include>*/User*</include>
</includes>
<!-- 设置不进行测试类 -->
<excludes>
<exclude>Test*</exclude>
</excludes>
<!-- 跳过测试阶段,測試類写的有问也会出错,一般不推荐 -->
<!--<skip>true</skip> -->
</configuration>
</plugin>

<!-- 构建项目站点报告插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.0-beta-3</version>
<configuration>
<!-- 配置站点国际化 -->
<locales>zh_CN</locales>
<!-- 输出编码 -->
<outputEncoding>GBK</outputEncoding>
</configuration>
</plugin>

<!-- 项目API doc报告 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.7</version>
</plugin>

<!-- 单元测试报告html -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.12.2</version>
<configuration>
<showSuccess>false</showSuccess>
</configuration>
</plugin>

<!-- 测试覆盖率的报告 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<formats>
<format>html</format>
<format>xml</format>
</formats>
</configuration>
<executions>
<execution>
<id>cobertura-report</id>
<goals>
<goal>cobertura</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>

</plugins>
</build>

生成的测试覆盖率会在你自己项目的target/site/cobertura/下面 我的路径是 file:///D:/InstallSoft/V2/workspace/user-parent/Struts/target/site/cobertura/index.html

参kao:
http://terrencexu.iteye.com/blog/718834 http://aofengblog.blog.163.com/blog/static/6317021201312734058296/ http://aofengblog.blog.163.com/blog/static/6317021201312763630878/ http://www.ibm.com/developerworks/cn/java/j-lo-cobertura/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: