JUnit学习笔记

Stella981
• 阅读 396

JUnit

JJUnit是用于编写和运行可重复的自动化测试的开源测试框架, 这样可以保证我们的代码按预期工作。 JUnit可广泛用于工业和作为支架(从命令行)或IDE(如Eclipse)内单独的Java程序。

基础知识

JUnit的安装和使用都非常的简单。这里使用IDEA+Maven演示。

创建项目

使用Idea和Maven创建一个最简单的Java项目:

JUnit学习笔记

添加JUnit4.x依赖

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
        </dependency>
    </dependencies>
    

这样子,就算完成了JUnit的基本安装。

注意:**TestCase需要在src/test/java下编写。**

TestCase

package app;

import org.junit.*;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 命名规则为:ClassNameTest
 */
public class AppTest {
    static final AtomicInteger count = new AtomicInteger(0);
    private ArrayList testList;

    /**
     * 每次运行@Test方法,都会实例化一个对象。
     */
    public AppTest() {
        System.out.println(String.format("CONSTRUCT CALL %d", count.incrementAndGet()));
    }

    /**
     * 指定一个静态方法,在所有@Test方法之前,执行一次。
     */
    @BeforeClass
    public static void onceExecutedBeforeAll() {
        System.out.println("@BeforeClass: onceExecutedBeforeAll");
    }

    /**
     * 指定一个静态方法,在所有@Test方法之后,执行一次。
     */
    @AfterClass
    public static void onceExecutedAfterAll() {
        System.out.println("@AfterClass: onceExecutedAfterAll");
    }

    /**
     * 在所有@Test方法之前执行
     */
    @Before
    public void executedBeforeEach() {
        testList = new ArrayList();
        System.out.println("@Before: executedBeforeEach");
    }

    /**
     * 在所有@Test方法之后执行
     */
    @After
    public void executedAfterEach() {
        testList.clear();
        System.out.println("@After: executedAfterEach");
    }

    /**
     * 命名规则:FunctionNameTest
     */
    @Test
    public void EmptyCollectionTest() {
        Assert.assertTrue(testList.isEmpty());
        System.out.println("@Test: EmptyArrayList");

    }

    /**
     * 命名规则:FunctionNameTest
     */
    @Test
    public void OneItemCollectionTest() {
        testList.add("oneItem");
        Assert.assertEquals(1, testList.size());
        System.out.println("@Test: OneItemArrayList");
    }

    /**
     * 忽略这个测试方法
     */
    @Ignore
    public void executionIgnoredTest() {
        System.out.println("@Ignore: This execution is ignored");
    }
}

上述是一个非常经典的例子,囊括了JUnit测试对象的生命周期

运行TestCase

运行TestCase是非常方便的。现在几乎所有的主流IDE(Idea,Eclipse)都支持JUnit。以下是Idea的启动过程:

JUnit学习笔记

这样子就开启了调试模式运行TestCase

运行日志

@BeforeClass: onceExecutedBeforeAll
CONSTRUCT CALL 1
@Before: executedBeforeEach
@Test: EmptyArrayList
@After: executedAfterEach
CONSTRUCT CALL 2
@Before: executedBeforeEach
@Test: OneItemArrayList
@After: executedAfterEach
@AfterClass: onceExecutedAfterAll

可以发现,JUnit的生命周期和注释保持一致。

扩展知识

@RunWith

使用JUnit的时候,有时候,需要自定义启动器(Runner)。这时候,我们可以通过@RunWith注解,来指定当前TestCase的Runner。 我们经常使用如下的Runner:

  • Suite : 测试套件
  • Parameterized : 参数化测试
  • SpringJUnit4ClassRunner : Spring针对JUnit4.x的测试框架

JUnitCore

在没有IDE的情况下,我们可以借助main函数,来运行我们的TestCase

package runner;

import app.AppTest;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class Main {
    public static void main(String[] args) {
        //通过JUnitCore指定,需要进行测试的TestCase
        Result result = JUnitCore.runClasses(AppTest.class);
        //搜集失败的测试用例信息
        for (Failure fail : result.getFailures()) {
            System.out.println(fail.toString());
        }
        //判断,单元测试是否全部通过
        if (result.wasSuccessful()) {
            System.out.println("All tests finished successfully...");
        }
    }
}

这样子,我们就可以通过命令行运行JUnit。

Suite

在JUnit中,我们可以将几个TestCase合并在一起进行单元测试:

JUnit学习笔记

通过@Suite.SuiteClasses()将几个TestCase合并在一起,方便单元测试。

异常和超时

在某些情况下,我们需要测试异常超时这两种情况。而这是通过@Test.expected@Test.timeout来实现的。

    /**
     * expected 期待获取的异常类型
     * timeout 测试用例超时时间
     * */
    @Test(expected = Exception.class, timeout = 1000)
    public void OneItemCollectionTest() throws Exception {
        Thread.sleep(500);
        System.out.println("@Test: OneItemArrayList");
    }
    

Spring 整合

依赖

Spring提供了spring-test来支持JUnit的测试框架。引入依赖:

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring-version}</version>
    </dependency>

SpringTest

**为了避免每个TestCase都添加@RunWith等注解,这里引入SpringTest方便TestCase编写**:

//http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#testing

//Spring针对JUnit4.x的支持Runner
@RunWith(SpringJUnit4ClassRunner.class)
//Spring配置类
@ContextConfiguration(classes = {SpringConf.class})
//支持Spring MVC
@WebAppConfiguration
//默认回滚
@Rollback
//默认事务
@Transactional
public abstract class SpringTest {
    
    //Spring 上下文
    @Autowired
    private WebApplicationContext wac;
    //Spring MVC测试支持类
    private MockMvc mockMvc;


    @Before
    public void init() {
        //构造mockMvc
        //不知道为什么Spring小组,不提供MockMvc注解方式@Autowired方式初始化
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
    
    //获取Spring MVC测试支持对象MockMvc
    public MockMvc getMockMvc() {
        return mockMvc;
    }
}

这样子,就定义了一个测试基类。具体的TestCase只需要继承这个测试基类即可。

Spring MVC测试

//继承测试基类
public class ArticleCtrlTest extends SpringTest {
    //路径
    final static String PATH = "/main/ArticleCtrl/";
    
    //支持@Autowired方式
    @Autowired
    ArticleIo articleIo;
    
    
    @Test
    public void getTest() throws Exception {
        final Article article = new Article(null, "测试数据", false);
        //插入一条数据
        articleIo.insert(article);
        //检测接口
        getMockMvc().perform(MockMvcRequestBuilders.post(PATH + "get").param("id", article.getId())).andDo(new ResultHandler() {
            @Override
            public void handle(MvcResult result) throws Exception {
                JSONObject ret = JSON.parseObject(result.getResponse().getContentAsString());
                //ok
                Assert.assertTrue(ret.getInteger("code") == 0);
                //check
                Assert.assertTrue(ret.getJSONObject("msg").getString("id").equals(article.getId()));
            }
        });
    }
    
}

以上,就是一个简单的Spring MVC测试用例。对于Dao或者Service测试就更加简单了。

注意:getTest的事务会进行回滚操作,不会真正的写入数据库。

运行截图

JUnit学习笔记

项目地址:java-fast-framework

执行流程

JUnit的测试流程大致如下:

  1. 指定需要测试的TestCase。假如采用Maven构建,则默认为所有/src/test/java/**Test类。
  2. JUnit加载TestCase的@RunWith指向的Runner。默认为:BlockJUnit4ClassRunner
  3. JUnit实例化Runner,然后调用Runner#run(RunNotifier notifier)方法,测试指定的TestCase。注意:Runner需要拥有一个Runner(Class clz)类型的构造函数。
  4. Runner通过notifier记录方法执行结果。
  5. JUnit收集所有TestCase的执行结果,然后打印报告。

注意:JUnit读取TestCase注解(@RunWith,@Test...)的时候,会遍历TestCase整个继承链。

BlockJUnit4ClassRunner

我们以BlockJUnit4ClassRunner这个Runner分析具体Runner#run的过程:

ParentRunner:
    
    //对指定的TestCase进行检测
    @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        try {
            //创造一个执行Block
            Statement statement = classBlock(notifier);
            //执行具体的Block
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.addFailedAssumption(e);
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            testNotifier.addFailure(e);
        }
    }
    
ParentRunner:
    
    //创建执行Block
    protected Statement classBlock(final RunNotifier notifier) {
        //获取待执行的语句,BlockJUnit4ClassRunner 为执行所有@Test方法语句
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            //处理@BeforeClass
            statement = withBeforeClasses(statement);
            //处理@AfterClass
            statement = withAfterClasses(statement);
            //处理@ClassRule
            statement = withClassRules(statement);
        }
        return statement;
    }
    
ParentRunner:
    //构造一个通过Statement,这个Statement具体执行的时候,会调用runChildren方法。
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                //语句被调用执行的时候,会真正的执行函数
                runChildren(notifier);
            }
        };
    }

这样子,就完成了Statement的构造过程。然后我们再看一下刚刚创建出来的Statement#evaluate函数:

ParentRunner:
    //构造一个通过Statement,这个Statement具体执行的时候,会调用runChildren方法。
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                //语句被调用执行的时候,会真正的执行函数
                runChildren(notifier);
            }
        };
    }

ParentRunner:
    //具体执行的过程
    private void runChildren(final RunNotifier notifier) {
        //获取当前的调度器,默认为主线程测试
        final RunnerScheduler currentScheduler = scheduler;
        try {
            //获取要测试的对象
            for (final T each : getFilteredChildren()) {
                currentScheduler.schedule(new Runnable() {
                    public void run() {
                        //进行刚刚给定的对象
                        ParentRunner.this.runChild(each, notifier);
                    }
                });
            }
        } finally {
            currentScheduler.finished();
        }
    }
    

上述的执行过程中,涉及到两个点:

  • getFilteredChildren:获取待测试的对象集合
  • runChild:进行具体的测试

我们,先看看getFilteredChildren方法:

ParentRunner:
    //获取要执行的对象集合
    private Collection<T> getFilteredChildren() {
        if (filteredChildren == null) {
            synchronized (childrenLock) {
                if (filteredChildren == null) {
                    //通过getChildren方法,委托子类,然后获取具体要测试的对象信息
                    filteredChildren = Collections.unmodifiableCollection(getChildren());
                }
            }
        }
        return filteredChildren;
    }
    
BlockJUnit4ClassRunner:
    
    //父类ParentRunner#getChildren具体实现方法,用来搜集执行对象信息
    @Override
    protected List<FrameworkMethod> getChildren() {
        return computeTestMethods();
    }
    
BlockJUnit4ClassRunner:

    //搜索@Test方法信息
    protected List<FrameworkMethod> computeTestMethods() {
        //搜索@Test方法信息,包括所有的父类
        return getTestClass().getAnnotatedMethods(Test.class);
    }

这样子,就搜集了待测试的@Test方法对象集合。然后,我们在看看具体的测试runChild

BlockJUnit4ClassRunner:

    @Override
    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
        Description description = describeChild(method);
        //判断这个对象是否@Ignored
        if (isIgnored(method)) {
            notifier.fireTestIgnored(description);
        } else {
            //1. 根据这个对象,通过methodBlock创建执行Block
            //2. 执行这个Block
            runLeaf(methodBlock(method), description, notifier);
        }
    }
    
BlockJUnit4ClassRunner:

    //创建待执行的Block
    protected Statement methodBlock(FrameworkMethod method) {
        Object test;
        try {
            //构造一个新的对象!!
            //也就是说,一个@Test方法对应一个对象
            test = new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest();
                }
            }.run();
        } catch (Throwable e) {
            return new Fail(e);
        }

        Statement statement = methodInvoker(method, test);
        //处理@Test#expected
        statement = possiblyExpectingExceptions(method, test, statement);
        //处理@Test#timeout
        statement = withPotentialTimeout(method, test, statement);
        //处理@Before
        statement = withBefores(method, test, statement);
        //处理@After
        statement = withAfters(method, test, statement);
        //处理@Rule
        statement = withRules(method, test, statement);
        return statement;
    }
    
BlockJUnit4ClassRunner:

    //执行刚刚创建的Block
    protected final void runLeaf(Statement statement, Description description,
            RunNotifier notifier) {
        EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
        eachNotifier.fireTestStarted();
        try {
            //执行
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            eachNotifier.addFailedAssumption(e);
        } catch (Throwable e) {
            eachNotifier.addFailure(e);
        } finally {
            eachNotifier.fireTestFinished();
        }
    }

注意:BlockJUnit4ClassRunner#methodBlock可以发现,每测试一个@Test方法,都会创建一个对象。

到此,BlockJUnit4ClassRunner#Runner#run(RunNotifier notifier)的运行流程,就基本分析完毕了。

SpringJUnit4ClassRunner

Spring 通过SpringJUnit4ClassRunner来支持JUnit。通过SpringJUnit4ClassRunner,我们可以实现如下特性:

  1. ApplicationContext仅仅初始化一次。
  2. SpringMVC 支持
  3. @Autowired 支持
  4. @Transactional和@Rollback支持

SpringJUnit4ClassRunner继承于BlockJUnit4ClassRunner对象,通过重写构造函数createTest来实现了以上的特性:

  1. 构造函数:ApplicationContext仅仅初始化一次。
  2. createTest:@Autowired IOC支持 和 @Transactional和@Rollback 等AOP支持。

最佳实践

这里总结一下JUnit最佳实践:

  1. 一个类,一个测试类;一个函数,一个测试函数;
  2. 命名规则: ClassNameTest 和 FunctionNameTest。
  3. 切勿@Test函数相互调用。
  4. 合理使用测试基类(如:SpringTest)。
  5. 覆盖率:业务类型>=60%,工具类型>=80%。

参考

点赞
收藏
评论区
推荐文章
光头强的博客 光头强的博客
4个月前
Java面向对象试题
1、请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
小森森 小森森
4个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Stella981 Stella981
1年前
Spring Boot(十二)单元测试JUnit
一、介绍JUnit是一款优秀的开源Java单元测试框架,也是目前使用率最高最流行的测试框架,开发工具Eclipse和IDEA对JUnit都有很好的支持,JUnit主要用于白盒测试和回归测试。<!more白盒测试:把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人员是公开的;回归测试
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue
NVIDIA安培架构下MIG技术分析
关键词:NVIDIA、MIG、安培一什么是MIG2020年5月,NVIDIA发布了最新的GPU架构:安培,以及基于安培架构的最新的GPU:A100。安培提供了许多新的特性,MIG是其中一项非常重要的新特性。MIG的全名是MultiInstanceGPU。NVIDIA安培架构中的MIG模式可以在A100GPU上并行运行七个作业。多实
密钥管理系统-为你的天翼云资产上把“锁
本文关键词:数据安全,密码机,密钥管理一、你的云上资产真的安全么?1.2021年1月,巴西的一个数据库30TB数据被破坏,泄露的数据包含有1.04亿辆汽车和约4000万家公司的详细信息,受影响的人员数量可能有2.2亿;2.2021年2月,广受欢迎的音频聊天室应用Clubhouse的用户数据被恶意黑客或间谍窃取。据悉,一位身份不明的用户能够将Clubho