自动化完成1000个用户的登录并获取token并生成tokens.txt文件
-
写作背景
在我学习使用redis实现秒杀功能的过程中,在编写完秒杀代码后,需要使用Jmeter实际测试1000个用户进行秒杀,由于秒杀功能需要在用户登录完成后才能实现,用户是否登录是由登录拦截器实现的,用户在登录成功后,后台会生成一个token然后发送给浏览器,每次发送秒杀请求时,登录拦截器都会判断浏览器中是否有token,没有token就说明用户未登录,不能进行秒杀,只有含有token的秒杀请求才会被执行,具体流程图如下:
现在存在的问题是:我们想要发送/voucher-order/seckill/id
请求,就先需要实现/user/code
和/user/login
请求,而这两个请求实现很简单,但是需要实现1000次,同时获取这1000给token
用于Jmeter压力测试。这总不可能手动测试把,所以就需要通过代码实现了(详情见P69) -
代码实现
核心实现思路:通过编写一个测试类,然后使用
MockMvc
发送请求,最终将获取到的token写入tokens.txt文件中(如果您有更好的方法,请留言告知在下,在下不胜感激)package com.hmdp.shop; import cn.hutool.core.lang.Assert; import cn.hutool.core.thread.ThreadUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.hmdp.dto.LoginFormDTO; import com.hmdp.dto.Result; import com.hmdp.entity.User; import com.hmdp.service.IUserService; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import javax.annotation.Resource; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; /** * @author ghp * @date 2023/2/7 * @title * @description */ @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) @AutoConfigureMockMvc @Slf4j public class GenerateToken { @Resource private MockMvc mockMvc; @Resource private IUserService userService; @Resource private ObjectMapper mapper; @Test // 忽视异常 @SneakyThrows public void login() { // 查询数据库得到1000个号码 List<String> phoneList = userService.lambdaQuery() .select(User::getPhone) .last("limit 1000") .list().stream().map(User::getPhone).collect(Collectors.toList()); // 使用线程池,线程池总放入1000个号码,提高效率 ExecutorService executorService = ThreadUtil.newExecutor(phoneList.size()); // 创建List集合,存储生成的token。多线程下使用CopyOnWriteArrayList,实现读写分离,保障线程安全(ArrayList不能保障线程安全) List<String> tokenList = new CopyOnWriteArrayList<>(); // 创建CountDownLatch(线程计数器)对象,用于协调线程间的同步 CountDownLatch countDownLatch = new CountDownLatch(phoneList.size()); // 遍历phoneList,发送请求,然后将获取的token写入tokenList中 phoneList.forEach(phone -> { executorService.execute(() -> { try { // 发送获取验证码的请求,获取验证码 String codeJson = mockMvc.perform(MockMvcRequestBuilders .post("/user/code") .queryParam("phone", phone)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn().getResponse().getContentAsString(); // 将返回的JSON字符串反序列化为Result对象 Result result = mapper.readerFor(Result.class).readValue(codeJson); Assert.isTrue(result.getSuccess(), String.format("获取“%s”手机号的验证码失败", phone)); String code = result.getData().toString(); // 创建一个登录表单 // 使用建造者模式构建 登录信息对象,我这里就没有使用了,我是直接使用new(效率较低不推荐使用) // LoginFormDTO formDTO = LoginFormDTO.builder().code(code).phone(phone).build(); LoginFormDTO formDTO = new LoginFormDTO(); formDTO.setCode(code); formDTO.setPhone(phone); // 将LoginFormDTO对象序列化为JSON String json = mapper.writeValueAsString(formDTO); // 发送登录请求,获取token // 发送登录请求,获取返回信息(JSON字符串,其中包含token) String tokenJson = mockMvc.perform(MockMvcRequestBuilders .post("/user/login").content(json).contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn().getResponse().getContentAsString(); // 将JSON字符串反序列化为Result对象 result = mapper.readerFor(Result.class).readValue(tokenJson); Assert.isTrue(result.getSuccess(), String.format("获取“%s”手机号的token失败,json为“%s”", phone, json)); String token = result.getData().toString(); tokenList.add(token); // 线程计数器减一 countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } }); }); // 线程计数器为0时,表示所有线程执行完毕,此时唤醒主线程 countDownLatch.await(); // 关闭线程池 executorService.shutdown(); Assert.isTrue(tokenList.size() == phoneList.size()); // 所有线程都获取了token,此时将所有的token写入tokens.txt文件中 writeToTxt(tokenList, "\\tokens.txt"); log.info("程序执行完毕!"); } /** * 生成tokens.txt文件 * @param list * @param suffixPath * @throws Exception */ private static void writeToTxt(List<String> list, String suffixPath) throws Exception { // 1. 创建文件 File file = new File(System.getProperty("user.dir") + "\\src\\main\\resources" + suffixPath); if (!file.exists()) { file.createNewFile(); } // 2. 输出 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); for (String content : list) { bw.write(content); bw.newLine(); } bw.close(); log.info("tokens.txt文件生成完毕!"); } }
测试结果:
注意点:- 需要使用前后端协议,将后端返回数据统一封装到Result类中
- 在获取验证码时,一定要记得将验证码封装到Result中,即:Result.ok(code),但在实际业务中是Result.ok(),因为生成的验证码不可能直接返回给浏览器吧,所以在测试完成后,一定要记得改回来