Java测试套件 - Junit与Mockito

作者: 阿武_Accat | 来源:发表于2018-09-12 20:55 被阅读34次

    说明

            在第一份工作中,我经历了我的第一个商用的项目,是一个微服务模块(后台管理模块),我所在的团队做的是南方电网的项目。不扯远了,当时我开始做的时候在想要以什么方式开发时,我的组长因担心项目到期出不了成品,所以催促我快点产出代码。之后就有了这篇文章,由于我为了赶进度而没有做单元测试,这导致了项目的后半部分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 未完待续

    相关文章

      网友评论

        本文标题:Java测试套件 - Junit与Mockito

        本文链接:https://www.haomeiwen.com/subject/yxtjgftx.html