Java 注解与单元测试
注解
Java注解是在JDK1.5 之后出现的新特性,用来说明程序的,注解的主要作用体现在以下几个方面:
- 编译检查,例如 @Override
- 编写文档,java doc 会根据注解生成对应的文档
- 代码分析,通过注解对代码进行分析[利用反射机制]
JDK 中有一些常用的内置注解,例如:
- Override:检查被该注解修饰的方法是否是重写父类的方法
- Deprecatedd:被该注解标注的内容已过时
- SuppressWarnning: 压制警告,传入参数all表示压制所有警告
自定义注解
JDK中虽然内置了大量注解,但是它也允许我们自定义注解,这样就为程序编写带来了很大的便利,像有些框架就大量使用注解。
java注解本质上是一个继承了 java.lang.annotation.Annotation
接口的一个接口,但是如果只是简单的使用关键字 interface
来定义接口,仍然不是注解,仅仅是一个普通的接口,在定义注解时需要使用关键字 @interface
, 该关键字会默认继承 Annotation
接口,并将定义的接口作为注解使用
注解中可以定义方法,这些方法的返回值只能是基本类型、String、枚举类型、注解以及这些类型的数组,我们称这些方法叫做属性。
在使用注解时需要注意以下几个事情
- 必须给注解的属性赋值,如果不想赋值可以使用default来设置默认值
- 如果属性列表中只有一个名为value的属性,那么在赋值时可以不用指定属性名称
- 多个属性值之间使用逗号隔开
- 数组属性的赋值使用
{}
, 而当数组属性中只有一个值时,{}
可以省略不写
元注解
元注解是用来描述注解的注解,Java中提供的元注解有下列几个
Target
描述注解能够作用的位置,即哪些Java代码元素能够使用该注解,注解的源代码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
这个注解只有一个value属性,属性需要传入一个 ElementType枚举类型的数组,该枚举类型可以取下列几个值
ElementType | 含义 |
---|---|
TYPE | 接口、类(包括注解)、枚举类型上使用 |
FIELD | 字段声明(包括枚举常量) |
METHOD | 方法 |
PARAMETER | 参数声明 |
CONSTRUCTOR | 构造函数 |
LOCAL_VARIABLE | 局部变量声明 |
ANNOTATION_TYPE | 注解类型声明 |
PACKAGE | 包声明 |
Retention
表示该注解类型的注解保留的时长,主要有3个阶段: 源码阶段,类对象阶段,运行阶段;源码阶段是只只存在与源代码中,类对象阶段是指被编译进 .class 文件中,类对象阶段是指执行时被加载到内存.则默认保留策略为RetentionPolicy.CLASS。
它的源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
Documented
表示拥有该注解的元素可通过javadoc此类的工具进行文档化。源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
Inherited
表示该注解类型被自动继承
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
内置注解解读
下面通过几个JDK内置注解的解读来说明注解相关使用
Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
该注解用于编译时检查,被该注解注释的方法是否是重写父类的方法。
从源码上看,它只能在方法上使用,并且它仅仅存在于源码阶段不会被编译进 .class 文件中
Deprecatedd
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
用于告知编译器,某一程序元素(例如类、方法、属性等等)不建议使用
从源码上看,几乎所有的Java程序元素都可以使用它,而且会被加载到内存中
SuppressWarnning
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
告知编译器忽略特定类型的警告
它需要传入一个字符串的数组,取值如下:
参数 | 含义 |
---|---|
deprecation | 使用了过时的类或方法时的警告 |
unchecked | 执行了未检查的转换时的警告 |
fallthrough | 当Switch程序块进入进入下一个case而没有Break时的警告 |
path | 在类路径、源文件路径等有不存在路径时的警告 |
serial | 当可序列化的类缺少serialVersionUID定义时的警告 |
finally | 任意finally子句不能正常完成时的警告 |
all | 以上所有情况的警告 |
在程序中解析注解
一般通过反射技术来解析自定义注解,要通过反射技术来识别注解,前提条件就是注解要在内存中被加载也就是要使它的范围为 RUNTIME;
JDK提供了以下常用API方便我们使用
返回值 | 方法 | 解释 |
---|---|---|
T | getAnnotation(Class annotationClass) | 当存在该元素的指定类型注解,则返回相应注释,否则返回null |
Annotation[] | getAnnotations() | 返回此元素上存在的所有注解 |
Annotation[] | getDeclaredAnnotations() | 返回直接存在于此元素上的所有注解。 |
boolean | isAnnotationPresent(Class<? extends Annotation> annotationClass) | 当存在该元素的指定类型注解,则返回true,否则返回false |
实战
下面使用一个完整的例子来说明自定义注解以及在程序中使用注解的例子,现在来模仿JUnit 定义一个MyTest的注解,只要被这个注解修饰的方法将来都会被自动执行
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
}
首先定义一个注解,后续来执行用这个注解修饰了的所有方法,通过Target来修饰标明注解只能用于方法上,通过Retention修饰标明注解会被保留到运行期
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Test {
@MyTest
public void test1(){
System.out.println("this is test1");
}
@MyTest
public void test2(){
System.out.println("this is test2");
}
public static void main(String[] args) {
Method[] methods = Test.class.getMethods();
for (Method method:methods){
if (method.isAnnotationPresent(MyTest.class)){
try {
method.invoke(new Test());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
在测试类中定义了两个测试函数都使用 @MyTest
修饰,在主方法中,首先通过反射机制获取该类中所有方法,然后调用方法的 isAnnotationPresent
函数判断该方法是否被 @Test
修饰,如果是则执行该方法。这样以后即使再添加方法,只要被 @MyTest
修饰就会被调用。
Junit框架
在软件开发中为了保证软件质量单元测试是必不可少的一个环节,Java中提供了Junit 测试框架来进行单元测试
一般一个Java项目每一个类都会对应一个test类用来做单元测试,例如有一个Person类,为了测试Person类会定义一个PersonTest类来测试所有代码
JUnit 中定义了一些注解来方便我们编写单元测试
- @Test:测试方法,被该注解修饰的方法就是一个测试方法
- @Before:在测试方法被执行前会执行该注解修饰的方法
- @After:在测试方法被执行后会执行该注解修饰的方法
除了注解JUnit定义了一些断言函数来实现自动化测试,常用的有如下几个:
- void assertEquals(boolean expected, boolean actual):检查两个变量或者等式是否平衡
- void assertTrue(boolean expected, boolean actual):检查条件为真
- void assertFalse(boolean condition):检查条件为假
- void assertNotNull(Object object):检查对象不为空
- void assertNull(Object object):检查对象为空
- void assertSame(boolean condition):assertSame() 方法检查两个相关对象是否指向同一个对象
- void assertNotSame(boolean condition):assertNotSame() 方法检查两个相关对象是否不指向同一个对象
- void assertArrayEquals(expectedArray, resultArray):assertArrayEquals() 方法检查两个数组是否相等
这些函数在断言失败后会抛出异常,后续只要查看异常就可以哪些测试没有通过
假设先定义一个计算器类,来进行两个数的算数运算
public class Calc {
public int add(int a, int b){
return a + b;
}
public int sub(int a, int b){
return a - b;
}
public int mul(int a, int b){
return a * b;
}
public float div(int a, int b){
return a / b;
}
}
为了测试这些方法是否正确,我们来定义一个测试类
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CalcTest {
@Test
public void addTest(){
int result = new Calc().add(1,2);
assertEquals(result, 3);
}
@Test
public void subTest(){
int result = new Calc().sub(1,2);
assertEquals(result, -1);
}
@Test
public void mulTest(){
int result = new Calc().mul(1,2);
assertEquals(result, 2);
}
@Test
public void divTest(){
float result = new Calc().div(1,2);
assertEquals(result, 0.5, 0.001); //会报异常
}
}
经过测试发现,最后一个divTest方法 会报异常,实际值是0,因为我们使用 /
来计算两个int时只会保留整数位,也就是得到的是0,与预期的0.5不匹配,因此会报异常