Java 注解与单元测试

注解

Java 注解是在 JDK1.5 之后出现的新特性,用来说明程序的,注解的主要作用体现在以下几个方面:

  1. 编译检查,例如 @Override
  2. 编写文档,java doc 会根据注解生成对应的文档
  3. 代码分析,通过注解对代码进行分析 [利用反射机制]

JDK 中有一些常用的内置注解,例如:

  1. Override:检查被该注解修饰的方法是否是重写父类的方法
  2. Deprecatedd:被该注解标注的内容已过时
  3. SuppressWarnning: 压制警告,传入参数 all 表示压制所有警告

自定义注解

JDK 中虽然内置了大量注解,但是它也允许我们自定义注解,这样就为程序编写带来了很大的便利,像有些框架就大量使用注解。

java 注解本质上是一个继承了 java.lang.annotation.Annotation 接口的一个接口,但是如果只是简单的使用关键字 interface来定义接口,仍然不是注解,仅仅是一个普通的接口,在定义注解时需要使用关键字 @interface, 该关键字会默认继承 Annotation 接口,并将定义的接口作为注解使用

注解中可以定义方法,这些方法的返回值只能是基本类型、String、枚举类型、注解以及这些类型的数组,我们称这些方法叫做属性。

在使用注解时需要注意以下几个事情

  1. 必须给注解的属性赋值,如果不想赋值可以使用 default 来设置默认值
  2. 如果属性列表中只有一个名为 value 的属性,那么在赋值时可以不用指定属性名称
  3. 多个属性值之间使用逗号隔开
  4. 数组属性的赋值使用 {}, 而当数组属性中只有一个值时, {} 可以省略不写

元注解

元注解是用来描述注解的注解,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");
}

<span class="hljs-meta">@MyTest</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">test2</span><span class="hljs-params">()</span>{
    System.out.println(<span class="hljs-string">"this is test2"</span>);
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {
    Method[] methods = Test.class.getMethods();
    <span class="hljs-keyword">for</span> (Method method:methods){
        <span class="hljs-keyword">if</span> (method.isAnnotationPresent(MyTest.class)){
            <span class="hljs-keyword">try</span> {
                method.invoke(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Test</span>());
            } <span class="hljs-keyword">catch</span> (IllegalAccessException e) {
                e.printStackTrace();
            } <span class="hljs-keyword">catch</span> (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
}

}

在测试类中定义了两个测试函数都使用 @MyTest 修饰,在主方法中,首先通过反射机制获取该类中所有方法,然后调用方法的 isAnnotationPresent 函数判断该方法是否被 @Test修饰,如果是则执行该方法。这样以后即使再添加方法,只要被 @MyTest 修饰就会被调用。

Junit 框架

在软件开发中为了保证软件质量单元测试是必不可少的一个环节,Java 中提供了 Junit 测试框架来进行单元测试

一般一个 Java 项目每一个类都会对应一个 test 类用来做单元测试,例如有一个 Person 类,为了测试 Person 类会定义一个 PersonTest 类来测试所有代码

JUnit 中定义了一些注解来方便我们编写单元测试

  1. @Test:测试方法,被该注解修饰的方法就是一个测试方法
  2. @Before:在测试方法被执行前会执行该注解修饰的方法
  3. @After:在测试方法被执行后会执行该注解修饰的方法

除了注解 JUnit 定义了一些断言函数来实现自动化测试,常用的有如下几个:

  1. void assertEquals(boolean expected, boolean actual): 检查两个变量或者等式是否平衡
  2. void assertTrue(boolean expected, boolean actual): 检查条件为真
  3. void assertFalse(boolean condition): 检查条件为假
  4. void assertNotNull(Object object): 检查对象不为空
  5. void assertNull(Object object): 检查对象为空
  6. void assertSame(boolean condition):assertSame() 方法检查两个相关对象是否指向同一个对象
  7. void assertNotSame(boolean condition):assertNotSame() 方法检查两个相关对象是否不指向同一个对象
  8. void assertArrayEquals(expectedArray, resultArray):assertArrayEquals() 方法检查两个数组是否相等

这些函数在断言失败后会抛出异常,后续只要查看异常就可以哪些测试没有通过

假设先定义一个计算器类,来进行两个数的算数运算

public class Calc {
    public int add(int a, int b){
        return a + b;
    }
<span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">sub</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span>{
    <span class="hljs-keyword">return</span> a - b;
}

<span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">mul</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span>{
    <span class="hljs-keyword">return</span> a * b;
}

<span class="hljs-keyword">public</span> <span class="hljs-type">float</span> <span class="hljs-title function_">div</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span>{
    <span class="hljs-keyword">return</span> 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);
    }
<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">subTest</span><span class="hljs-params">()</span>{
    <span class="hljs-type">int</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Calc</span>().sub(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>);
    assertEquals(result, -<span class="hljs-number">1</span>);
}

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">mulTest</span><span class="hljs-params">()</span>{
    <span class="hljs-type">int</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Calc</span>().mul(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>);
    assertEquals(result, <span class="hljs-number">2</span>);
}

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">divTest</span><span class="hljs-params">()</span>{
    <span class="hljs-type">float</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Calc</span>().div(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>);
    assertEquals(result, <span class="hljs-number">0.5</span>, <span class="hljs-number">0.001</span>); <span class="hljs-comment">//会报异常</span>
}

}

经过测试发现,最后一个 divTest 方法 会报异常,实际值是 0,因为我们使用 / 来计算两个 int 时只会保留整数位,也就是得到的是 0,与预期的 0.5 不匹配,因此会报异常