目录
1️⃣ account-captcha
1.1 account-captcha
1.2 account-captcha 的主代码
1.3 account-captcha的测试代码
2️⃣ maven-surefire-plugin 简介
3️⃣ 跳过测试
4️⃣ 动态指定要运行的测试用例
5️⃣ 包含与排除测试用例
6️⃣ 测试报告
6.1基本的测试报告
6.2 测试覆盖率报告
7️⃣ 运行 TestNG 测试
8️⃣ 重用测试代码
🌾 总结
随着敏捷开发模式的日益流行,软件开发人员也越来越认识到日常编程工作中单元测试的重要性。Maven的重要职责之一就是自动运行单元测试,它通过 maven-surefire-plugin与主流的单元测试框架JUnit3、JUnit4以及TestNG集成,并且能够自动生成丰富的结果报告。本文将介绍Maven关于测试的一些重要特性,但不会深入解释单元测试框架本身及相关技巧,重点是介绍如何通过Maven控制单元测试的运行。
除了测试之外,本文还会进一步丰富账户注册服务这一背景案例,引入其第3个模块:account-captcha。
1️⃣ account-captcha
在讨论maven-surefire-plugin之前,本文先介绍实现账户注册服务的 account-captcha模块,该模块负责处理账户注册时验证码的key生成、图片生成以及验证等。可以回顾前面的文章的背景案例以获得更具体的需求信息。
1.1 account-captcha
该模块的POM(Project Object Model,项目对象模型)还是比较简单的,内容见代码:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http:/www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/apache.org/maven-v4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.xiaoshan.mvnbook.account</groupId> <artifactId>account-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent><artifactId>account-captcha</artifactId><name>Account Captcha</name><properties><kaptcha.version>2.3</kaptcha.version></properties><dependencies><dependency><groupId>com.google.code.kaptcha</groupId> <artifactId>kaptcha</artifactId><version>${kaptcha.version}</version><classifier>jdk15</classifier></dependency><dependency><groupId>org.springframework/groupId><artifactId>spring-core</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-bean</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactid></dependency></dependencies><repositories><repository> <id>sonatype-forge</id><name>Sonatype Forge</name><url>http:/repository.sonatype.org/content/groups/forge/</url><releases><enabled>true</enabled></releases><snapshots><enabled>false</enabled></snapshots></repository></repositories>
</project>
首先POM中的第一部分是父模块声明,如同 account-email、account-persist 一样,这里将父模块声明为 account-parent。紧接着是该项目本身的 artifactld和名称,groupld 和version没有声明,将自动继承自父模块。再往下声明了一个Maven属性 kaptcha.version, 该属性用在依赖声明中,account-captcha的依赖除了Spring Framework和 JUnit之外,还有一个 com.google.code.kaptcha:kaptcha。Kaptcha是一个用来生成验证码(Captcha)的开源类库,
account-captcha将用它来生成注册账户时所需要的验证码图片,如果想要了解更多关于Kaptcha的信息,可以访问其项目主页:http://code.gpoogle.com/p/kaptcha/。
POM中SpringFramework和JUnit的依赖配置都继承自父模块,这里不再赘述。Kaptcha依赖声明中version使用了Maven属性,这在之前也已经见过。需要注意的是,Kaptcha依赖还有一个 classifier元素,其值为 jdk5, Kaptcha针对Java1.5和Java1.4提供了不同的分发包,因此这里使用classifier来区分两个不同的构件。
POM的最后声明了Sonatype Forge这一公共仓库,这是因为 Kaptcha并没有上传的中央仓库,我们可以从Sonatype Forge仓库获得该构件。如果有自己的私服,就不需要在POM中声明该仓库了,可以代理Sonatype Forge仓库,或者直接将Kaptcha上传到自己的仓库中。
最后,不能忘记把account-captcha加入到聚合模块(也是父模块)account-parent中,见代码:
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http:/maven.apache.org/maven-v4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.xiaoshan.mvnbook.account</groupId><artifactId>account-parent</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Parent</name><modules><module>account-email</module><module>account-persist</module><module>account-captcha</module></modules>
</project>
1.2 account-captcha 的主代码
account-caplcha需要提供的服务是生成随机的验证码主键,然后用户可以使用这个主键要求服务生成一个验证码图片,这个图片对应的值应该是随机的,最后用户用肉眼读取图片的值,并将验证码的主键与这个值交给服务进行验证。这一服务对应的接口可以定义,如代码所示。
package com.xiaosahn.mvnbook.account.captcha;
import java.util.List;public interface AccountCaptchaService{String generateCaptchakey() throws AccountCaptchaException;byte[] generatecaptchaImage(String captchaKey) throws AccountCaptchaException; boolean validateCaptcha(String captchaKey, String captchaValue) throws AccountCaptchaException;List<String> getPreDefinedTexts();void setPreDefinedTexts(List<String> preDefinedTexts);
}
很显然,generateCaptchaKey() 用来生成随机的验证码主键,generateCaptchalmage() 用来生成验证码图片,而validateCaptcha()用来验证用户反馈的主键和值。
该接口定义了额外的 getPreDefinedTexts()和 setPreDefinedTexts()方法,通过这一组方法,用户可以预定义验证码图片的内容,同时也提高了可测试性。如果AccountCaptchaService永远生成随机的验证码图片,那么没有人工的参与就很难测试该功能。现在,服务允许传入一个文本列表,这样就可以基于这些文本生成验证码,那么我们也就能控制验证码图片的内容了。
为了能够生成随机的验证码主键,引入一个RandomGenerator类,见代码:
package com.xiaoshan.mvnbook.account.captcha;import java.util.Random;public class RandomGenerator{private static String range="0123456789abcdefghijklmnopqrstuvwxyz";public static synchronized String getRandomString(){Random random = new Random();StringBuffer result = new StringBuffer();for(int i=0;i<8;i++){result.append(range.charAt(random.nextInt(range.Length())));}return result.toString();}
}
RandomGenerator类提供了一个静态且线程安全的 getRandomString()方法。该方法生成一个长度为8的字符串,每个字符都是随机地从所有数字和字母中挑选,这里主要是使用了java.util.Random类,其 nextlnt(int n)方法会返回一个大于等于0且小于n的整数。代码中的字段 range包含了所有的数字与字母,将其长度传给 nextInt()方法后就能获得一个随机的下标,再调用range.charAt()就可以随机取得一个其包含的字符了。
现在看AccountCaptchaService的实现类AccountCaptchaServicelmpl。首先需要初始化验证码图片生成器,见代码:
package com.xiaoshan.mvnbook.account.captcha;import java.awt.image.BufferedImage;
import java.io.ByteArrayoutputStream;
import java.io.IOException;
import java.util.HashMap;import java.util.List;
import java.util.Map;
import java.util.Properties;import javax.imageio.ImageIo;import org.springframework.beans.factory.InitializingBean;
import com.google.code.kaptcha.impl.DefauitKaptcha;
import com.google.code.kaptcha.uti1.config;public class AccountCaptchaServiceImpl implements AccountCaptchaService, InitializingBean {private DefaultKaptcha producer;public void afterPropertiesSet() throws Exception {producer = new DefaultKaptcha();producer.setConfig(new Config(new Properties()));}...
}
AccountCaptchaServiceImpl 实现了Spring Framework的 InitializingBean接口,该接口定义了一个方法 afterPropertiesSet(), 该方法会被Spring Framework初始化对象的时候调用。该代码清单中使用该方法初始化验证码生成器 producer,并且为 producer提供了默认的配置。接着AccountCaptehaServicelmpl需要实现 generateCaptchaKey()方法,见代码:
private Map<String, String> captchaMap = new HashMap<String, String>();private List<String> preDefinedTexts;private int textCount=0;public String generateCaptchaKey()
{String key=RandomGenerator.getRandomstring();String value=getCaptchaText();captchaNap.put(key,value);return key;
}public List<String> getPreDefinedTexts()
{return preDefinedTexts;
}public void setPreDefinedTexts(List<String> preDefinedTexts)
{this.preDefinedTexts=preDefinedTexts;
}private String getCaptchaText()
{if(preDefinedTexts!=null&&!preDefinedTexts.isEmpty()){String text=preDefinedTexts.get(textCount);textCount=(textCount+1)%preDefinedTexts.size();return text;}else{return producer.createText();}
}
上述代码清单中的 generateCaptchaKey()首先生成一个随机的验证码主键,每个主键将和一个验证码字符串相关联,然后这组关联会被存储到 captchaMap中以备将来验证。主键的目的仅仅是标识验证码图片,其本身没有实际的意义。代码清单中的 getCaptchaText()用来生成验证码字符串,当 preDefinedTexts不存在或者为空的时候,就是用验证码图片生成器 pruducer创建一个随机的字符串,当 preDefinedTexts不为空的时候,就顺序地循环该字符串列表读取值。preDefinedTexts有其对应的一组get和set方法,这样就能让用户预定义验证码字符串的值。
有了验证码图片的主键,AccountCaptchaServicelmpl就需要实现 generateCaptchalmage()方法来生成验证码图片,见代码:
public byte[] generateCaptchaImage(String captchakey) throws AccountCaptchaException
{String text = captchaMap.get(captchakey);if(text == null){throw new AccountCaptchaException("Captch key'"+ captchakey +"'not found!");}BufferedImage image = producer.createImage(text);ByteArrayOutputStream out = new ByteArrayOutputStream();try{ImageIO.write(image,"jpg",out);}catch(IOException e){throw new AccountCaptchaException("Failed to write captcha stream!",e);}return out.toByteArray();
}
为了生成验证码图片,就必须先得到验证码字符串的值,代码清单中通过使用主键来查询captchaMap获得该值,如果值不存在,就抛出异常。有了验证码字符串的值之后,generateCaptchalmage()方法就能通过 producer来生成一个 BufferedImage, 随后的代码将这个图片对象转换成 jpg格式的字节数组并返回。有了该字节数组,用户就能随意地将其保存成文件,或者在网页上显示。
最后是简单的验证过程,见代码:
public boolean validateCaptcha(String captchaKey, String captchaValue) throws AccountCaptchaException {String text = captchaMap.get(captchakey);if(text == null){throw new AccountCaptchaException("Captch key'"+ captchaKey +"not
found!");}if(text.equals(captchaValue)){captchaMap.remove(captchaKey);return true;}else{return false;}
}
用户得到了验证码图片以及主键后,就会识别图片中所包含的字符串信息,然后将此验证码的值与主键一起反馈给 validateCaptcha()方法以进行验证。validateCaptcha()通过主键找到正确的验证码值,然后与用户提供的值进行比对,如果成功,则返回true。
当然,还需要一个Spring Framework的配置文件,它在资源目录 src/main/resources/下,名为account-captcha.xml,见代码:
<?xml version="1.0"encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd"><bean id="accountCaptchaService"class="com.xiaoshan.mvmbook.account.captcha.AccountCaptchaServiceImpl"></bean>
</beans>
这是一个最简单的Spring Framework配置,它定义了一个id为accountCaptchaService的 bean,其实现为刚才讨论的AccountCaptchaServicelmpl。
1.3 account-captcha的测试代码
测试代码位于src/test/java日录,其包名也与主代码一致,为com.xiaoshan.mvnbook.accountcaptcha。首先看一下简单的RandomeGeneratorTest,见代码:
package com.xiaoshan.mvnbook.account.captcha;import static org.junit.Assert.assertFalse;
import java.util.Hashset;
import java.util.Set;
import org.junit.Test;public class RandomGeneratorTest
{@Test public void testGetRandomString() throws Exception{Set<String> randoms = new HashSet<String>(100);for(int i=0; i<100; i++){String random = RandomGenerator.getRandomString();aasertFalse(randoms.cantains(random));randoms.add(random);}}
}
该测试用例创建一个初始容量为100的集合randoms,然后循环100次用RandomGenerator生成随机字符串并放入randoms中,同时每次循环都检查新生成的随机值是否已经包含在集合中。这样一个简单的检查能基本确定RandomGenerator生成值是否为随机的。
当然这个模块中最重要的测试应该在AccounCaptchaService上,见代码:
package com.xiaoshan.mvnbook.account.captcha;
import static org.junit.Assert.*;
import java.io.File;
import java.io.FileoutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.apringframework.context.support.ClassPathxmlapplicationcontext;public class AccountCaptchaServiceTest
{private AcccuntCaptchaService service;@Beforepublic void prepare() throws Exception{ApplicatlonContext ctx = new ClassPathXmlApplicationContext("accounc-captcha.xml");service = (AccountCaptchaservice) ctx.getBean("accountCaptchaservice");}@Testpublic void testGenerateCaptcha() throws Exception{String captchaKey = service.generaceCaptchaKey();assertNotNull(captchakey);byte[] captchaImage = service.generateCaptchaImage(captchakey);assertTrue(captchaImage.Length >0);File image = new File("target/"+ captchakey + ".jpg");OutputStream output = null;try{output = new FileOutputStream(image);output.write(captchaImage);}finally{if(output != null){output.close();}}assertTrue(image.exists() && image.length() > 0);}@Testpublic void testValidatecaptchacorrect() throws Exception{List<String> preDefinedTexts = new ArrayList<String>();preDefinedTexts.add("12345");preDefinedTexts.add("abcde"); service.setPreDefinedTexts(preDefinedTexts);String captchaKey = service.generatecaptchakey();service.generateCaptchaImage(captchaKey);assertTrue(service.validateCaptcha(captchaKey,"12345"));captchaKey = service.generateCaptchaKey();service.generateCaptchaImage(captchaKey):assertTrue(service.validateCaptcha(captchaKey,"abcde"));}@Testpublic void testValidateCaptchaIncorrect() throws Exception{List<String> preDefinedTexts = new ArrayList<String>();preDefinedTexts.add("12345");service.setPreDefinedTexts(preDefinedTexts);String captchaKey = service.generateCaptchakey();service.generateCaptchaImage(captchaKey);assertFalse(service.validatecaptcha(captchaKey,"67890"));}
}
该测试类的prepare()方法使用@Before标注,在运行每个测试方法之前初始化AccountCaptchaService这个bean。
testGenerateCaptcha()用来测试验证码图片的生成。首先它获取一个验证码主键并检查其非空,然后使用该主键获得验证码图片,实际上是一个字节数组,并检查该字节数组的内容非空。紧接着该测试方法在项目的target目录下创建一个名为验证码主键的jpg格式文件,并将AccountCaptchaService返回的验证码图片字节数组内容写入到该jpg文件中,然后再检查文件存在且包含实际内容。运行该测试之后,就能在项目的target目录下找到一个名如dhb022fc.jpg的文件,打开是一个验证码图片。
testValidateCaptchaCorrect()用来测试一个正确的Captcha验证流程。它首先预定义了两个Captcha的值放到服务中,然后依次生成验证码主键、验证码图片,并且使用主键和已知的值进行验证,确保服务正常工作。
最后的testValidateCaptchalncorrect()方法测试当用户反馈的Captcha值错误时发生的情景,它先预定义Captcha的值为“12345”,但最后验证是传入了“67890”,并检查validateCaptcha()方法返回的值为false。
现在运行测试,在项目目录下运行mvn test,就会得到如下输出:
[INFO] Scanning for projects...
[INFO]
[INFO]
[INFO] Building Account Captcha 1.0.0-SNAPSHOT
[INFO]
[INFO].
[INFO]
[INFO]---maven-surefire-plugin:2.4.3:test(defauit-test)@account-captcha---
[INFO] surefire report directory:D:\code\ch-10\account-aggregator account-captcha\target\surefire-reports
TESTS
Running com.xiaoshan.mvnbook.account.captcha.RandomGeneratorTest
Tests run:1, Failures:0, Errors:0, Skipped:0, Time elapsed:0.037 sec
Running com.xiaoshan.mvnbook.account.captcha.AccountCaptchaServiceTest
Tests run:3, Failures:0, Errors:0, Skipped:0, Time elapsed:1.016 sec
Results;
Tests run:4, Failures:0, Errors:0, Skipped:0
[INFO]-----
[INFO] BUILD SUCCESS
[INFO]
这个简单的报告告诉我们,Maven运行了两个测试类,其中第一个测试类 RandomGeneratorTest包含1个测试,第二个测试类 AccountCaptchaServiceTest包含3个测试,所有4个测试运行完毕后,没有任何失败和错误,也没有跳过任何测试。
报告中的Failures、Errors、Skipped信息来源于JUnit测试框架。Failures(失败) 表示要测试的结果与预期值不一致,例如测试代码期望返回值为true, 但实际为false; Errors(错误)表示测试代码或产品代码发生了未预期的错误,例如产品代码抛出了一个空指针错误,该错误又没有被测试代码捕捉到;Skipped表示那些被标记为忽略的测试方法,在JUnit中用户可以使用@Ignore注解标记忽略测试方法。
2️⃣ maven-surefire-plugin 简介
Maven本身并不是一个单元测试框架,Java世界中主流的单元测试框架为 JUnit(http://www.junit.org/)和 TestNG(http://testng.org/)。Maven所做的只是在构建执行到特定生命周期阶段的时候,通过插件来执行JUnit或者TestNG的测试用例。这一插件就是maven-surefire-plugin,可以称之为测试运行器(TestRuner), 它能很好地兼容JUnit3、JUnit4以及TestNG。
可以回顾一下前面介绍过的default生命周期,其中的 test阶段被定义为“使用单元测试框架运行测试”。我们知道,生命周期阶段需要绑定到某个插件的目标才能完成真正的工作,test阶段正是与maven-surefire-plugin的test目标相绑定了,这是一个内置的绑定,具体可参考前面的文章。
在默认情况下,maven-surefire-plugin的test目标会自动执行测试源码路径(默认为src/test/java/)下所有符合一组命名模式的测试类。这组模式为:
- **/Test *.java:任何子目录下所有命名以Test开头的Java类。
- **/* Test.java:任何子目录下所有命名以Test结尾的Java类。
- **/* TestCase.java:任何子目录下所有命名以TestCase结尾的Java类。
只要将测试类按上述模式命名,Maven就能自动运行它们,用户也就不再需要定义测试集合(TestSuite)来聚合测试用例(TestCase)。关于模式需要注意的是,以Tests结尾的测试类是不会得以自动执行的。
当然,如果有需要,可以自己定义要运行测试类的模式,这一点将在下文中详细描述。此外,maven-surefire-plugin还支持更高级的TestNG测试集合xml文件,这一点也将在下文中详述。
当然,为了能够运行测试,Maven需要在项目中引入测试框架的依赖,我们已经多次涉及了如何添加JUnit测试范围依赖,这里不再赘述,而关于如何引入TestNG依赖,可参看下文第7节。
3️⃣ 跳过测试
日常工作中,软件开发人员总有很多理由来跳过单元测试,“我敢保证这次改动不会导致任何测试失败", "测试运行太耗时了,暂时跳过一下", “有持续集成服务跑所有测试呢,我本地就不执行啦”。在大部分情况下,这些想法都是不对的,任何改动都要交给测试去验证,测试运行耗时过长应该考虑优化测试,更不要完全依赖持续集成服务来报告错误,测试错误应该尽早在尽小范围内发现,并及时修复。
不管怎样,我们总会要求Maven跳过测试,这很简单,在命令行加入参数skipTests就可以了。例如:
$mvn package -D skipTests
Maven输出会告诉你它跳过了测试:
[INFO]---maven-compller-plugin:2.0.2:testCompile(default-testCompile)@ac-
count-captcha---
[INFO]Compiling 2 source files to D:\\code\ch-10\account-aggregator\account-
captcha\target\test-classes
[INFO]
[INFO]---maven-surefire-plugin:2.4.3:test(default-test)@account-captcha---
[INPO]Tests are skipped.
当然,也可以在POM中配置 maven-surefire-plugin插件来提供该属性,如代码所示。但这是不推荐的做法,如果配置POM让项目长时间地跳过测试,则还要测试代码做什么呢?
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.5</version><configuration><skipTests>true</skipTests></configuration>
</plugin>
有时候用户不仅仅想跳过测试运行,还想临时性地跳过测试代码的编译,Maven也允许你这么做,但记住这是不推荐的:
$mvn package -D maven.test.skip=true
这时Maven的输出如下:
[INFO]---maven-compiler-plugin:2.0.2:testCompile(default-testcompile)@account-captcha---
[INFO]Not compiling test sources
[INFO]
[INFO]---maven-surefire-plugin:2.4.3:test(default-test)@account-captcha---
[INFO]Tests are skipped.
参数maven.test.skip同时控制了maven-compiler-plugin和 maven-surefire-plugin两个插件的行为,测试代码编译跳过了,测试运行也跳过了。
对应于命令行参数maven.test.skip的POM配置如代码所示,但这种方法也是不推荐使用的。
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>2.1</version><configuration><skip>true</skip></configuration>
</plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.5</version><configuration><skip>true</skip></configuration>
</plugin>
实际上 maven-compiler-plugin的 testCompile目标和 maven-surefire-plugin的 test目标都提供了一个参数skip用来跳过测试编译和测试运行,而这个参数对应的命令行表达式为maven.test.skip。
4️⃣ 动态指定要运行的测试用例
反复运行单个测试用例是日常开发中很常见的行为。例如,项目代码中有一个失败的测试用例,开发人员就会想要再次运行这个测试以获得详细的错误报告,在修复该测试的过程中,开发人员也会反复运行它,以确认修复代码是正确的。如果仅仅为了一个失败的测试用例而反复运行所有测试,未免太浪费时间了,当项目中测试的数目比较大的时候,这种浪费尤为明显。
maven-surefire-plugin提供了一个test参数让Maven用户能够在命令行指定要运行的测试用例。例如,如果只想运行account-captcha的RandomGeneratorTest,就可以使用如下命令:
$mvn test-Dtest=RandomGeneratorTest
这里test参数的值是测试用例的类名,这行命令的效果就是只有RandomGeneratorTest这一个测试类得到运行。
maven-surefire-plugin的test参数还支持高级一些的赋值方式,能让用户更灵活地指定需要运行的测试用例。例如:
$mvn test -Dtest=Random*Test
星号可以匹配零个或多个字符,上述命令会运行项目中所有类名以Random开头、Test 结尾的测试类。
除了星号匹配,还可以使用逗号指定多个测试用例:
$mvn test -Dtest=RandomGeneratorTest,AccountCaptchaServiceTest
该命令的test参数值是两个测试类名,它们之间用逗号隔开,其效果就是告诉Maven只运行这两个测试类。
当然,也可以结合使用星号和逗号。例如:
$mvn test -Dtest=Random*Test,AccountCaptchaServiceTest
需要注意的是,上述几种从命令行动态指定测试类的方法都应该只是临时使用,如果长时间只运行项目的某几个测试,那么测试就会慢慢失去其本来的意义。
test参数的值必须匹配一个或者多个测试类,如果maven-surefire-plugin找不到任何匹配的测试类,就会报错并导致构建失败。例如下面的命令没有匹配任何测试类:
$mvn test -Dtest
这样的命令会导致构建失败,输出如下:
[INFO]---maven-surefire-plugin:2.4.3:test(default-test)@account-captcha---
[INPO]Surefire report directory:D:\code\ch-10\account-aggregator\account-captcha\target\surefire-reports
------------------------------------------------
TESTS
------------------------------------------------
There are no tests to run.
Results:
Tests run:0,Failures:0,Errors:0,Skipped:0[INFO]------------------------------------------------
[INFO]BUILD FAILURE
[INPO]------------------------------------------------
[INFO]Total time:1.747s
[INFO]Finished at: Sun Mar 28 17:00:27 CST 2023
[INFO]FinalMemory:2M/5M
[INFO]------------------------------------------------
[ERROR]Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:
2.4.3:test(default-test)on project account-captcha:No tests were executed!(Set-DfailIfNoTests=false to ignore this error.)->[Help1]
[ERROR]
[ERROR]To see the full stack trace of the errors,re-run Maven with the -e switch.
根据错误提示可以加上-DfaiIlfNoTests=false,告诉maven-surefire-plugin即使没有任何测试也不要报错:
$mvn test -Dtest-DfaillfNoTests=false
这样构建就能顺利执行完毕了。可以发现,实际上使用命令行参数-Dtest-DfaillfNoTests=false是另外一种跳过测试的方法。
我们看到,使用test参数用户可以从命令行灵活地指定要运行的测试类。可惜的是,maven-surefire-plugin并没有提供任何参数支持用户从命令行跳过指定的测试类,好在用户可以通过在POM中配置maven-surefire-plugin排除特定的测试类。
5️⃣ 包含与排除测试用例
上文第2节介绍了一组命名模式,符合这一组模式的测试类将会自动执行。Maven提倡约定优于配置原则,因此用户应该尽量遵守这一组模式来为测试类命名。即便如此,maven-surefire-plugin还是允许用户通过额外的配置来自定义包含一些其他测试类,或者排除一些符合默认命名模式的测试类。
例如,由于历史原因,有些项目所有测试类名称都以Tests结尾,这样的名字不符合默认的3种模式,因此不会被自动运行,用户可以通过代码所示的配置让Maven自动运行这些测试。
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.5</version><configuration><includes><include>**/*Tests.java</include></includes></configuration>
</plugin>
上述代码清单中使用了 **/*Tests.java来匹配所有以Tests结尾的Java类,两个星号**用来匹配任意路径,一个星号*匹配除路径风格符外的0个或者多个字符。
类似地,也可以使用excludes元素排除一些符合默认命名模式的测试类,如下代码所示。
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.5</version><configuration><excludes><exclude>**/*ServiceTest.java</exclude><exclude>**/TempDaoTest.java</exclude></excludes></configuration>
</plugin>
上述代码清单排除了所有以ServiceTest结尾的测试类,以及一个名为TempDaoTest的测试类。它们都符合默认的命名模式**/*Test.java,不过,有了excludes配置后,maven-surefire-plugin将不再自动运行它们。
6️⃣ 测试报告
除了命令行输出,Maven用户可以使用maven-surefire-plugin等插件以文件的形式生成更丰富的测试报告。
6.1基本的测试报告
默认情况下,maven-surefire-plugin会在项目的 target/surefire-reports目录下生成两种格式的错误报告:
- 简单文本格式
- 与JUnit兼容的XML格式
例如,运行 1.3节代码中的RandomGeneratofTest后会得到一个名为 com.xiaoshan.mvnbook.account.captcha.RandomGeneratorfTest.txt的简单文本测试报告和一个名为TEST-com.xiaoshan.mvnbook.account.captcha.RandomGeneratorTest.xml的 XML测试报告。前者的内容十分简单:
----------------------------------------------------------------
Test set:com.xiaoshan.mvnbook.account.captcha.RandomGeneratorTest
----------------------------------------------------------------
Tests run:1,Failures:0,Errors:0,Skipped:0,Time elapsed:0.029sec
这样的报告对于获得信息足够了,XML格式的测试报告主要是为了支持工具的解析,如Eclipse的JUnit插件可以直接打开这样的报告。
由于这种XML格式已经成为了Java单元测试报告的事实标准,一些其他工具也能使用它们。例如,持续集成服务器Hudson就能使用这样的文件提供持续集成的测试报告。
以上展示了一些运行正确的测试报告,实际上,错误的报告更具价值。我们可以修改 1.3节代码中的AccountCaptchaServiceTest让一个测试失败,这时得到的简单文本报告会是这样:
-----------------------------------------------------------------------
Test set:com.xiaoshan.mvnbook.account.captcha.AccountCaptchaServiceTest
-----------------------------------------------------------------------
Tests run:3,Failures:1,Errors:0,Skipped:0,Time elapsed:0.932 sec
FAILURE!
testValidateCaptchaCorrect(com.xiaoshan.mvnbook.account.captcha.AccountCaptchaserviceTest)Time elapsed:0.047 sec <<< FAILURE!
Java,lang.AssertionError:at org.junit.Assert.fail(Assert.java:91)at org.jundt.Assert.assertTrue(Assert.java:43)at org.junlt.Assert.assertTrue(Assert.java:54)at com.xiaoshan.mvnbook.account.captcha.AccountCaptchaServiceTest.testvalidateCaptchaCorrect(AccountCaptchaServiceTest.java:66)
报告说明了哪个测试方法失败、哪个断言失败以及具体的堆栈信息,用户可以据此快速地寻找失败原因。该测试的XML格式报告用EelipseJUnit插件打开,从所示的堆栈信息中可以看到,该测试是由maven-surefire-plugin发起的。
6.2 测试覆盖率报告
测试覆盖率是衡量项目代码质量的一个重要的参考指标。Cobertura是一个优秀的开源测试覆盖率统计工具(详见http://cobertura.soureeforge.net/), Maven通过cobertura-maven-plugin与之集成,用户可以使用简单的命令为Maven项目生成测试覆盖率报告。例如,可以在account-captcha目录下运行如下命令生成报告:
$mvn cobertura:cobertura
接着打开项目目录 target/site/cobertura/下的index.html文件,就能看到测试覆盖率报告。单击具体的类,还能看到精确到行的覆盖率报告。
7️⃣ 运行 TestNG 测试
TestNG是Java社区中除JUnit之外另一个流行的单元测试框架。NG是NextGeneration的缩写,译为“下一代”。TestNG在JUnit的基础上增加了很多特性,读者可以访问其站点 http://testng.org/ 获取更多信息。值得一提的是,《Next Generation Java Testing》(Java测试新技术,中文版已由机械工业出版社引进出版,书号为978-7-111-24550-6) 一书专门介绍TestNG和相关测试技巧。
使用Maven运行TestNG十分方便。以1.3节中的account-captcha测试代码为例,首先需要删除POM中的JUnit依赖,加入TestNG依赖,见代码:
<dependency><groupId>org.testng</groupId><artifactId>testng</artifactId><version>5.9</version><scope>test</scope><classifier>jdk15</classifier>
</dependency>
与JUnit类似,TestNG的依赖范围应为test。此外,TestNG使用 classifier jdk15和jdk14为不同的Java平台提供支持。
下一步需要将对JUnit的类库引用更改成对TestNG的类库引用。下表给出了常用类库的对应关系。
JUnit类 | TestNG类 | 作 用 |
org. junit. Test | org. testng. annotations. Test | 标注方法为测试方法 |
org. junit. Assert | org. testng. Asser | 检查测试结果 |
org. junit. Before | org. testng. annotations. BeforeMethod | 标注方法在每个测试方法之前运行 |
org. junit. After | org. testng. annotations. AfterMethod | 标注方法在每个测试方法之后运行 |
org. junit. BeforeClass | org. testng. annotations. BeforeClass | 标注方法在所有测试方法之前运行 |
org. junit. AfterClass | org. testng. annotations. AfterClass | 标注方法在所有测试方法之后运行 |
将JUnit的类库引用改成TestNG之后,在命令行输入mvn test, Maven就会自动运行那些符合命名模式的测试类。这一点与运行JUnit测试没有区别。
TestNG允许用户使用一个名为testng.xml的文件来配置想要运行的测试集合。例如,可以在account-captcha的项目根目录下创建一个testng.xml文件,配置只运行RandomGeneratorTest,如代码所示:
<?xml version="1.0" encoding="UTF-8"?><suite name="Suitel" verbose="1"><test name="Regression1"><classes><class name="com.xiaoshan.mvnbook.account.captcha.RandomGeneratorTest"/></classes> </test>
</suite>
同时再配置maven-surefire-plugin使用该testng.xml,如代码所示:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.5</version><configuration><suiteXmlFiles><suiteXmlFile>testng.xml</suiteXmlFile></suiteXmlFiles></configuration>
</plugin>
TestNG较JUnit的一大优势在于它支持测试组的概念,如下的注解会将测试方法加入到两个测试组util和medium中:
@Test(groups={"util","medium"})
由于用户可以自由地标注方法所属的测试组,因此这种机制能让用户在方法级别对测试进行归类。这一点JUnit无法做到,它只能实现类级别的测试归类。Maven用户可以使用代码所示的配置运行一个或者多个TestNG测试组。
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-piugin</artifactId><version>2.5</version><configuration><groups>util,medium</groups></configuration>
</plugin>
由于篇幅所限,这里不再介绍更多TestNG的测试技术,感兴趣的读者请访问TestNG 站点。
8️⃣ 重用测试代码
优秀的程序员会像对待产品代码一样细心维护测试代码,尤其是那些供具体测试类继承的抽象类,它们能够简化测试代码的编写。还有一些根据具体项目环境对测试框架的扩展,也会被大范围地重用。
在命令行运行mvn package的时候,Maven会将项目的主代码及资源文件打包,将其安装或部署到仓库之后,这些代码就能为他人使用,从而实现Maven项目级别的重用。默认的打包行为是不会包含测试代码的,因此在使用外部依赖的时候,其构件一般都不会包含测试代码。
然后,在项目内部重用某个模块的测试代码是很常见的需求,可能某个底层模块的测试代码中包含了一些常用的测试工具类,或者一些高质量的测试基类供继承。这个时候Maven用户就需要通过配置maven-jar-plugin将测试类打包,如代码所示:
<plugin><groupId>org.apache,maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.2</version><executions><execution><goals><goal>test-jar</goal></goals></execution></executions>
<plugin>
maven-jar-plugin有两个目标,分别是jar和testjar,前者通过Maven的内置绑定在default生命周期的package阶段运行,其行为就是对项目主代码进行打包,而后者并没有内置绑定,因此上述的插件配置显式声明该目标来打包测试代码。通过查询该插件的具体信息可以了解到,test-jar的默认绑定生命周期阶段为 package,因此当运行mvn clean package后就会看到如下输出:
[INFO]---maven-jar-plugin:2.2:jar (default-jar)@account-captcha ---
[INFO] Building jar:D:\code\ch-10\account-aggregator\account-captcha\target\account-captcha-1.0.0-SNAPSHOT.jar
[INFO]
[INFO]---maven-jar-plugin:2.2:test-jar (default)@account-captcha ---
[INFO] Building jar:D:\code\ch-10\account-aggregator\account-captcha\target\account-captcha-1.0.0-SNAPSHOT-tests.jar
maven-jar-plugin的两个目标都得以执行,分别打包了项目主代码和测试代码。现在,就可以通过依赖声明使用这样的测试包构件了,如代码所示:
<dependency><groupId>com.xiaoshan.mvnbook.account</groupId><artifactId>account-captcha</artifactId><version>1.0.0-SNAPSHOT<version><type>test-jar</type><scope>test</scope>
</dependency>
上述依赖声明中有一个特殊的元素 type, 所有测试包构件都使用特殊的 test-jar 打包类型。需要注意的是,这一类型的依赖同样都使用test依赖范围。
🌾 总结
本文的主题是Maven与测试的集成,不过在讲述具体的测试技巧之前先实现了背景案例的account-captcha模块,这一模块的测试代码也成了本章其他内容良好的素材。maven-surefire-plugin是Maven背后真正执行测试的插件,它有一组默认的文件名模式来匹配并自动运行测试类。用户还可以使用该插件来跳过测试、动态执行测试类、包含或排除测试等。maven-surefire-plugin能生成基本的测试报告,除此之外还能使用cobertura-maven-plugin生成测试覆盖率报告。
除了主流的JUnit之外,本章还讲述了如何与TestNG集成,最后介绍了如何重用测试代码。
⏪ 温习回顾上一篇(点击跳转): 《【Maven教程】(八):使用 Nexus 创建私服 ~》
⏩ 继续阅读下一篇(点击跳转)