淺談高效撰寫單元測試
- 以 Java為例
By Zen
Aug 2017
Zen 陳嘉豪
● I am from Macau
● 現職 TenMax AdTech Lab 騰學廣告科技
● 第一份工作是Android開發,後投身到web 全端
打雜,現專注於後端開發與測試
● 受 odd-e Daniel、Joseph、Jackson 所啟發,
現在致力於幫助團隊打造更順暢、更貼近成年人
的開發環境
● 新任 TenMax TDD 傳教士
● 寫code以外的興趣: 旅行,跳舞, Capoeira,腳
踏車
2017
Unit Test
主軸
Why test
What to test
地雷誤區
Valid Test
Effective Test
有想過/被問過以下問題嗎?
● 我寫的 Test case 夠嗎?
● 我寫的 Test case 達到測試的目的了嗎?
● 我寫的 Test case 有品質嗎?
● 我寫的 Test case 正確碼? 他也需要被測試嗎?
主軸
Why unit test
What to test地雷誤區
Valid Test
Effective Test
那些看的到的 UI
就算了
還有那些看不到的一堆 db record …
1. Automation
- Continuous Integration (Jenkins, Travis ..)
- Test report ( jacoco, Bug report …)
- 可量化,視覺化
- 不需人工介入
- 人會累、眼殘
Cost of test VS. Feature change
2. high ROI
- 投入開發成本相對少
- 規模小,debug相對容易
- 發現早期錯誤
20% test , kill 80% problem
Integration testAcceptance test Manual testMonkey test
Other tests ...
Unit test
Program
Stress test
Clients / End users
主軸
What to test
地雷誤區
Valid Test
Effective Test
Why unit test
誤區Boom !
讓 test 失去意義,反而成為開發的路障:
● 照著 function 來寫 test
● 時好時壞
● 測不到要害
誤區1 - 為了 function 而寫 test case
function
branch 1
branch 2
branch 3
Solution:
- 先了解SPEC/ requirement
- 不要看著product code來寫
test
誤區2 -時好時壞
● 沒有控制 random 變數
● 沒做好 Isolation
public static void updateSpentCheckpoint(Campaign campaign,
boolean doUpdateUTime) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime currHour = now.truncatedTo(ChronoUnit.HOURS);
//assume that Hourly sync finish
if (!LocalDateTimeUtil.isBeforeMidnight(now)) {
currHour = currHour.minusHours(1);
}
campaign.getCheckpoint().setTime(currHour);
}
Solution:
Wrapper/Boundary
E.g. TimeMachine
A Story
Long long time ago …
有一間電商公司
X
誤區3 - 無法正中要害
X
x
血氣方剛的工程師
Bob:
「交給我去開發
吧!」
public void updateReport(Instant targetHourstamp) {
EbaxHourlyReport ebaxHourlyReport =
Optional.ofNullable(hourlyReportRepository.findByHourstamp(targetHourstamp))
.orElse(new
EbaxHourlyReport(targetHourstamp));
try {
String reportBody = tenMaxAPI.fetchReport(targetHourstamp);
TenMaxHourlyReport tenMaxHourlyReport = parseTenMaxReport(reportBody);
updateRecordFromRemoteReport(tenMaxHourlyReport, ebaxHourlyReport);
hourlyReportRepository.save(ebaxHourlyReport);
}
catch(ParseException e) {
logger.error("update fail from TenMaxReport for hourstamp:{}", targetHourstamp,
e);
}
}
血氣方剛的工程師
Bob:
「我知道我要寫 Test
case !」
Mockito
Most popular Mocking framework for unit
tests written in Java
- Mock / Stub / Spy
- when
- Verify
- doNothing
@RunWith(MockitoJUnitRunner.class)
public class ReportServiceTest {
@InjectMocks
private ReportService service;
@Mock
private HourlyReportRepository hourlyReportRepository;
@Mock
private TenMaxAPI tenMaxAPI;
先寫個成功的 case !
@Test
public void testUpdateReportFromTenmax_Ok() {
// GIVEN
Instant targetHour = Instant.parse("2017-03-25T00:00:00");
when(hourlyReportRepository.findByHourstamp(targetHour)).thenReturn(null);
when(tenMaxAPI.fetchReport(targetHour)).thenReturn(
goodTenMaxReportResponseXmlString());
// WHEN
service.updateReport(targetHour);
// THEN
EbaxHourlyReport expect = new EbaxHourlyReport(targetHour, 100, 200, 300);
Mockito.verify(hourlyReportRepository, times(1)).save(expect);
}
「然後再一個 Fail 的
case 就妥穩了!」
EASY !!!
PowerMockito
A more powerful unit test mocking
framework
● Mock private methods
● Mock static methods
● to let static methods throw exceptions
● Whitebox inject
@Test
public void testUpdateReport_fail_for_receive_incorrect_response() throws Exception {
//GIVEN
Instant targetHour = Instant.parse("2017-03-25T00:00:00");
when(hourlyReportRepository.findByHourstamp(targetHour)).thenReturn(null);
when(tenMaxAPI.fetchReport(targetHour)).thenReturn(badResponseXmlString());
//WHEN
service.updateReport(targetHour);
//THEN
// Bob 發現 PowerMockito驚為天人的強大,馬上使用
PowerMockito.verifyPrivate(service, never())
.invoke("updateRecordFromAdHubReport",
any(),
any());
}
驗證private function? 好像蠻直觀… DONE!
三個月後 ...
TenMax 工程師:
「我更新了格式,
你可以不用轉了」
太棒了!
我馬上改!
public void updateReport(Instant targetHourstamp) {
EbaxHourlyReport ebaxHourlyReport =
Optional.ofNullable(hourlyReportRepository.findByHourstamp(targetHourstamp))
.orElse(new
EbaxHourlyReport(targetHourstamp));
try {
String reportBody = tenMaxAPI.fetchReport(targetHourstamp);
EbaxHourlyReport tenMaxHourlyReport = parseTenMaxReport(reportBody);
//updateRecordFromRemoteReport(tenMaxHourlyReport, ebaxHourlyReport);
hourlyReportRepository.save(ebaxHourlyReport);
}
catch(ParseException e) {
logger.error("update fail from TenMaxReport for hourstamp:{}", targetHourstamp, e);
}
}
How about
the tests?
不要讓 Test
流於形式
● Code coverage report ?
● 給老闆看 ?
要測到要害,提早曝露問題
主軸
What to test
地雷誤區
Valid Test
Effective Test
Why unit test
Motivation
Of function
Function Type
1. 回傳計算結果
● Return value
2. 執行動作
● Interaction
● State changed
● Exception throw
Return result 回傳計算結果
public static boolean isValidXmlFormat(String xmlContent) {
if(//....)
return true;
return false;
}
public HourlyRecord findByHourstamp(Instant targetHourstamp) {
//...
return result;
}
public void updateReport(Instant targetHourstamp) {
EbaxHourlyReport ebaxHourlyReport =
Optional.ofNullable(hourlyReportRepository.findByHourstamp(targetHourstamp))
.orElse(new
EbaxHourlyReport(targetHourstamp));
try {
String reportBody = tenMaxAPI.fetchReport(targetHourstamp);
EbaxHourlyReport tenMaxHourlyReport = parseTenMaxReport(reportBody);
//updateRecordFromRemoteReport(tenMaxHourlyReport, ebaxHourlyReport);
hourlyReportRepository.save(ebaxHourlyReport);
}
catch(ParseException e) {
logger.error("update fail from TenMaxReport for hourstamp:{}", targetHourstamp, e);
Invoke interface
修改狀態
public static void updateLastModified(HourlyRecord record, String stuff) {
record.setUpdateTime(LocalDateTime.now());
record.setLastUpdate(stuff);
//...
}
Throw Exception
public static LocalDate parseDate(String str) throws InvalidDateFormatException {
if(str.contains( /*... */)) {
throw new InvalidDateFormatException("Date format should be yyyy-mm-dd");
}
//... more
return result;
}
private TenMaxHourlyReport parseTenMaxReport(String reportXml) throws ParseException {
if(reportXml.contains("bad xml format")) {
throw new ParseException("unsupport xml format", 0);
}
TenMaxHourlyReport report = new TenMaxHourlyReport();
report.setImpression(100);
report.setRequestCount(1000);
return report;
}
private void updateRecordFromRemoteReport(TenMaxHourlyReport src, EbaxHourlyReport
target) {
target.setRequestCount(src.getRequestCount());
target.setImpreCount(src.getImpression());
// series properties to assign ...
}
Test 在乎的是
結果不在乎過程(private function),
只在乎結果 從input/output 判斷該對 function 做甚麼驗證
主軸
What to test地雷:
- Invalid
- unreasonable test
Valid Test
Effective Test
Why unit test
Valid +
Efficient =
Effective
Valid
Team
Convention
Fast
(IDE support)
其他成員難以理解來不及寫
日後維護困難
Effective
讀code 也是溝通成本
@Test
public void testCloneObject() throws Exception {
//case1
AdsOrder ao1 = null;
AdsOrder ao2 = BeanUtils.cloneObject(ao1);
assertNull(ao2);
//case2
AdsOrder ao3 = MockDataUtils.getAdsOrder();
AdsOrder ao4 = BeanUtils.cloneObject(ao3);
assertEquals(ao3, ao4);
}
@Test
public void
test_cloneObject_WHEN_src_is_null_THEN_target_is_null()
throws IllegalAccessException, InstantiationException {
AdsOrder src = null;
AdsOrder target = BeanUtils.cloneObject(src);
Assertions.assertThat(target).isNull();
}
@Test
public void
test_cloneObject_WHEN_src_isNot_null_THEN_target_is_equalTo_s
rc() throws IllegalAccessException, InstantiationException {
AdsOrder src = MockDataUtils.getAdsOrder();
AdsOrder target = BeanUtils.cloneObject(src);
Assertions.assertThat(target).isEqualTo(src);
}
Convention of test
● Naming of test function
● Construction of test function
● Readable content
● Libraries for test
from Clean Code Ch#9
What makes a clean test?
Three things. Readability, readability, and readability.
Readability is perhaps even more important in unit tests than it is in production
code.
是甚麼造就了一個整潔的測試?
三件事,可讀性、可讀性,還是可讀性。可讀性,在單元測試裡可能比在產品程式裡還
要重要。
Accessory
● JUnit5
@Test
@DisplayName("GIVEN: DB 存在 2016-06-30 09:00:00 的 AdxHourly 資料,和 Hourly 資料 "
+ "WHEN: 呼叫 compareWithHourlyReport THEN: 沒有任何錯誤訊息 ")
void test_compareWithHourlyReport_with_compare_success() {
// TODO ...
}
● JUnit4 - extends BlockJUnit4ClassRunner.class
Readable test name
Accessory #2
Opensource:
● JGiven
● JBehavior
● https://github.com/junit-team/junit4/wiki/Custom-runners
FASTER
● Mindset
● IDE tool
● practice
跟工具混熟點
● Paper & Pen (真的是紙跟筆)=> mind map
● IDE Hot keys (Intellij):
- fix error
- refactor
- run
- re-run
● Code snippet / template
Case1
Case2
Case3
Case4
Practice
&
Practice
&
Practice
寫測試最爽的事
別人submit code
因為我的test case 而
build fail.
進行Refactor 時
每改好一個地方,
就可以馬上驗證
Code coverage
蒸蒸日上
你改壞了!
又綠了!
Conclusion
Why to unit test ?
● 確保程式修改後還能正確運作的能力
● Automation
What to test ?
● Motivation
● 在乎結果,忽略過程 (private function)
● Specification By Example (SBE)
How to boost ?
● Team convention
● Familiar with IDE/tools
● Hotkeys
● Practice & Practice & Practice
學會內功,其他
武功都學得快
JS - mocha.js + chai.js +
sinon.js
Php - phpunit
Question ?
Reference
Clean code
https://github.com/rmsadik/x/blob/master/CleanCode/Clean%20Code.pdf
Agile Testing
http://www.ambysoft.com/essays/agileTesting.html
【TDD】課堂心得與筆記 by James Wang
https://dotblogs.com.tw/jameswang/2017/06/12/174704
Contact me
Email: zen0106(at)gmail.com
Logdown: http://oreo0725-blog.logdown.com/

淺談高效撰寫單元測試