對一些困難的單元測試場景提供了解決方法
測試多線程代碼
一般來說,如果一段代碼是多線程的,那它不應該屬于單元測試的範疇,而是應該通過內建測試保障。
保持簡單
- 盡量減少線程控制代碼與應用代碼的重疊
- 重新設計代碼,可以線上程不參與的情況下進行測試,專注業務塊
- 編寫針對多線程執行邏輯的代碼,進行有重點的測試
- 詳細他人的工作,使用已經驗證多的多線程工具和庫
示例
ProfileMatcher
/***
* Excerpted from "Pragmatic Unit Testing in Java with JUnit",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/utj2 for more book information.
***/
package iloveyouboss;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
public class ProfileMatcher {
private Map<String, Profile> profiles = new HashMap<>();
private static final int DEFAULT_POOL_SIZE = 4;
private ExecutorService executor = Executors.newFixedThreadPool(DEFAULT_POOL_SIZE);
protected ExecutorService getExecutor() {
return executor;
}
public void add(Profile profile) {
profiles.put(profile.getId(), profile);
}
public void findMatchingProfiles(Criteria criteria, MatchListener listener, List<MatchSet> matchSets,
BiConsumer<MatchListener, MatchSet> processFunction) {
for (MatchSet set : matchSets) {
Runnable runnable = () -> processFunction.accept(listener, set);
executor.execute(runnable);
}
executor.shutdown();
}
public void findMatchingProfiles(Criteria criteria, MatchListener listener) {
findMatchingProfiles(criteria, listener, collectMatchSets(criteria), this::process);
}
// 進一步将業務邏輯拆分出來,然後直接針對process就可以測試了
public void process(MatchListener listener, MatchSet set) {
if (set.matches()) {
listener.foundMatch(profiles.get(set.getProfileId()), set);
}
}
public List<MatchSet> collectMatchSets(Criteria criteria) {
List<MatchSet> matchSets = profiles.values().stream().map(profile -> profile.getMatchSet(criteria))
.collect(Collectors.toList());
return matchSets;
}
}
ProfileMatcherTest
package iloveyouboss;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;
public class ProfileMatcherTest {
private BooleanQuestion question;
private Criteria criteria;
private ProfileMatcher matcher;
private Profile matchingProfile;
private Profile nonMatchingProfile;
private MatchListener listener;
@Before
public void create() {
question = new BooleanQuestion(1, "");
criteria = new Criteria();
criteria.add(new Criterion(matchingAnswer(), Weight.MustMatch));
matchingProfile = createMatchingProfile("matching");
nonMatchingProfile = createNonMatchingProfile("nonMatching");
}
@Before
public void createMatcher() {
matcher = new ProfileMatcher();
}
@Before
public void createMatchListner() {
listener = mock(MatchListener.class);
}
@Test
public void collectsMatchSets() {
matcher.add(matchingProfile);
matcher.add(nonMatchingProfile);
List<MatchSet> sets = matcher.collectMatchSets(criteria);
assertThat(sets.stream().map(set -> set.getProfileId()).collect(Collectors.toSet()),
equalTo(new HashSet<>(Arrays.asList(matchingProfile.getId(), nonMatchingProfile.getId()))));
}
@Test
public void processNotifiesListenerOnMatch() {
matcher.add(matchingProfile);
MatchSet set = matchingProfile.getMatchSet(criteria);
matcher.process(listener, set);
verify(listener).foundMatch(matchingProfile, set);
}
@Test
public void processDoesNotNotifyListenerWhenNoMatch() {
matcher.add(nonMatchingProfile);
MatchSet set = nonMatchingProfile.getMatchSet(criteria);
matcher.process(listener, set);
verify(listener, never()).foundMatch(nonMatchingProfile, set);
}
@Test
public void gathersMatchingProfiles() {
Set<String> processedSets = Collections.synchronizedSet(new HashSet<>());
BiConsumer<MatchListener, MatchSet> processFunction = (listener, set) -> {
processedSets.add(set.getProfileId());
};
List<MatchSet> matchSets = createMatchSets(100);
matcher.findMatchingProfiles(criteria, listener, matchSets, processFunction);
while (!matcher.getExecutor().isTerminated())
;
assertThat(processedSets, equalTo(matchSets.stream().map(MatchSet::getProfileId).collect(Collectors.toSet())));
}
private List<MatchSet> createMatchSets(int count) {
List<MatchSet> sets = new ArrayList<>();
for (int i = 0; i < count; i++) {
sets.add(new MatchSet(String.valueOf(i), null, null));
}
return sets;
}
private Answer matchingAnswer() {
return new Answer(question, Bool.TRUE);
}
private Answer nonMatchingAnswer() {
return new Answer(question, Bool.FALSE);
}
private Profile createMatchingProfile(String name) {
Profile profile = new Profile(name);
profile.add(matchingAnswer());
return profile;
}
private Profile createNonMatchingProfile(String name) {
Profile profile = new Profile(name);
profile.add(nonMatchingAnswer());
return profile;
}
}
測試資料庫
- 對于持久層代碼,繼續用樁沒有意義,這時候需要編寫一些低速測試以保證通路持久層的資料沒有問題
- 為了避免拖累已有的用例執行速度,可以将持久層的測試與其他的測試分離,而放到內建測試部分中去處理
- 由于內建測試存在的難度,盡量以單元測試的形式覆寫,以減少內建測試的數量
總結
采用單元測試測試多線程的關鍵在于将業務代碼與多線程代碼區分開,由不同的用例分别負責測試業務邏輯的正确性與多線程代碼執行的正确性。
- ProfileMatcher是一個多線程處理類
- 方法findMatchingProfiles(Criteria criteria, MatchListener listener, List<MatchSet> matchSets, BiConsumer<MatchListener, MatchSet> processFunction),負責線程的啟動和運作,業務行為通過processFunction傳入(Java8之前不知道怎麼解決)
- 業務邏輯在process()方法中,不涉及任何多線程的内容
- 經過上面的拆分,可以将業務代碼與多線程邏輯分别編寫測試
- gathersMatchingProfiles負責測試多線程代碼,直接植入了一個新定義的processFunction,展現了Java8将方法作為一類對象的強大能力
- processNotifiesListenerOnMatch和processDoesNotNotifyListenerWhenNoMatch負責測試業務邏輯
- 總結一下
- 如果想輕松的做單元測試,需要仔細設計代碼,短小、内聚,減少依賴,良好的分層
- 進階一點可以做TDD,習慣之後代碼品質會有很大提高
- 學會利用樁和mock解決一些苦難的測試
- 對于某些困難的場景(多線程、持久存儲)的測試,本質上也是将關注點拆分來進行測試
- 單元測試需要配合持續內建才能達到最好的效果
- 團隊如果沒有意識到單元測試的價值,推廣是沒有用的,蠻力和硬性要求隻能有驅動作用,但如果沒有内在的自覺性一定會流于形式
- 關注點分離,将業務邏輯與多線程、持久化存儲等依賴分類,單獨進行測試
- 使用mock來避免依賴緩慢和易變的資料
- 根據需要 編寫內建測試,但是保持簡單