Java测试套件 - Junit与Mockito

说明

        在第一份工作中,我经历了我的第一个商用的项目,是一个微服务模块(后台管理模块),我所在的团队做的是南方电网的项目。不扯远了,当时我开始做的时候在想要以什么方式开发时,我的组长因担心项目到期出不了成品,所以催促我快点产出代码。之后就有了这篇文章,由于我为了赶进度而没有做单元测试,这导致了项目的后半部分bug产出数量随着代码的增加而增加,更郁闷地是修改之后为了配合前端都得去服务器部署一次,浪费的时间不比写单元测试少,也让我明白了单元测试的重要性。我的要求是让自己学会TDD测试驱动开发的开发习惯,且写下该文记录自己学习的历程。

        该文将介绍单元测试框架Junit,模仿对象的框架Mockito。文章介绍核心的概念,感兴趣可以进入参考文章或者书籍进一步阅读。

参考文章以及书籍

《测试驱动开发》
Hamcrest 总结
深入JUnit源码之Rule
JUnit4.11 理论机制 @Theory 完整解读
Junit4.8之Category
Mockito官方文档中文版
Junit测试Controller-RESTful接口
IDEA代码覆盖率测试

测试驱动开发-TDD

        也有人把这种流程称为极限编程。

        测试驱动开发是使用测试框架的目的,能更好更快地让我们写完代码然后傲游二次元或者是出门做个现充。

碧海之蓝 -- 羞耻的生活喜剧番

        测试驱动开发指的是就是字面上的意思,用测试来驱动整个开发流程,也就是在写开发代码前先提前写测试代码,然后写业务代码来让测试通过。这里只是简单地概念性介绍,主要是要避免“开发后简单地验证结果”而转为“主动编写测试用例然后编写代码使用例通过”

整个流程以就是不断重复下面几步:

|--->快速创建一个测试
       |--->运行所有测试,发现新测试无法通过
              |--->做一些细微的调整
                     |--->运行所有测试,所有测试通过
                           |--->重构代码,消除重复,优化代码结构

Junit

JUnit is a simple framework to write repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.

Junit是一个简单的测试框架,其官网涵盖大量的用例可以作为使用参考。/home/harry

Junit 4 官网的用例

Assertions-断言机制

        断言机制是判断结果是否正确的机制,总是以“assertXXXX”格式出现,只要有一条验证结果与预想不匹配,则测试不通过。
        Hamcrest提供了其他的一些断言方式,请查看Hamcrest 总结

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;

import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;

public class AssertTests {
  @Test
  public void testAssertArrayEquals() {
    byte[] expected = "trial".getBytes();
    byte[] actual = "trial".getBytes();
    assertArrayEquals("failure - byte arrays not same", expected, actual);
  }

  @Test
  public void testAssertEquals() {
    assertEquals("failure - strings are not equal", "text", "text");
  }

  @Test
  public void testAssertFalse() {
    assertFalse("failure - should be false", false);
  }

  @Test
  public void testAssertNotNull() {
    assertNotNull("should not be null", new Object());
  }

  @Test
  public void testAssertNotSame() {
    assertNotSame("should not be same Object", new Object(), new Object());
  }

  @Test
  public void testAssertNull() {
    assertNull("should be null", null);
  }

  @Test
  public void testAssertSame() {
    Integer aNumber = Integer.valueOf(768);
    assertSame("should be same", aNumber, aNumber);
  }

  // JUnit Matchers assertThat
  @Test
  public void testAssertThatBothContainsString() {
    assertThat("albumen", both(containsString("a")).and(containsString("b")));
  }

  @Test
  public void testAssertThatHasItems() {
    assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
  }

  @Test
  public void testAssertThatEveryItemContainsString() {
    assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
  }

  // Core Hamcrest Matchers with assertThat
  @Test
  public void testAssertThatHamcrestCoreMatchers() {
    assertThat("good", allOf(equalTo("good"), startsWith("good")));
    assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
    assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
    assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
    assertThat(new Object(), not(sameInstance(new Object())));
  }

  @Test
  public void testAssertTrue() {
    assertTrue("failure - should be true", true);
  }
}

Runner-测试执行器

        你写的测试代码都会在这些测试执行器中运行,Junit默认的执行器为BlockJUnit4ClassRunner,当你不指定时,就会使用这个类来运行测试。可以使用注解@RunWith(xxxx)来指定运行测试的执行器。

Suite组合测试

        Suite测试在Junit中是测试一组测试用例,用途比较明确,就是有些操作是需要执行多个操作才能完成的,所以可以组成一个组来进行测试。

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
  TestFeatureLogin.class,
  TestFeatureLogout.class,
  TestFeatureNavigate.class,
  TestFeatureUpdate.class
})

public class FeatureTestSuite {
  // the class remains empty,
  // used only as a holder for the above annotations
}

指定期待异常

        如果没有抛出指定异常则测试不通过。

@Test(expected = IndexOutOfBoundsException.class) 
public void empty() { 
     new ArrayList<Object>().get(0); 
}

忽略测试

        可以让测试暂时失效。

@Ignore("Test is ignored as a demonstration")
@Test
public void testSame() {
    assertThat(1, is(1));
}

测试失效时间

@Test(timeout=1000)
public void testWithTimeout() {
  ...
}

Rule机制

JUnit中的Rule是对@BeforeClass、@AfterClass、@Before、@After等注解的另一种实现,其中@ClassRule实现的功能和@BeforeClass、@AfterClass类似;@Rule实现的功能和@Before、@after类似。JUnit引入@ClassRule@Rule注解的关键是想让以前在@BeforeClass、@AfterClass、@Before、@After中的逻辑能更加方便的实现重用,因为@BeforeClass、@AfterClass、@Before、@After是将逻辑封装在一个测试类的方法中的,如果实现重用,需要自己将这些逻辑提取到一个单独的类中,再在这些方法中调用,而@ClassRule、@Rule则是将逻辑封装在一个类中,当需要使用时,直接赋值即可,对不需要重用的逻辑则可用匿名类实现,也因此,JUnit在接下来的版本中更倾向于多用@ClassRule和@Rule。

        Junit @Rule和@ClassRule只能注解在字段上,且该字段必须实现TestRule接口,Junit提供了一些默认实现类。

深入JUnit源码之Rule类图 - 来源于博文《深入JUnit源码之Rule》
public class DigitalAssetManagerTest {
  @Rule
  public final TemporaryFolder tempFolder = new TemporaryFolder();

  @Rule
  public final ExpectedException exception = ExpectedException.none();

  @Test
  public void countsAssets() throws IOException {
    File icon = tempFolder.newFile("icon.png");
    File assets = tempFolder.newFolder("assets");
    createAssets(assets, 3);

    DigitalAssetManager dam = new DigitalAssetManager(icon, assets);
    assertEquals(3, dam.getAssetCount());
  }

  private void createAssets(File assets, int numberOfAssets) throws IOException {
    for (int index = 0; index < numberOfAssets; index++) {
      File asset = new File(assets, String.format("asset-%d.mpg", index));
      Assert.assertTrue("Asset couldn't be created.", asset.createNewFile());
    }
  }

  @Test
  public void throwsIllegalArgumentExceptionIfIconIsNull() {
    exception.expect(IllegalArgumentException.class);
    exception.expectMessage("Icon is null, not a file, or doesn't exist.");
    new DigitalAssetManager(null, null);
  }
}

Theory机制

        Theory是一个自动化填充参数并进行多次参数测试的一个机制,可以自己实现注解来定义参数。下面的代码中,定义了两个实参,而当测试 filenameIncludesUsername 方法时,形参username会被两个实参填充并各执行一次。

@RunWith(Theories.class)
public class UserTest {
    @DataPoint
    public static String GOOD_USERNAME = "optimus";
    @DataPoint
    public static String USERNAME_WITH_SLASH = "optimus/prime";

    @Theory
    public void filenameIncludesUsername(String username) {
        assumeThat(username, not(containsString("/")));
        assertThat(new User(username).configFileName(), containsString(username));
    }
}

Test Fixture 测试固件

The test fixture is everything we need to have in place to exercise the SUT

Test Fixture(测试固件)是指一个测试运行所需的固定环境。

Fixtures 是测试中非常重要的一部分。他们的主要目的是建立一个固定/已知的环境状态以确保 测试可重复并且按照预期方式运行。Junit提供了一些方法来设置fixture,可以用来设置测试方法所需的环境数据等,允许你精确的定义你的Fixtures。大致上分为三类:
Test Fixtures
规则(Rules&RulesClass)
Theories

大致包含了如下过程

@BeforeClass setUpClass
@Before setUp
@Test test2()
@After tearDown
@Before setUp
@Test test1()
@After tearDown
@AfterClass tearDownClass

Category机制

一种级联的测试方式,
@IncludeCategory(XXX.class)可以包含@Category(XXX.class)
@ExcludeCategory(XXX.class)将会忽略@Category(XXX.class)
细节参照博文

Mock和Mockito

        mock其实是一种工具的简称,他最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。

对象间存在依赖关系
使用Mock来模仿对象,消除耦合关系

下面的代码来自官网示例

1. 验证某些行为

 // 静态导入会使代码更简洁
 import static org.mockito.Mockito.*;

 // mock creation 创建mock对象
 List mockedList = mock(List.class);

 //using mock object 使用mock对象
 mockedList.add("one");
 mockedList.clear();

 //verification 验证
 verify(mockedList).add("one");
 verify(mockedList).clear();

2. 制作测试桩

 //You can mock concrete classes, not only interfaces
 // 你可以mock具体的类型,不仅只是接口
 LinkedList mockedList = mock(LinkedList.class);

 //stubbing
 // 测试桩
 when(mockedList.get(0)).thenReturn("first");
 when(mockedList.get(1)).thenThrow(new RuntimeException());

 //following prints "first"
 // 输出“first”
 System.out.println(mockedList.get(0));

 //following throws runtime exception
 // 抛出异常
 System.out.println(mockedList.get(1));

 //following prints "null" because get(999) was not stubbed
 // 因为get(999) 没有打桩,因此输出null
 System.out.println(mockedList.get(999));

 //Although it is possible to verify a stubbed invocation, usually it's just redundant
 //If your code cares what get(0) returns then something else breaks (often before even verify() gets executed).
 //If your code doesn't care what get(0) returns then it should not be stubbed. Not convinced? See here.
 // 验证get(0)被调用的次数
 verify(mockedList).get(0);

        默认情况下,所有的函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean。

3. 参数匹配器(ArgumentMatchers)

//stubbing using built-in anyInt() argument matcher
 // 使用内置的anyInt()参数匹配器
 when(mockedList.get(anyInt())).thenReturn("element");

 //stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
 // 使用自定义的参数匹配器( 在isValid()函数中返回你自己的匹配器实现 )
 when(mockedList.contains(argThat(isValid()))).thenReturn("element");

 //following prints "element"
 // 输出element
 System.out.println(mockedList.get(999));

 //you can also verify using an argument matcher
 // 你也可以验证参数匹配器
 verify(mockedList).get(anyInt());

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//above is correct - eq() is also an argument matcher
// 上述代码是正确的,因为eq()也是一个参数匹配器
// 该示例展示了如何多次应用于测试桩函数的验证

        参数匹配器使验证和测试桩变得更灵活,参考API文档

4. 验证函数的确切、最少、从未调用次数

 //using mock
 mockedList.add("once");

 mockedList.add("twice");
 mockedList.add("twice");

 mockedList.add("three times");
 mockedList.add("three times");
 mockedList.add("three times");

 //following two verifications work exactly the same - times(1) is used by default
 // 下面的两个验证函数效果一样,因为verify默认验证的就是times(1)
 verify(mockedList).add("once");
 verify(mockedList, times(1)).add("once");

 //exact number of invocations verification
 // 验证具体的执行次数
 verify(mockedList, times(2)).add("twice");
 verify(mockedList, times(3)).add("three times");

 //verification using never(). never() is an alias to times(0)
 // 使用never()进行验证,never相当于times(0)
 verify(mockedList, never()).add("never happened");

 //verification using atLeast()/atMost()
 // 使用atLeast()/atMost()
 verify(mockedList, atLeastOnce()).add("three times");
 verify(mockedList, atLeast(2)).add("five times");
 verify(mockedList, atMost(5)).add("three times");

5. 使用stub抛出异常

doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:
// 调用这句代码会抛出异常
mockedList.clear();

6. 验证执行顺序

 // A. Single mock whose methods must be invoked in a particular order
 // A. 验证mock一个对象的函数执行顺序
 List singleMock = mock(List.class);

 //using a single mock
 singleMock.add("was added first");
 singleMock.add("was added second");

 //create an inOrder verifier for a single mock
 // 为该mock对象创建一个inOrder对象
 InOrder inOrder = inOrder(singleMock);

 //following will make sure that add is first called with "was added first, then with "was added second"
 // 确保add函数首先执行的是add("was added first"),然后才是add("was added second")
 inOrder.verify(singleMock).add("was added first");
 inOrder.verify(singleMock).add("was added second");

 // B. Multiple mocks that must be used in a particular order
 // B .验证多个mock对象的函数执行顺序
 List firstMock = mock(List.class);
 List secondMock = mock(List.class);

 //using mocks
 firstMock.add("was called first");
 secondMock.add("was called second");

 //create inOrder object passing any mocks that need to be verified in order
 // 为这两个Mock对象创建inOrder对象
 InOrder inOrder = inOrder(firstMock, secondMock);

 //following will make sure that firstMock was called before secondMock
 // 验证它们的执行顺序
 inOrder.verify(firstMock).add("was called first");
 inOrder.verify(secondMock).add("was called second");

 // Oh, and A + B can be mixed together at will

7. 确保交互(interaction)操作不会执行在mock对象上

 //using mocks - only mockOne is interacted
 // 使用Mock对象
 mockOne.add("one");

 //ordinary verification
 // 普通验证
 verify(mockOne).add("one");

 //verify that method was never called on a mock
 // 验证某个交互是否从未被执行
 verify(mockOne, never()).add("two");

 //verify that other mocks were not interacted
 // 验证mock对象没有交互过
 verifyZeroInteractions(mockTwo, mockThree);

8. 简化mock对象的创建

public class ArticleManagerTest {

   @Mock private ArticleCalculator calculator;
   @Mock private ArticleDatabase database;
   @Mock private UserProvider userProvider;

   private ArticleManager manager;
    // 需要执行下面语句来初始化注解的使用
   @Before  
   public void initMocks() {  
    MockitoAnnotations.initMocks(this);  
   }  

    //也可以在测试类加上注解@RunWith(MockitoJUnit44Runner.class)  
    //或者使用MockitoJUnitRunner

9. 为连续的调用做测试桩

 when(mock.someMethod("some arg"))
   .thenThrow(new RuntimeException())
   .thenReturn("foo");

 //First call: throws runtime exception:
 // 第一次调用 : 抛出运行时异常
 mock.someMethod("some arg");

 //Second call: prints "foo"
 // 第二次调用 : 输出"foo"
 System.out.println(mock.someMethod("some arg"));

 //Any consecutive call: prints "foo" as well (last stubbing wins).
 // 后续调用 : 也是输出"foo"
 System.out.println(mock.someMethod("some arg"));

// 简短的写法,第一次调用时返回"one",第二次返回"two",第三次返回"three"
 when(mock.someMethod("some arg"))
   .thenReturn("one", "two", "three");

10. 为回调做测试桩

Allows stubbing with generic Answer interface. 运行为泛型接口Answer打桩。

 when(mock.someMethod(anyString())).thenAnswer(new Answer() {
     Object answer(InvocationOnMock invocation) {
         Object[] args = invocation.getArguments();
         Object mock = invocation.getMock();
         return "called with arguments: " + args;
     }
 });

 //Following prints "called with arguments: foo"
 // 输出 : "called with arguments: foo"
 System.out.println(mock.someMethod("foo"));

11. 监控真实对象

        你可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了。

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对象调用真实对象的函数
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
// 因为size()函数被打桩了,因此这里返回的是100
System.out.println(spy.size());

//optionally, you can verify
// 交互验证
verify(spy).add("one");
verify(spy).add("two");

12. 为下一步的断言捕获参数(ArgumentCaptor)

ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
// 参数捕获
verify(mock).doSomething(argument.capture());
// 使用equal断言
assertEquals("John", argument.getValue().getName());

13. TDD与BDD(行为驱动开发)结合使用

 import static org.mockito.BDDMockito.*;

 Seller seller = mock(Seller.class);
 Shop shop = new Shop(seller);

 public void shouldBuyBread() throws Exception {

   //given
   given(seller.askForBread()).willReturn(new Bread());

   //when
   Goods goods = shop.buyBread();

   //then
   assertThat(goods, containBread());
 }

14. @Captor,@Spy,@ InjectMocks

@Captor 简化 ArgumentCaptor 的创建 - 当需要捕获的参数是一个令人讨厌的通用类,而且你想避免编译时警告。

@Spy - 你可以用它代替 spy(Object) 方法

@InjectMocks - 自动将模拟对象或侦查域注入到被测试对象中。需要注意的是 @InjectMocks 也能与 @Spy 一起使用,这就意味着 Mockito 会注入模拟对象到测试的部分测试中。它的复杂度也是你应该使用部分测试原因。

15. 验证超时

   //passes when someMethod() is called within given time span
   verify(mock, timeout(100)).someMethod();
   //above is an alias to:
   verify(mock, timeout(100).times(1)).someMethod();

   //passes when someMethod() is called *exactly* 2 times within given time span
   verify(mock, timeout(100).times(2)).someMethod();

   //passes when someMethod() is called *at least* 2 times within given time span
   verify(mock, timeout(100).atLeast(2)).someMethod();

   //verifies someMethod() within given time span using given verification mode
   //useful only if you have your own custom verification modes.
   verify(mock, new Timeout(100, yourOwnVerificationMode)).someMethod();

以上内容,足以让我们使用Junit 4进行日常代码的单元测试

使用Junit + Mockito 对Service做单元测试


public interface RegisterUserService {  
    boolean insert(String passid,String msisdn,String email) throws SQLException;
}   

@Service("registerUserService")
public class RegisterUserServiceImpl implements RegisterUserService {
 
    private Logger loggor = Logger.getLogger(getClass());
 
    
 
    @Autowired
    private UserMapper userMapper;
 
    @Autowired
    private PassidUserMapper passidUserMapper;
 
    @Autowired
    @Qualifier("redisService")
    private CacheService redisService;
 
    
 
    @Override
    @Transactional
    public boolean insert(String passid, String msisdn, String email)
            throws SQLException {
        if (StringUtils.isEmpty(passid))
            return false;
 
        User user = new User();
        if (!StringUtils.isEmpty(msisdn))
            user.setPhoneNo(msisdn);
        if (!StringUtils.isEmpty(email))
            user.setEmail(email);
 
        PassidUser passidUser = new PassidUser();
        String serverCode = ServerCodeConfig.serverCodeMap
                .get(PayUtil.ipAddress);
        if (StringUtils.isEmpty(serverCode)) {
            serverCode = "999";
        }
        String userid = serverCode + UIDUtil.next();
        passidUser.setUserid(userid);
        passidUser.setPassid(passid);
 
        user.setPassid(passid);
        user.setUserid(userid);
        Date date = new Date();
        user.setCreateTime(date);
        user.setUpdateTime(date);
        user.setDeleteFlag(0);
 
        /*if(loggor.isInfoEnabled()){
            loggor.info("passid:" + passid + "  userid:" + userid + "  msisdn:"
                    + msisdn + "  email:" + email);
        }*/
 
        int result = passidUserMapper.insert(passidUser);
 
        if (passidUserMapper.insert(passidUser) > 0
                && userMapper.insertSelective(user) > 0)
            redisService.set("passid:" + passid + ":userid", userid);
        else
            throw new SQLException("数据插入失败,数据回滚");
 
        return true;
    }
 
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("file:src/main/resources/conf/springConfig.xml")
public class RegisterUserServiceImplTest {
 
    @InjectMocks
    private RegisterUserService registerUserService = new RegisterUserServiceImpl();
 
    @Mock
    private UserMapper userMapper;
 
    @Mock
    private PassidUserMapper passidUserMapper;
 
    @Mock
    private CacheService redisService;
 
    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        when(passidUserMapper.insert(any(PassidUser.class))).thenReturn(1);
        when(userMapper.insertSelective(any(User.class))).thenReturn(1);
    }
 
    @Test
    public void testInsert() throws Exception {
        String passid = "12344";
        String msisdn = "18867131210";
        String email = "test@test.cn";
        Assert.assertTrue(registerUserService.insert(passid, msisdn, email));
 
    }
}

使用SpringMVC,Junit, Mockito测试Controller(Restful接口)

<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-context</artifactId>  
    <version>${spring.version}</version>  
</dependency>  
  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-webmvc</artifactId>  
    <version>${spring.version}</version>  
</dependency>
<dependency>  
    <groupId>junit</groupId>  
    <artifactId>junit</artifactId>  
    <version>${junit.version}</version>  
    <scope>test</scope>  
</dependency>  
  
<dependency>  
    <groupId>org.hamcrest</groupId>  
    <artifactId>hamcrest-core</artifactId>  
    <version>${hamcrest.core.version}/version>  
    <scope>test</scope>  
</dependency>  
<dependency>  
    <groupId>org.mockito</groupId>  
    <artifactId>mockito-core</artifactId>  
    <version>${mockito.core.version}</version>  
    <scope>test</scope>  
</dependency>  
  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-test</artifactId>  
    <version>${spring.version}</version>  
    <scope>test</scope>  
</dependency>  
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
* Created by zhengcanrui on 16/8/11.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:spring/applicationContext-*xml"})

//配置事务的回滚,对数据库的增删改都会回滚,便于测试用例的循环利用
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional

@WebAppConfiguration
public class Test {
   //记得配置log4j.properties ,的命令行输出水平是debug
   protected Log logger= LogFactory.getLog(TestBase.class);

   protected MockMvc mockMvc;

   @Autowired
   protected WebApplicationContext wac;

   @Before()  //这个方法在每个方法执行之前都会执行一遍
   public void setup() {
       mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();  //初始化MockMvc对象
   }

   @org.junit.Test
   public void getAllCategoryTest() throws Exception {
       String responseString = mockMvc.perform(
               get("/categories/getAllCategory")    //请求的url,请求的方法是get
                       .contentType(MediaType.APPLICATION_FORM_URLENCODED)  //数据的格式
               .param("pcode","root")         //添加参数
       ).andExpect(status().isOk())    //返回的状态是200
               .andDo(print())         //打印出请求和相应的内容
               .andReturn().getResponse().getContentAsString();   //将相应的数据转换为字符串
       System.out.println("--------返回的json = " + responseString);
   }

}

Spring MVC的测试往往看似比较复杂。其实他的不同在于,他需要一个ServletContext来模拟我们的请求和响应。
@webappconfiguration是一级注释,用于声明一个ApplicationContext集成测试加载WebApplicationContext。作用是模拟ServletContext。

@ContextConfiguration:因为controller,component等都是使用注解,需要注解指定spring的配置文件,扫描相应的配置,将类初始化等。

  • perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理;
  • get:声明发送一个get请求的方法。MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的。另外提供了其他的请求的方法,如:post、put、delete等。
  • param:添加request的参数,如上面发送请求的时候带上了了pcode = root的参数。假如使用需要发送json数据格式的时将不能使用这种方式,可见后面被@ResponseBody注解参数的解决方法
  • andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确(对返回的数据进行的判断);
  • andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断);
  • andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理(对返回的数据进行的判断);

写RESTful接口测试时需要注意返回的数据格式要标注成JSON : MediaType.APPLICATION_JSON

SoftInfo softInfo = new SoftInfo();
      //设置值
     ObjectMapper mapper = new ObjectMapper();
        ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
        java.lang.String requestJson = ow.writeValueAsString(softInfo);
        String responseString = mockMvc.perform( post("/softs").contentType(MediaType.APPLICATION_JSON).content(requestJson)).andDo(print())
                .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();

最后关于代码覆盖率

  • Intellij IDEA对这方面做了集成支持,右键运行测试代码


    右键测试代码选择覆盖率测试
  • 可以在包含有单元测试的代码中看到,绿色为已经覆盖的部分,红色未覆盖。


    绿已覆盖,红未覆盖

Junit 5 未完待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容