Junit

使用

@Rule

  • @Rule是JUnit4.7加入的新特性,有点类似于拦截器,用于在测试方法执行前后添加额外的处理。实际上是@Before,@After的另一种实现
    • 需要注解在实现了TestRule的public成员变量上或者返回TestRule的方法上
    • 相应Rule会应用于该类每个测试方法
  • 允许在测试类中非常灵活的增加或重新定义每个测试方法的行为,简单来说就是提供了测试用例在执行过程中通用功能的共享的能力 ^1
  • 案例参考下文ErrorCollector

ErrorCollector类收集错误统一抛出

  • Junit在遇到一个测试失败时,并会退出,通过ErrorCollector可实现收集所有的错误,等方法运行完后统一抛出
  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Example {
@Rule
public ErrorCollector collector = new ErrorCollector();

@Test
public void example() {
collector.addError(new RuntimeException("error 1"));
System.out.println("==================================");

// 如果测试值 myVal != true 则将错误添加到collector中
boolean myVal = false;
// IsEqual.equalTo("xxx") IsNull.nullValue()
collector.checkThat("error2", myVal, Is.is(true));

// 代码执行完,此处会统一抛出错误,提示2个异常。但是如果是前面代码执行报错则不会打印测试结果
// 如果全部成功则不报错
}
}

Springboot测试

  • 测试环境使用单独的配置文件
    • 可使用@ActiveProfiles("test")激活application-test.yml的配置文件
    • 如果在src/test/resources目录下增加application-test.yml,运行时会覆盖src/main/resources下的该文件
  • 普通测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@AutoConfigureMockMvc // 可以自动的注册所有添加@Controller或者@RestController的路由的MockMvc了
@RunWith(SpringRunner.class)
@SpringBootTest
// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // websocket环境需要,否则报错:javax.websocket.server.ServerContainer not available
//@ActiveProfiles(value = {"dev", "dev-local"}) // 可设置配置文件(如果文件在外部可在测试类配置中增加环境变量,如spring.config.additional-location=/Users/smalle/data/project/aezo-chat-gpt/)
public class DynamicAddTests {
@Autowired
private MockMvc mockMvc;

@Test
public void login(){
try {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/test3?dsKey=mysql-two-dynamic"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
String content = mvcResult.getResponse().getContentAsString();
Assert.assertEquals("success", "hello world!", content);

mockMvc.perform(MockMvcRequestBuilders.post("/api/login/auth")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"smalle\"}")
).andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print()); // 打印请求过程
} catch (Exception e) {
e.printStackTrace();
}
}
}

多线程测试

多线程简单测试模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public class TestU {
public static void multiThreadSimple(MultiThreadSimpleTemplate.Exec exec) {
new MultiThreadSimpleTemplate().run(exec, null, null);
}

public static void multiThreadSimple(MultiThreadSimpleTemplate.Exec exec, int totalNum, int threadNum) {
new MultiThreadSimpleTemplate().run(totalNum, threadNum, exec, null, null);
}

public static void multiThreadSimple(MultiThreadSimpleTemplate.Exec exec, MultiThreadSimpleTemplate.BeforeExec beforeExec,
MultiThreadSimpleTemplate.AfterExec afterExec, int totalNum, int threadNum) {
new MultiThreadSimpleTemplate().run(totalNum, threadNum, exec, beforeExec, afterExec);
}

private static class MultiThreadSimpleTemplate {
// 总访问量是totalNum,并发量是threadNum
private int totalNum = 1000;
private int threadNum = 10;

private int count = 0;
private float sumExecTime = 0;
private long firstExecTime = Long.MAX_VALUE;
private long lastDoneTime = Long.MIN_VALUE;

public void run(int totalNum, int threadNum, Exec exec, BeforeExec beforeExec, AfterExec afterExec) {
this.totalNum = totalNum;
this.threadNum = threadNum;
this.run(exec, beforeExec, afterExec);
}

public void run(Exec exec, BeforeExec beforeExec, AfterExec afterExec) {
if(beforeExec != null) {
if(!beforeExec.beforeExec()) {
System.out.println("BeforeExec返回false, 中断运行");
}
}

final ConcurrentHashMap<Integer, ThreadRecord> records = new ConcurrentHashMap<Integer, ThreadRecord>();

// 建立ExecutorService线程池,threadNum个线程可以同时访问
ExecutorService es = Executors.newFixedThreadPool(threadNum);
final CountDownLatch doneSignal = new CountDownLatch(totalNum); // 此数值和循环的大小必须一致

for (int i = 0; i < totalNum; i++) {
Runnable run = () -> {
try {
int index = ++count;
long systemCurrentTimeMillis = System.currentTimeMillis();

exec.exec();

records.put(index, new ThreadRecord(systemCurrentTimeMillis, System.currentTimeMillis()));
} catch (Exception e) {
e.printStackTrace();
} finally {
// 每调用一次countDown()方法,计数器减1
doneSignal.countDown();
}
};
es.execute(run);
}

try {
// 计数器大于0时,await()方法会阻塞程序继续执行。直到所有子线程完成(每完成一个子线程,计数器-1)
doneSignal.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

// 获取每个线程的开始时间和结束时间
for (int i : records.keySet()) {
ThreadRecord r = records.get(i);
sumExecTime += ((double) (r.endTime - r.startTime)) / 1000;

if (r.startTime < firstExecTime) {
firstExecTime = r.startTime;
}
if (r.endTime > lastDoneTime) {
this.lastDoneTime = r.endTime;
}
}

float avgExecTime = this.sumExecTime / records.size();
float totalExecTime = ((float) (this.lastDoneTime - this.firstExecTime)) / 1000;
NumberFormat nf = NumberFormat.getNumberInstance();
nf.setMaximumFractionDigits(4);

// 需要关闭,否则JVM不会退出。(如在Springboot项目的Job中切勿关闭)
es.shutdown();

System.out.println("======================================================");
System.out.println("线程数量:\t" + threadNum + " 个");
System.out.println("总访问量:\t" + totalNum + " 次");
System.out.println("平均执行时间:\t" + nf.format(avgExecTime) + " 秒");
System.out.println("总执行时间:\t" + nf.format(totalExecTime) + " 秒");
System.out.println("吞吐量:\t\t" + nf.format(totalNum / totalExecTime) + " 次/秒");
System.out.println("======================================================");

if(afterExec != null) {
afterExec.afterExec();
}
}

private static class ThreadRecord {
long startTime;
long endTime;

ThreadRecord(long st, long et) {
this.startTime = st;
this.endTime = et;
}
}

@FunctionalInterface
public interface BeforeExec {
boolean beforeExec();
}

@FunctionalInterface
public interface Exec {
void exec();
}

@FunctionalInterface
public interface AfterExec {
void afterExec();
}
}
}

基于GroboUtils

  • 多线程测试(基于Junit+GroboUtils)

    • 安装依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!-- 第三方库 -->
      <repositories>
      <repository>
      <id>opensymphony-releases</id>
      <name>Repository Opensymphony Releases</name>
      <url>https://oss.sonatype.org/content/repositories/opensymphony-releases</url>
      </repository>
      </repositories>

      <dependency>
      <groupId>net.sourceforge.groboutils</groupId>
      <artifactId>groboutils-core</artifactId>
      <version>5</version>
      </dependency>
    • 使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      @Test
      public void multiRequestsTest() {
      int runnerCount = 100; // 并发数
      // 构造一个Runner
      TestRunnable runner = new TestRunnable() {
      @Override
      public void runTest() throws Throwable {
      // TODO 测试内容
      // Thread.sleep(1000); // 结合sleep表示业务处理过程,测试效果更加明显
      System.out.println("===>" + Thread.currentThread().getId());
      }
      };

      TestRunnable[] arrTestRunner = new TestRunnable[runnerCount];
      for (int i = 0; i < runnerCount; i++) {
      arrTestRunner[i] = runner;
      }
      MultiThreadedTestRunner mttr = new MultiThreadedTestRunner(arrTestRunner);
      try {
      mttr.runTestRunnables();
      } catch (Throwable e) {
      e.printStackTrace();
      }
      }

参考

ChatGPT开源小程序