JUnit教程

JUnit教程

为什么需要进行单元测试?

有的时候我们编写完一个方法或者功能后需要简单的进行一下测试,看实现的功能是否符合预期,这个时候我们会选择编写一个Main方法,在方法中调用我们编写的方法是否与期望的值相等,若相等则测试成功。

但是使用Main()方法测试存在如下缺点

  • 只能有一个main()方法,不能把测试代码分离

  • 没有打印出测试结果和期望结果

    例如,expected:362800,but actual:123456

所以,需要一种测试框架,帮助我们编写测试。

单元测试的好处:

  • 确保单个方法运行正常
  • 如果修改了方法代码,只需确保其对应的单元测试通过
  • 测试代码本身就可以作为实例代码
  • 可以自动化运行所有测试并获得报告

JUnit特点

  • 使用断言(Assertion)测试期望结果
  • 可以方便地组织和运行测试
  • 可以方便地查看测试结果
  • 常用IDE(例如Eclipse)都集成了JUnit
  • 可以方便地集成到Maven

JUnit的设计

  • TestCase:一个TestCase表示一个测试
  • TestSuite:一个TestSuite包含一组TestCase,表示一组测试
  • TestFixture:一个TestFixture表示一个测试环境
  • TestResult:用于收集测试结果
  • TestRunner:用于运行测试
  • TestListener:用于监听测试过程,收集测试数据
  • Assert:用于断言测试结果是否正确

JUnit目前的版本

版本JUnit 3.xJUnit 4JUnit 5
JDKJDK < 1.5JDK >= 1.5JDK >= 1.8
ClassClass MyTest extends TestCase{}Class MyTest{}Class MyTest{}
MethodPublic testAbc(){}@Test public abc(){}@Test public abd(){}

使用Assert断言

  • 断言相等:assertEquals(100,x);

  • 断言数组相等:assertArrayEquals({1,2,3},x);

  • 浮点数断言相等:assertEquals(3.1416,x,0.0001);

  • 断言为null:assertNull(x);

  • 断言为true/false:assertTrue(x>0) assertFalse(x < 0)

  • 其他:assertNotEquals/assertNotNull

    package cn.wustxiao;
    
    import java.util.Arrays;
    
    public class Calculator {
    	public int calculate(String expression) {
    		String[] ss = expression.split("\\+");
    		System.out.println(expression+" => "+Arrays.toString(ss));
    		int sum = 0;
    		for(String s:ss) {
    			sum = sum + Integer.parseInt(s);
    		}
    		return sum;
    	}
    	
    	public static void main(String[] args) {
    		Calculator c = new Calculator();
    		int r = c.calculate("1+2+3");
    		System.out.println(r);
    	}
    }
    
    package cn.wustxiao;
    
    import static org.junit.Assert.*;
    
    import org.junit.Test;
    
    public class CalculatorTest {
    
    	@Test
    	public void testCalculate() {
    		assertEquals(3, new Calculator().calculate("1+2"));
    		assertEquals(13, new Calculator().calculate("11+2"));
    		assertEquals(24, new Calculator().calculate("1+23"));
    	}
    	
    	@Test
    	public void testCalculateWithSpace() {
    		assertEquals(3, new Calculator().calculate("1 + 2"));
    	}
    
    }
    

使用before和after

同一个单元测试内的多个测试方法:

  • 测试前都需要初始化某些对象

  • 测试后可能需要清理资源

    fileInputStream.close()

JUnit对于每个@Test方法:

  1. 实例化CalculatorTest
  2. 执行@Before方法
  3. 执行@Test方法
  4. 执行@After方法

初始化测试资源称为Fixture

  • @Before:初始化测试对象,例如:input = new FileInputStream();
  • @After:销毁@Before创建的测试对象,例如:input.close()
  • @BeforeClass:初始化非常耗时的资源,例如:创建数据库
  • @AfterClass:清理@BeforeClass创建的资源,例如:删除数据库

异常测试

对可能抛出的异常进行测试:

异常本身是方法签名的一部分:

public static int parseInt(String s) throws NumberFormatException

测试错误的输入是否导致特定的异常:

Integer.parseInt(null);
Integer.parseInt("");
Integer.parseInt("xyz");

如何测试异常:

@Test
public void testNumberFormatException(){
  try{
    Integer.parseInt(null);
    fail("Should throw exception!");
  }catch(NumberFormatException e){
    // ok
  }
}

更好的方法是使用@Test(expected=Exception.class)测试异常

@Test(expected = NumberFormatException.class)
public void testNumbeFormatException(){
  Integer.parseInt(null);
}

参数化测试

如果待测试的输入和输出是一组数据:

  • 可以把测试数据组织起来
  • 用不同的测试数据调用相同的测试方法
@RunWith(Parameterized.class)
public class AbsTest{
  @Parameters
  public static Collection<?> data(){
    return Arrays.asList(new Object[][]{
      {0,0},{1,1},{-1,-1}
    });
  }
  int input;
  int expected;
  public AbsTest(int input,int expected){
    this.input = input;
    this.expected = expected;
  }
  
  @Test
  public void testAbs{
    int r = Math.abs(this.input);
    assertEquals(this.expected,r);
  }
}

参数必须由静态方法data()返回,返回类型为Collection<Object[]>,静态方法必须标记为@Parameters,测试类必须标记为@RunWith(Parameterized.class)构造方法参数必须和测试参数对应。

超时测试

可以为JUnit的单个测试设置超时:

  • 超时设置为1秒:@Test(timeout=1000)
  • timeout单位是毫秒
  • 超时测试不能取代性能测试和压力测试

实例

最后用最近培训JUnit的课后习题为例子

package com.nssol.learning;

import org.joda.time.DateTime;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class BirthdayCaculator {

    public int caculate(String birthday) {
        // 首先对输入的日期是否是 null 或者是 "" 进行判断
        if (birthday == null || birthday.isEmpty()) {
            throw new RuntimeException("Birthday should not be null or empty");
        }

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Calendar today = Calendar.getInstance();
        today.setTime(DateTime.now().toDate());

        // 处理输入的日期恰好等于今年生日的情况
        if (birthday.equals(sdf.format(today.getTime()))) {
            return 0;
        }

        // 输入日期格式的有效性检查
        Calendar birthDate = Calendar.getInstance();
        try {
            birthDate.setTime(sdf.parse(birthday));
        } catch (ParseException e) {
            throw new RuntimeException("Birthday format is invalid!");
        }
        birthDate.set(Calendar.YEAR, today.get(Calendar.YEAR));

        // 实际计算的逻辑
        int days;
        if (birthDate.get(Calendar.DAY_OF_YEAR) < today.get(Calendar.DAY_OF_YEAR)) {
            days = today.getActualMaximum(Calendar.DAY_OF_YEAR) - today.get(Calendar.DAY_OF_YEAR);
            days += birthDate.get(Calendar.DAY_OF_YEAR);
        } else {
            days = birthDate.get(Calendar.DAY_OF_YEAR) - today.get(Calendar.DAY_OF_YEAR);
        }

        return days;
    }
}

请为该方法编写一个测试类

package com.nssol.learning;

import org.joda.time.DateTimeUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.junit.MockitoJUnitRunner;

import java.text.ParseException;
import java.util.Date;

import static org.junit.Assert.assertEquals;

@RunWith(MockitoJUnitRunner.class)
public class BirthdayCaculatorTest {

    @InjectMocks
    private BirthdayCaculator birthdayCaculator;

    @Before
    public void setUp() {
        Date fixedDateTime = new Date("2019/08/24");
        DateTimeUtils.setCurrentMillisFixed(fixedDateTime.getTime());
    }

    @After
    public void tearDown() {
        DateTimeUtils.setCurrentMillisSystem();
    }

    // 输入日期为空字符串
    @Test(expected = RuntimeException.class)
    public void testBirthdayCaculatorWithSpace() {
        birthdayCaculator.caculate("");
    }

    // 输入日期为null
    @Test(expected = RuntimeException.class)
    public void testBirthdayCaculatorWithNull() {
        birthdayCaculator.caculate(null);
    }

    // 输入日期为全角
    @Test(expected = RuntimeException.class)
    public void testBirthdayCaculatorWithQuanJiao() {
        birthdayCaculator.caculate("2019-01-01");
    }

    // 输入日期格式不正确
    @Test(expected = RuntimeException.class)
    public void testBirthdayCaculatorWrongFormat(){
        birthdayCaculator.caculate("2019 01 01");

    }

    // 输入日期无效
    @Test(expected = RuntimeException.class)
    public void testBirthdayCaculatorWrongDate(){
        birthdayCaculator.caculate("2019-02-29");
        birthdayCaculator.caculate("2019-02-30");
    }

    // 测试结果是否正确
    @Test
    public void testBirthdayCaculatorResult() throws Exception {
        assertEquals(0, birthdayCaculator.caculate("2019-08-24"));
        assertEquals(364, birthdayCaculator.caculate("2019-08-23"));
        assertEquals(0, birthdayCaculator.caculate("2020-08-24"));
        assertEquals(9, birthdayCaculator.caculate("2019-09-02"));
    }

}

运行结果如图

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值