單元測試(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,詳細介紹待續
本文已同步更新到公眾號,溝通交流請關注公眾號。