单元测试实践总结

1、A\B测试

1、是什么
一个好的产品都是迭代出来的,而我们很可能不清楚这次的迭代最终是好是坏(至少我们是觉得迭代对用户是好的,是有帮助的,对公司的转化也是好的),但是我们的用户未必就买账。
ABTest最主要做的就是一个分流:

  • 将10%流量分给用户群体A
  • 将10%流量分给用户群体B

我们需要保证的是:一个用户再次请求进来,用户看到的结果是一样的

一般可以这样做:

  • 对 用 户 ID( 设 备 ID/CookieId/userId/openId) 取hash值,每 次 Hash 的 结 果 都 是 相 同的。
  • 直接取用户ID的某一位

2、性能测试-执行时间

2.1 简单统计方式
start/end = 
System.currentTimeMillis()
System.nanoTime()
new Date()
2.2 框架统计
  • commons-langs3
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Thread.sleep(1000);
stopWatch.stop();
  • guava
Stopwatch stopwatch = Stopwatch.createStarted();
stopwatch.stop();
stopwatch.elapsed(TimeUnit.MILLISECONDS)

3、单元测试

3.1 接口层测试

对于Java开发的开发,主要是Springboot的接口进行mock测试。
下面是单侧注解说明:

1、@RunWith: 
该注解标签是Junit提供的,用来说明此测试类的运行者,这里用了SpringRunner,
它实际上继承了 SpringJUnit4ClassRunner类,而 SpringJUnit4ClassRunner
这个类是一个针对Junit 运行环境的自定义扩展,用来标准化在Springboot环境下Junit4.x的测试用例
@RunWith就是一个运行器
@RunWith(JUnit4.class)就是指用JUnit4来运行
@RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境
@RunWith(Suite.class)的话就是一套测试集合
@RunWith(Parameterized.class)


2、@SpringBootTest:
为springApplication创建上下文并支持SpringBoot特性
@SpringBootTest的webEnvironment属性定义运行环境:
Mock(默认): 
此值为默认值,该类型提供一个mock环境,可以和@AutoConfigureMockMvc或@AutoConfigureWebTestClient搭配使用,开启Mock相关的功能。注意此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web服务端口。
RANDOM_PORT: 
加载WebServerApplicationContext 并提供真实的web环境,嵌入式服务器,监听端口是随机的
DEFINED_PORT: 
加载WebServerApplicationContext并提供真实的Web环境,嵌入式服务器启动并监听定义的端口(来自 application.properties或默认端口 8080)
NONE: 启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务。

写单元测试,难免需要操作数据库。有时候单元测试的数据库跟开发时候的数据库是同一个,为了不影响数据库的数据,需要在单测完成之后,将操作回滚。这在springboot中也是很容易解决的事情,只需要将单测类继承AbstractTransactionalJUnit4SpringContextTests即可。

3.1.1 模拟环境进行测试(不启动服务器)
  • @AutoConfigureMockMvc:
    该注解表示启动测试的时候自动注入 MockMvc,而这个MockMvc有以下几个基本的方法:
  • perform : 执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理。
  • andExpect: 添加RequsetMatcher验证规则,验证控制器执行完成后结果是否正确
  • andDo: 添加ResultHandler结果处理器,比如调试时打印结果到控制台
  • andReturn: 最后返回相应的MvcResult,然后进行自定义验证/进行下一步的异步处理
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void userMapping() throws Exception {
        String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
        mockMvc.perform(request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }
    
    @Test
	public void TestUpgradeApp() throws Exception{
		RequestBuilder request = null;
		request = get("/appProducer/upgradeApp")
				.param("appId", "1001"); 
		mockMvc.perform(request) 
		        .andExpect(status().isOk())//返回HTTP状态为200
		        .andDo(print());//打印结果
		
	}

}
3.1.2 真实Web环境进行测试(启动一个Spring应用程序上下文)
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest3 {

    @Autowired
    private TestRestTemplate testRestTemplate;
    
    @Test
    public void userMapping() throws Exception {
        User user = new User();
        user.setUsername("pj_pj");
        user.setPassword("123456");
        ResponseEntity<String> responseEntity = 
        testRestTemplate.postForEntity("/user", user, String.class);
        System.out.println("Result: "+responseEntity.getBody());
        System.out.println("状态码: "+responseEntity.getStatusCodeValue());
    }
}
3.1.3 仅Web层启动

将启动完整的Spring应用程序上下文,但没有服务器,测试范围缩小到仅Web层

@WebMvcTest(GreetingController.class)
public class WebMockTest {

	@Autowired
	private MockMvc mockMvc;

	@MockBean
	private GreetingService service;

	@Test
	public void greetingShouldReturnMessageFromService() throws Exception {
		when(service.greet()).thenReturn("Hello, Mock");
		this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, Mock")));
	}
}

补充:SpringBoot Test及注解详解

3.2 业务层测试

3.2.1 Junit
  • 常用注解
 @BeforeClass
 @Before
 @Test
 @Test(expected=xxxException.class)   断言该方法会抛出异常
 @Test(timeout=1000)                  执行时间超过设置的值该案例会失败
 

@RunWith(JUnit4.class)                默认运行器
@Suite.SuiteClasses({ CalculatorTest.class,SquareTest.class})

JUnit测试套件:
@RunWith(Suite.class)                 
测试集运行器配合使用测试集功能
如果您有多个测试类,那么可以将它们组合成一个测试套件。
运行测试套件将以指定顺序执行该套件中的所有测试类。
一个测试套件也可以包含其他测试套件。
  • 测试实例
@RunWith(Parameterized.class)         参数化运行器
public class PrimeFactorTest {
    private PrimeFactor primeFactor;
    private int input;
    private List<Integer> expected;
    //构造函数
    public PrimeFactorTest(int input, List<Integer> expected) {
        this.input = input;
        this.expected = expected;
    }
    
    @Parameterized.Parameters
    public static Collection init() {
        return Arrays.asList(new Object[][]{
             {18, Arrays.asList(2, 3, 3)}
        });
    }
    
    @Test
    public void testFactor_when_input_18_then_must_return_2_3_3() {
        Assert.assertEquals(expected, primeFactor.factor(input));
    }
}

使用JUnit Rules:
您可以为测试类中的每个测试添加行为。您可以使用@Rule注解来标注TestRule类型的字段。
3.2.2 AssertThat结果验证
常用的断言方法如下:
assertEquals(a, b)    测试a是否等于b(a和b是原始类型数值(primitive value)或者必须为实现比较而具有equal方法)

assertFalse(a)        测试a是否为false(假),a是一个Boolean数值。

assertTrue(a)         测试a是否为true(真),a是一个Boolean数值

assertNotNull(a)      测试a是否非空,a是一个对象或者null。

assertNull(a)         测试a是否为null,a是一个对象或者null。

assertNotSame(a, b)   测试a和b是否没有都引用同一个对象。

assertSame(a, b)      测试a和b是否都引用同一个对象。

fail(string)          Fail让测试失败,并给出指定信息。

assertThat(expected, Matcher)  通过Matcher断言

assertThat替代assertion

Assertions
ReflectionTestUtils

补充: 流式断言器AssertJ介绍

3.2.3 Mockit
   //快速创建Mock对象
    @Mock
    private List mockList;

    // 验证行为
    @Test
    public void verify_behaviour(){
    	// 模拟List 的一个对象  
        List mock = mock(List.class);
        mock.add(1);
        mock.clear();
        verify(mock).add(2);
        verify(mock).clear();
    }

    @Test
    public void verify_behaviour2(){
        mockList.add(1);
        // 均验证add方法是否被调用1
        verify(mockList).add(1);
    }

    // 模拟期望结果
    @Test
    public void when_thenReturn() {
        Iterator iterator = mock(Iterator.class);
        // 模拟方法调用的返回值
        when(iterator.next()).thenReturn("hello");
        String result = iterator.next() + " " + iterator.next();
        assertEquals("hello world",result);
    }

    // 模拟方法体抛出异常
    @Test(expected = RuntimeException.class)
    public void doThrow_when(){
    	// 模拟获取第二个元素时,抛出RuntimeException  
	    when(mockedList.get(1)).thenThrow(new RuntimeException());
        List list = mock(List.class);
        如果一个函数没有返回值类型,那么可以使用此方法模拟异常抛出
        doThrow(new RuntimeException()).when(list).add(1);
        list.add(1);
    }


    // 参数匹配
    @Test
    public void with_arguments(){
        Comparable comparable = mock(Comparable.class);
        when(comparable.compareTo("Test")).thenReturn(1);
        when(comparable.compareTo("Omg")).thenReturn(2);
        assertEquals(1, comparable.compareTo("Test"));
        assertEquals(2, comparable.compareTo("Omg"));
        assertEquals(0, comparable.compareTo("Not stub"));
    }

    // 包装实际对象
    @Test
    public void spy_on_real_objects(){
        List list = new LinkedList();
        List spy = Mockito.spy(list);

        // 对监控对象的行为回调判断
        // 每次调用都委托给实际对象
        // when(spy.get(0)).thenReturn(100);
        doReturn(100).when(spy).get(0);

        Assert.assertEquals(100,spy.get(0));
    }
    
    
    List list = new LinkedList();
    List spy = spy(list);
    //optionally, you can stub out some methods:
    when(spy.size()).thenReturn(100);
    //using the spy calls *real* methods
    spy.add("one");
    spy.add("two");
    //prints "one" - the first element of a list
    System.out.println(spy.get(0));
    //size() method was stubbed - 100 is printed
    System.out.println(spy.size());
    //optionally, you can verify
    verify(spy).add("one");
    verify(spy).add("two");
    
    Foo mock = mock(Foo.class);
    //Be sure the real implementation is 'safe'.
    //If real implementation throws exceptions or
    depends on specific state of the object then you're in trouble.
    when(mock.someMethod()).thenCallRealMethod();
3.2.4 JMockit
   @Mocked
    Calendar cal;

    @Test
    public void testRecordOutside() {
        new Expectations() {
            {
                cal.get(Calendar.YEAR);
                result = 2016;
                cal.get(Calendar.HOUR_OF_DAY);
                result = 7;
            }
        };
        Assert.assertTrue(cal.get(Calendar.YEAR) == 2016);
        Assert.assertTrue(cal.get(Calendar.HOUR_OF_DAY) == 7);
        Assert.assertTrue(cal.get(Calendar.DAY_OF_MONTH) == 0);
        new Verifications() {
            {
                cal.get(Calendar.YEAR);
                times = 1;
            }
        };
    }

JMockit教程
使用 JaCoCo 生成代码覆盖率报告

4、测试流程

  • 流程
1.数据准备
2.执行测试
3.验证结果
4.数据清理
	mysql: 使用@Transactional注解回滚数据
	redis: 测试方法的最后执行清理
  • 分层测试
dao:h2
	
service: mockit+juint
	
controller: mvcTest
	
mock:
	controller 层主要测试参数校验逻辑,测试的时候并不需要真正的调用 service 层服务,
	可以通过 mock 框架将 service 方法 mock 掉
	
	service 层主要测试业务逻辑是否正确,当 service 层发生调用外部服务的时候,
	需要 mock 掉外部服务的调用代码,避免单元测试的时候调用外部服务,导致外部服务异常。
	同时单元测试不应该依赖外部服务
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值