1. 程式人生 > >單元測試(junit+dubbo+mockito)

單元測試(junit+dubbo+mockito)

關於單元測試請先回憶下面幾個問題:

1)單元測試是否依賴網路?如果依賴網路,當沒有網的時候怎麼辦?

2)單元測試是否支援多次可重複執行?

3)dubbo介面怎麼單元測試?

4)如何計算單元測試對程式碼的覆蓋率?

在回答上面幾個問題前請先看下面介紹:

1、準備測試環境

1)引入依賴包:

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>1.10.19</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>4.3.10.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.8.4</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.197</version>
            <scope>test</scope>
        </dependency>

2)服務啟動時載入sql指令碼配置

	<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
		<jdbc:script location="classpath:data/init-table.sql" />
		<jdbc:script location="classpath:data/import-data.sql" />
	</jdbc:initialize-database>

3)sql指令碼

init-table.sql


SET MODE MYSQL;

drop table if exists `user_1`;
create table `user_1` (
  `id`     int(10) not null primary key AUTO_INCREMENT ,
  `name`   varchar(20) not null default '' ,
  `status` int(11) not null DEFAULT 0
);
import-data.sql
SET MODE MYSQL;

insert into `user_1`(`id`,`name`,`status`)values(100,'test1',0);

4)準備單元測試基類,所有測試類繼承該基類。

@RunWith(SpringJUnit4ClassRunner.class)  //使用junit4進行測試
@ContextConfiguration({"classpath:spring/spring-config.xml"}) //載入配置檔案
public class CommonTest {

    @Test
    public void test(){

    }
}

spring-config.xml對應web.xml中的配置

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath*:/spring/spring-config.xml;
		</param-value>
	</context-param>

2、使用Junit進行常規單元測試

需要測試的service,包含增、刪、改、查等介面。

@Service
public class UserService {

    public static final String ERROR_MSG = "this is test exception!";

    @Resource
    private UserDao userDao;

    public int saveUser(User user) {
        return userDao.saveUser(user);
    }

    public User queryUserById(int id) {
        return userDao.queryUserById(id);
    }

    public int updateUserById(User user) {
        Preconditions.checkNotNull(user, "使用者資訊不能為空!");
        return userDao.updateUserById(user);
    }

    public int deleteUserById(int id) {
        Preconditions.checkArgument(id > 0, "id(%s)必須大於0!", id);
        return userDao.deleteUserById(id);
    }

    public void testException() {
        throw new IllegalStateException(ERROR_MSG);
    }
}

單元測試類(繼承上面的基類CommonTest.java)注入Service使用@Resource或@Autowired

//繼承單元測試基類CommonTest.java
public class UserServiceTest extends CommonTest {

    @Resource //注入Service使用@Resource或@Autowired
    private UserService userService;

    /**
     * 建立測試模型
     */
    private User createUser() {
        Random random = new Random(1000000);
        User user = new User();
        user.setStatus(0);
        user.setName("new name_" + random.nextInt());
        return user;
    }

    @Test
    public void saveUser() {
        User user = createUser();
        // 儲存資料
        userService.saveUser(user);
        Assert.assertTrue(user.getId() > 0);

        // 校驗儲存的資料與查詢出來的資料是否一致
        User temp = userService.queryUserById(user.getId());
        Assert.assertNotNull(temp);
        Assert.assertNotNull(temp.getName());
        Assert.assertEquals(user.getName(), temp.getName());
        Assert.assertEquals(user.getStatus(), temp.getStatus());
    }

    @Test
    public void queryUserById() {
        // 檢查初始化指令碼載入資料是否正確
        User user = userService.queryUserById(100);
        Assert.assertNotNull(user);
        Assert.assertNotNull(user.getName());
        Assert.assertEquals("test1", user.getName());
    }

    @Test
    public void updateUserById() {
        User user = createUser();
        // 儲存資料
        userService.saveUser(user);
        int id = user.getId();
        Assert.assertTrue(id > 0);

        // 校驗儲存的資料與查詢出來的資料是否一致
        User temp = userService.queryUserById(id);
        Assert.assertNotNull(temp);
        Assert.assertNotNull(temp.getName());
        Assert.assertEquals(user.getName(), temp.getName());
        Assert.assertEquals(user.getStatus(), temp.getStatus());
        // 修改資訊+更新資料
        user.setStatus(1000);
        user.setName("new-name" + new Random(1000000).nextInt());
        int updateResult = userService.updateUserById(user);
        Assert.assertEquals(1, updateResult);

        // 校驗更新資料是否成功
        temp = userService.queryUserById(id);
        Assert.assertNotNull(temp);
        Assert.assertNotNull(temp.getName());
        Assert.assertEquals(user.getName(), temp.getName());
        Assert.assertEquals(user.getStatus(), temp.getStatus());
    }

    @Test
    public void deleteUserById() {
        // 檢驗資料庫是否存在資料
        User user = userService.queryUserById(100);
        Assert.assertNotNull(user);
        Assert.assertNotNull(user.getName());
        Assert.assertEquals("test1", user.getName());

        // 校驗刪除
        int result = userService.deleteUserById(100);
        Assert.assertEquals(1, result);
    }
}

增、刪、改、查主流程測試要點:

查詢:初始化資料庫表結構時匯入資料,校驗查詢功能是否正常

新增:新增後需要再從資料查詢,確保儲存的資料與儲存後查詢的資料一致

修改:

1)先儲存資料;

2)再根據儲存的資料返回的主鍵查詢資料,確認可在成功;

3)然後修改資料,從資料庫獲取資料,與修改的資料是否一致

刪除:先校驗資料是否存在,如果存在,再校驗刪除功能是否正常

3、Mockito工具介紹:

public class MockitoTest extends CommonTest {

    @InjectMocks    // mock注入說明:會注入變數
    @Resource
    private UserService userService;

    @Mock           // mock一個例項,注入到上面宣告的 UserService 中
    @Resource
    private UserDao userDao;

    @Rule           // 對異常程式碼進行mock宣告
    public ExpectedException thrown = ExpectedException.none();

    @Before
    public void setUp() {
        // mock註解宣告及初始化
        MockitoAnnotations.initMocks(this);

        // 針對 @Mock 類中的方法進行定製:當呼叫該介面時返回固定值
        when(userDao.saveUser(any(User.class))).thenReturn(1314);
        when(userDao.updateUserById(any(User.class))).thenReturn(111);
    }

    @Test
    public void testMock() {
        // 呼叫前面指定的mock介面
        int result = userService.saveUser(new User());
        // 檢驗返回的固定值
        Assert.assertEquals(1314, result);

        // 呼叫前面指定的mock介面
        result = userService.updateUserById(new User());
        // 檢驗返回的固定值
        Assert.assertEquals(111, result);
    }

    @Test
    public void testThrown() {
        // 檢驗異常類
        thrown.expect(IllegalStateException.class);
        // 檢驗異常資訊
        thrown.expectMessage(UserService.ERROR_MSG);
        // 開始執行方法
        userService.testException();
    }
}

要點介紹:

1)mock service時使用 @InjectMocks

2)mock上面servicek中的變數時,使用 @Mock

3)mock異常時使用 (需要校驗異常類及異常資訊)

@Rule           // 對異常程式碼進行mock宣告
public ExpectedException thrown = ExpectedException.none();

4)@Before中初始化:MockitoAnnotations.initMocks(this);

5)mock類中的方法時使用when(介面名(引數名)).thenRerutn(返回值),樣例如下:

when(userDao.saveUser(any(User.class))).thenReturn(1314);

4、dubbo介面mock介紹

1)在test/resource/*中配置dubbo

<dubbo:application name="dubbo-application" owner="yaohua.liu" organization="xx"/>
<dubbo:consumer timeout="60000" retries="0"/>
<!-- mock專用 -->
<dubbo:reference interface="com.example.liuyaohua.service.DubboService" mock="com.example.liuyaohua.dubbo.DubboServiceImpl"/>

配置中對應的實現類

@Service
public class DubboServiceImpl implements DubboService {
    @Override
    public String sayHello(int id) {
        if(id == 1){
            return "123456";
        }else {
            return "000000";
        }
    }
}

dubbo單元測試類(繼承基類CommonTest.java)

public class DubboTest extends CommonTest {

    @Resource
    private DubboService dubboService;

    @Test
    public void testDubbo(){
        String hello = dubboService.sayHello(1);
        Assert.assertNotNull(hello);
        Assert.assertEquals(hello,"123456");

        hello = dubboService.sayHello(123);
        Assert.assertNotNull(hello);
        Assert.assertEquals(hello,"000000");
    }
}

要點介紹:

1)需要配置dubbo配置資訊,覆蓋正常配置。

2)可實現並自定義實現類

3)正常呼叫dubbo介面時,返回的結果是上面自定義實現類中返回值

總結:

1)單元測試不依賴網路:dubbo方法、http呼叫介面使用Mock;jdbc使用記憶體資料庫(H2)

2)單元測試需要支援多次可重複執行:服務每次啟動時都會載入sql指令碼(重新初始化表結構及匯入原始資料),保證資料在每次執行都是同樣的狀態

3)dubbo介面mock測試:使用本地偽裝

4)如何計算單元測試對程式碼的覆蓋率:使用sonar,詳細介紹待續

本文已同步更新到公眾號,溝通交流請關注公眾號。