测试多线程代码

一般来说,如果一段代码是多线程的,那它不应该属于单元测试的范畴,而是应该通过集成测试保障。

保持简单

  • 尽量减少线程控制代码与应用代码的重叠
    • 重新设计代码,可以在线程不参与的情况下进行测试,专注业务块
    • 编写针对多线程执行逻辑的代码,进行有重点的测试
  • 详细他人的工作,使用已经验证多的多线程工具和库

示例

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;
 
    }
 
}

测试数据库

  • 对于持久层代码,继续用桩没有意义,这时候需要编写一些低速测试以保证访问持久层的数据没有问题
  • 为了避免拖累已有的用例执行速度,可以将持久层的测试与其他的测试分离,而放到集成测试部分中去处理
  • 由于集成测试存在的难度,尽量以单元测试的形式覆盖,以减少集成测试的数量

总结

采用单元测试测试多线程的关键在于将业务代码与多线程代码区分开,由不同的用例分别负责测试业务逻辑的正确性与多线程代码执行的正确性。
  1. ProfileMatcher是一个多线程处理类
  2. 方法findMatchingProfiles(Criteria criteria, MatchListener listener, List<MatchSet> matchSets, BiConsumer<MatchListener, MatchSet> processFunction),负责线程的启动和运行,业务行为通过processFunction传入(Java8之前不知道怎么解决)
  3. 业务逻辑在process()方法中,不涉及任何多线程的内容
  4. 经过上面的拆分,可以将业务代码与多线程逻辑分别编写测试
    1. gathersMatchingProfiles负责测试多线程代码,直接植入了一个新定义的processFunction,体现了Java8将方法作为一类对象的强大能力
    2. processNotifiesListenerOnMatch和processDoesNotNotifyListenerWhenNoMatch负责测试业务逻辑
  5. 总结一下
    1. 如果想轻松的做单元测试,需要仔细设计代码,短小、内聚,减少依赖,良好的分层
    2. 高级一点可以做TDD,习惯之后代码质量会有很大提高
    3. 学会利用桩和mock解决一些苦难的测试
    4. 对于某些困难的场景(多线程、持久存储)的测试,本质上也是将关注点拆分来进行测试
    5. 单元测试需要配合持续集成才能达到最好的效果
    6. 团队如果没有意识到单元测试的价值,推广是没有用的,蛮力和硬性要求只能有驱动作用,但如果没有内在的自觉性一定会流于形式
  6. 关注点分离,将业务逻辑与多线程、持久化存储等依赖分类,单独进行测试
  7. 使用mock来避免依赖缓慢和易变的数据
  8. 根据需要 编写集成测试,但是保持简单
posted @ 2020-10-19 17:43 纪玉奇 阅读(452) 评论(0) 推荐(0) 编辑
摘要: 在编写业务代码前,先考虑如何编写测试,再编写业务代码,这种开发方式称作:TDD test-driven development。 使用TDD的主要优点 就通常的单元测试而言,最为明显优点就增强了我们对代码按照设计运行的信心。 而TDD,由于是在编写业务代码提前设计,可以说,这些单元测试就反映了业务需 阅读全文
posted @ 2020-10-19 17:41 纪玉奇 阅读(301) 评论(0) 推荐(0) 编辑
摘要: 进行重构以使得代码更为清晰 单元测试可以有效的支持重构,在单元测试保障下,可能的错误会因单元测试的存在而被及时识别。 重构的范围比较大,这里仅说明方法级别的重构。 方法级别的重构的原则是将复杂的,能够隔离的逻辑单独提取,给与明确的方法命名,保证方法内的逻辑简洁,以调高代码的易读性和易测试性。 在进行 阅读全文
posted @ 2020-10-19 17:38 纪玉奇 阅读(149) 评论(0) 推荐(0) 编辑
摘要: 很多情况下,代码需要与外部依赖打交道,如一个REST地址,数据库链接、外部IO等;这些依赖有些速度过慢、有些不够稳定,不符合单元测试要求的快速、可重复等原则性要求,因此引入了Mock对象这一概念。与Mock相关的还有Stub这个单词。 stub 桩,它针对指定的输入缓存了行为 mock 模拟对象,增 阅读全文
posted @ 2020-10-19 17:36 纪玉奇 阅读(1312) 评论(0) 推荐(0) 编辑
摘要: 编写单元测试的用例是验证代码的正确性,很多情况下,我们倾向于让测试沿着主路径,也就是”happy path"前进,仅能验证代码的功能,却无法保证代码的健壮,因此本文就将解决单元测试测试什么这个问题。 在《Pragmatic Unit Testing in Java 8 with JUNIT》这本书中 阅读全文
posted @ 2020-10-19 17:34 纪玉奇 阅读(322) 评论(0) 推荐(0) 编辑
摘要: 单元测试常见问题 单元测试对接手人没有意义 测试会间断性的失败 ”测试“并没有实际意义 测试需要过长的时间执行 测试没有有效覆盖代码 测试与实现耦合太紧密,意味着一点点调整将会导致大量测试失败 测试太复杂,需要预制太多条件 好的单元测试所要遵循的几个原则 [F]AST 快速性 [I]solate 隔 阅读全文
posted @ 2020-10-19 17:31 纪玉奇 阅读(1711) 评论(0) 推荐(0) 编辑
摘要: 主要对单元测试相关的一些方法论进行了说明,单元测试本身不难,难的是如何使团队认识、认可和执行单元测试,并产生正常的效果。 阅读全文
posted @ 2020-10-19 17:30 纪玉奇 阅读(553) 评论(0) 推荐(0) 编辑
摘要: MonitoredItem 每个监控项均指明了要监控的项目(item)和用来发送通知的订阅。 item可以是一个节点的属性(node attribute)。 MonitorItem可以监控一个属性,一个变量或者一个事件 可以通过MonitorItem定义的过滤器(fiter),来决定是否产生一个通知 阅读全文
posted @ 2018-09-13 10:30 纪玉奇 阅读(16577) 评论(1) 推荐(1) 编辑
摘要: 适用场景 当系统需要应用高并发的冲击时,一个最常用的策略是使用缓存提高系统容量,这通常是效果最好的方式,但如论如何提升系统容量,都会存在一个QPS/TPS的阈值,超过该阈值则认为系统不再稳定,因此需要采取措施屏蔽掉这些请求,达到系统稳定可用的目的。 实现这一目标的常见策略为限流: 限流,顾名思义就是 阅读全文
posted @ 2018-05-31 17:46 纪玉奇 阅读(1945) 评论(0) 推荐(0) 编辑
摘要: 在计算机科学与信息科学领域,理论上,本体是指一种“形式化的,对于共享概念体系的明确而又详细的说明”[1]。本体提供的是一种共享词表,也就是特定领域之中那些存在着的对象类型或概念及其属性和相互关系[2];或者说,本体就是一种特殊类型的术语集,具有结构化的特点,且更加适合于在计算机系统之中使用;或者说,本体实际上就是对特定领域之中某套概念及其相互之间关系的形式化表达(formal representation)。本体是人们以自己兴趣领域的知识为素材,运用信息科学的本体论原理而编写出来的作品。本体一般可以用来针对该领域的属性进行推理,亦可用于定义该领域(也就是对该领域进行建模)。此外,有时人们也会将“本体”称为“本体论”。 阅读全文
posted @ 2018-02-26 16:00 纪玉奇 阅读(1875) 评论(0) 推荐(0) 编辑
点击右上角即可分享
微信分享提示