MarK’s Blog

Rules for happiness: something to do, someone to love, something to hope for.

用JUnit对你的代码进行单元测试

| Comments

拖延症患者来补档了,这次要记录下来的内容是一个测试工具在项目中的应用,没错,就是前面有提到的java程序的测试工具JUnit。传送门:JUnit

对有些人来说JUnit是再熟悉不过的了,但对于另一些人来说JUnit是啥,它是干什么。

现在集成的Java的IDE环境,如IntelliJ,MyEclipse,大多数都已经将JUnit模块集成好了,开发者可以直接的在自己的项目中使用JUnit的测试模块,十分的方便。

在这里我们主要介绍的是JUnit测试工具的基本功能与基本的使用方法,至于如果将JUnit导入你的项目这里我就不再赘述了,你可以去JUnit官网下载最新的版本包导入工程,你也可以通过maven更方便的将JUnit4导入你的项目中。【PS:maven真是个好东西】

那么我们就开始吧,


目录

一、JUnit4的基本介绍与使用

二、用JUnit4来测试web项目中DAO层与Service层逻辑的正确性


如果对JUnit有一定基础的人可以完全无视第一部分,第一部分主要是UP用一种逗比的心态去用尽可能的简单的方式来介绍一下JUnit;而第二部分则是一个开发web项目人必备的一个技能。

开发环境:

操作系统:Mac OSX Yosemite Version 10.10.3

集成开发环境(IDE):IntelliJ IDEA 14.1

本地服务器:Tomcat 8.0.21

Maven版本:Maven3 3.0.5

JUnit版本:JUnit 4.11


一、JUnit4的基本介绍与使用

最开始我接触JUnit的功能的时候,它给我的感觉就是原来我不用每次都那么麻烦的自己去新建一个java class再写一个main函数啊,JUnit就直接帮我做好了。

复用老师总说的一句话是:你的开发代码要和测试代码完全的分开,不可以让测试代码混在你的开发代码中,或者说不能让你的测试代码“玷污”你的开发代码。

这里的混在一起不单单指的是混入你的开发代码中的某一行,也包括你的测试程序混在你的开发路径里也是不科学的。

所以一个java项目应该有三个路径,Sources, TestResources三个部分,Sources是你的开发代码,Test是你的测试代码,Resources是你的资源文件或配置文件。这就好像你的开发过程中使用MVC分层的方式一样,将不同功能的文件放到相应的路径下,便于管理。

上面说了一大堆的废话,下面让我们举一个例子吧,先看下面的一个Class,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Fawofolo {

    private String name;
    private int age;
    private String gender;
    private double height;
    private double weight;

    public Fawofolo(){
        this.name = "Fawofolo";
        this.age = 21;
        this.gender = "female";
        this.height = 1.414;
        this.weight = 70;
    }

    public Fawofolo(String _name, int _age, double _height, double _weight){
        this.name = _name;
        this.age = _age;
        this.gender = "female";
        this.height = _height;
        this.weight = _weight;
    }

    public void face(){
        System.out.println("I'm " + name + "'s face, but he/she doesn't need me anymore!!!");
    }

    public void eat(double _eat){
        weight += _eat;
        System.out.println(name + "'s weight is "+ (weight) + "kg");
    }

    public void leftHand(){
        System.out.println(name + "'s left hand is weaker than his/her right hand");
    }

    public void rightHand(){
        System.out.println(name + "'s right hand is stronger than his/her left hand");
    }

    public String print(){
        return "My name is " + name + ". I'm " + age + "years old.\n" + "I'm a "+ gender
                + ".\nMy height is " + height + ". My weight is " + weight +"kg.";
    }
}

在这个类中,它有许多的属性和函数,每个函数都执行对应的操作,如果我们不使用JUnit来测试的话,我想很大的可能性我们会这样做,

1
2
3
4
5
6
7
8
9
10
11
public class Main {
    public static void main(String[] args) {
        Fawofolo fool = new Fawofolo();
        System.out.println(fool.print());
        fool.face();
        fool.eat(12);
        fool.leftHand();
        fool.rightHand();
        System.out.println(fool.print());
    }
}

写一个新的class,里面有一个main方法,然后在这个main方里写一些调用语句,在console中查看输出结果,来测试我们代码的正确性。如果你是一个严谨的程序员你可能会把这个class放到一个单独的路径下,但是如果你也把这个class放到你的开发路径下,哦那糟糕透了。。。。。

看一下console的输出:

1
2
3
4
5
6
7
8
9
10
My name is Fawofolo. I'm 21years old.
I'm a female.
My height is 1.414. My weight is 70.0kg.
I'm Fawofolo's face, but he/she doesn't need me anymore!!!
Fawofolo's weight is 82.0kg
Fawofolo's left hand is weaker than his/her right hand
Fawofolo's right hand is stronger than his/her left hand
My name is Fawofolo. I'm 21years old.
I'm a female.
My height is 1.414. My weight is 82.0kg.

看样子我们的目的还是达到了的,输出的结果和我预期的结果是相同的,但是综合其他的因素,这段代码还是糟糕了!!!

下面该我们的JUnit登场了,在class类名字处打开IntelliJ的quickLips,就可以看到如下的画面: 然后我们选择Create Test, 接下来会出现 选择JUnit4做为测试工具,Destination Package的根路径是你设置的Test的路径,这里我选择com.mark.entity就是在Test的路径下的com.mark.entity里生成我们的测试程序,

setUp的功能就是在执行测试程序之前要准备做的事情,我勾选这个是要在程序测试开始之前先实例化一个Fawofolo类。对应的tearDown就是程序结束之后要做的事情,可能你在测试结束之后要释放一些资源或者其他的事情都写在这个方法下。再下面就是勾选你要测试的方法了,这里我们全部勾选,我们要为每一个方法生成一个测试方法。

然后我们的测试路径下就多了这样的一个测试文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class FawofoloTest {

    @Before
    public void setUp() throws Exception {

    }

    @Test
    public void testFace() throws Exception {

    }

    @Test
    public void testEat() throws Exception {

    }

    @Test
    public void testLeftHand() throws Exception {

    }

    @Test
    public void testRightHand() throws Exception {

    }

    @Test
    public void testPrint() throws Exception {

    }
}

@Before是测试程序执行前要做的操作,每一个@Test都是一个小的测试程序,它们都可以独立的运行,你只需要在对应的方法处右击,选择run相应的方法就可以。

我们把要测试的内容填写完整,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class FawofoloTest {

    Fawofolo fool;

    @Before
    public void setUp() throws Exception {
        fool = new Fawofolo();
    }

    @Test
    public void testFace() throws Exception {
        fool.face();
    }

    @Test
    public void testEat() throws Exception {
        fool.eat(23);
    }

    @Test
    public void testLeftHand() throws Exception {
        fool.leftHand();
    }

    @Test
    public void testRightHand() throws Exception {
        fool.rightHand();
    }

    @Test
    public void testPrint() throws Exception {
        System.out.println(fool.print());
    }
}

光标在testFace()处的时候右击就会看到如下的情况, 我们选择runTestFace()的话,会看到如下的结果,其他的方法同理 首先右上的绿条表示程序正常的运行且通过了,如果为红条则表示程序中存在bug不可以正常的运行。其中console也可以显示你的所有输出。当然你也可以在非测试方法的地方右击选行运行整个测试类。

第一部分就这样结束了,有木有感觉很高大上呢。。。


二、用JUnit4来测试web项目中DAO层与Service层逻辑的正确性

这一部分是在先前的文章Spring-SpringMVC-Hibernate在IntelliJ与Maven的环境下搭建的基础上,添加使用JUnit4对DAO层(与Service层)的业务逻辑代码进行测试,因为当项目层次较多就会涉及到合作,合作就意味着你要确保你的代码的正确性,于是测试工作就是必不可少的,而与那些我们人工的创建的测试相比,JUnit4的自动测试的强大性就体现出来了。

首先我们要在环境的配置上做一个小小的变更,因为在之前的环境搭建里将spring与hibernate配置文件分开管理,然后在web.xml中添加了配置路径。但这在Web项目部署到服务器上是有效的,而我们现在要做是测试我们DAO层的java代码,这一过程完全不涉及到部署与服务器。所以我们要在spring的配置文件中添加索引,指向我们的hibernate的配置是什么,修改如下,在applicationContext.xml中加入下面这句,就可以了。

1
<import resource="infrastructure.xml"/>

除此之外,我们要在我们创建的测试类前声明以下内容,

1
2
3
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/META-INF/applicationContext.xml")
@Transactional

@SpringJUnit4ClassRunner,是指我们要在spring的管理下进行我们的测试工作,这一部可能需要引入新的包,添加maven dependicy即可。

@ContextConfigurate,指向的是我们spring的配置文件,请根据项目的实际情况指向你的spring配置文件。

@Transactional,这是因为我们项目架构的原因,我们在项目中引入了事务,只有事务的开启的时候才可以进行Hibernate的操作,如过操作出现问题就会回滚,但是我们仅仅在service层开启了事务,在dao层并没有,所以如果对我们DAO层的程序代码进行测试就需要添加这句,表示在测试程序中开启事务,以便我们正常的完成测试过程。

其实我们DAO层的大部分工作都是对数据库中相应的表进行CRUD操作,也就意味着大多数的DAO做着相同的工作,只是工作的对象不同而以,于是我们可以采取继承和模板的方法来简化我们的工作,我们创建一个一BaseDAO其中实现了基本的CRUD操作,然后让其他的DAO来继承这个基类,这样相同的CRUD操作我们直接使用父类的方法就可以了,这在一定的程度上可以简化我们的工作量,下面列出一个自己的BaseDAO的接口与抽象类的实现,

1
2
3
4
5
6
7
8
public interface BaseDAO<T> {

    T queryById(String id);
    List<T> queryAll();
    void insert(T t);
    void delete(T t);
    void update(T t);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public abstract class BaseDAOImpl<T> implements BaseDAO<T>  {
    private Class<T> entityClass;

    public BaseDAOImpl(Class<T> clazz) {
        this.entityClass = clazz;
    }

    @Autowired
    private SessionFactory sessionFactory;

    @Override
    public void insert(T t) {
        sessionFactory.getCurrentSession().save(t);
    }

    @Override
    public void delete(T t) {
        sessionFactory.getCurrentSession().delete(t);
    }

    @Override
    public void update(T t) {
        sessionFactory.getCurrentSession().update(t);
    }

    @SuppressWarnings("unchecked")
    @Override
    public T queryById(String id) {
        return (T) sessionFactory.getCurrentSession().get(entityClass, id);
    }

    @Override
    public List<T> queryAll() {
        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(entityClass);
        return criteria.list();
    }

}

然后我们在数据库中创建一个User表,结构如下, 对应的EJB如下,User.java,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Entity
@Table
public class User implements Serializable {
    @Id
    private String userId;

    private String userName;
    private int userLevel;
    private int userExperience;
    private String userPassword;

    public int getUserExperience() {
        return userExperience;
    }

    public void setUserExperience(int userExperience) {
        this.userExperience = userExperience;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public int getUserLevel() {
        return userLevel;
    }

    public void setUserLevel(int userLevel) {
        this.userLevel = userLevel;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserPassword() {
        return userPassword;
    }

    public void setUserPassword(String userPassword) {
        this.userPassword = userPassword;
    }
}

DAO层的接口UserDAO.java如下,当然因为BaseDAO比较简单,不能完成你全部的功能,你也可以在UserDAO下新添一些特定的方法,

1
2
3
public interface UserDAO extends BaseDAO<User> {

}

UserDAOImpl.java如下,如果你在接口中添加了新的方法别忘了在这里实现,

1
2
3
4
5
6
7
8
@Repository
@Transactional
public class UserDAOImpl extends BaseDAOImpl<User> implements UserDAO {

    public UserDAOImpl(){
        super(User.class);
    }
}

在这里我们只补全父类中的构造方法,我们就可以使用父类中定义的CRUD操作了,省去了实现的过程,是不是少了很多的工作呢。

下面我们来测试一下我们通过父类继承的CRUD操作是否能正常使用呢?

过程和第一部分一样,首先在要测试的类名字处右击,选择创建测试类,生成的测试类加上我们之前说的必要声明之后,应该是这样的,(如果发现没有父类的方法,记得勾选Show Inherited Methods哦)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/META-INF/applicationContext.xml")
@Transactional
public class UserDAOImplTest {

    @Autowired
    UserDAO dao;

    @Test
    public void testInsert() throws Exception {

    }

    @Test
    public void testDelete() throws Exception {

    }

    @Test
    public void testUpdate() throws Exception {

    }

    @Test
    public void testQueryById() throws Exception {

    }

    @Test
    public void testQueryAll() throws Exception {

    }
}

在相应的测试方法下添加对应的操作,来测试一下通过继承BaseDAO的CRUD的实现是否可以正常的运行吧,不过要注意的有两点:

1. console中出现了SQL语句并不代表顺利的执行了,因为可能存在异常导致事物回滚

2. 在JUnit接合Spring配置和Hibernate的环境下的测试都是回滚的!!!!你没有看错,JUnit非常智能的,他将你的测试程序执行完成之后,会回滚所有的操作,做到了测试代码不影响项目程序的要求,也就解释了为什么你测试程序运行后去查看数据库并没有变化,因为所有的操作都被回滚了。你可以通过看测试的状态条是否为绿色来判断程序的正确性,或者加入几条输入语句来confirm一下。

至此,高大上的JUnit就介绍完了,从现在起就用JUnit对你的java代码进行单元测试吧!谢谢阅读~

Comments

返回顶部