java+testng+selenium实现测试用例过程的录制,生成GIF。(实现一)

1.功能需求:

  1. 支持灵活配置:因为本身已有用例执行失败的截图功能,所以需要支持针对单条测试用例的配置;
  2. 支持testng框架xml多线程的执行;
  3. 录制内容文件小、支持调整录制每帧间隔、每条用例录制最大时长(避免用例元素未定位到时长时间录制)。

2.灵活配置实现

创建注解,通过在测试用例上方添加生成GIF注解控制每条用例的开关。
在这里插入图片描述

  1. 创建注解,设置默认值为true
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 创建GenerateGif自定义注解
 * 控制测试方法中是否需要录制GIF
 */
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可见
@Target(ElementType.METHOD) // 注解适用于方法
public  @interface GenerateGif {
    boolean value() default true; // 默认为 true,表示需要截图
}
  1. 在监听器TestListenerAdapter相关的onTestStart、onTestSuccess、onTestFailure相关方法中通过获取测试用例的注解判断是否执行相关录制。
	@Override
	public void onTestStart(ITestResult tr) {
		// 获取测试方法上的 @GenerateGif 注解
		GenerateGif GenerateGif = tr.getMethod().getConstructorOrMethod().getMethod().getAnnotation(GenerateGif.class);
		boolean generateGifStatus = GenerateGif != null && GenerateGif.value(); // 根据注解决定是否捕捉截图
	}

3.多线程及录制时长和每帧间隔实现

在每条用例开始执行前创建一个线程,负责按照播放间隔去截取浏览器屏幕,用例成功、失败时停止线程合成GIF图片并添加到报告当中。用例跳过时停止线程。

  1. 创建相关变量及常量
	private static final String PATH = System.getProperty("user.dir")+File.separator+"screenshot"+File.separator;//存储截图根目录

	private static final Map<String, Thread> threadMap = new ConcurrentHashMap<>();//线程列表
	private static final Map<String,Boolean> threadConditionMap = new HashMap<>();//线程状态列表
	private static final Map<String, List<File>> imageMap = new HashMap<>();// 存储截图文件的列表

	private static final long MAX_CAPTURE_TIME_MS = 20000; // 最大捕捉时间 20 秒
	private static final int MAX_FARAME_INTERVAL_MS = 1000; // 每帧播放间隔 1000毫秒

  1. 创建启动线程截图方法
	// 启动截图线程
	private void startGenerateGif(WebDriver driver,String testName,String gifPath){


		Thread  screenshotThread = new Thread(() -> {

			boolean threadStatus = true;
			long startTime = System.currentTimeMillis(); // 记录开始时间
			// 创建 List<File> 作为 Map 的值
			List<File> fileList = new ArrayList<>();

				while (threadStatus) {
					if ( !threadConditionMap.get(testName) || (System.currentTimeMillis() - startTime >= MAX_CAPTURE_TIME_MS)){
						break;
					}else {
						try {
							File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); // 捕捉浏览器视口的截图
							File destination = new File(gifPath+ System.currentTimeMillis() + ".png"); // 截图保存的目标文件
							FileUtils.copyFile(screenshot, destination); // 复制截图到目标文件
							fileList.add(destination); // 将目标文件添加到 List 中
							Thread.sleep(MAX_FARAME_INTERVAL_MS); // 固定时间秒捕捉一次截图
						}catch (InterruptedException e) {
							e.printStackTrace();
						}catch (IOException e) {
							e.printStackTrace(); // 捕捉并打印异常
						}
					}
				}
			imageMap.put(testName,fileList);
		});
		threadConditionMap.put(testName,true);
		threadMap.put(testName,screenshotThread);

		screenshotThread.start(); // 启动线程
	}
  1. 创建停止线程添加GIF到报告中方法
	// 停止截图并生成 GIF 动图
	private void stopGenerateGif(String testName,String gifPath) {

		Thread thread = threadMap.get(testName);
		threadConditionMap.put(testName,false);
		if (thread != null && thread.isAlive()){
			try{
				thread.join(); // 等待截图线程完成
			} catch ( InterruptedException e) {
				e.printStackTrace();
			}
		}
		try {
			InputStream content;
			takeGif(testName,gifPath);//生成GIF
			File f= new File(gifPath + "screenshot.gif") ;
			content = new FileInputStream(f);
			Allure.addAttachment("用例执行过程如下:", content);
			content.close();
		}catch (IOException e) {
			e.printStackTrace(); // 捕捉并打印异常
		}
	}
  1. 创建截图合成GIF方法
	// 将截图合成 GIF 动图
	public void takeGif(String testName,String gifPath){
		try {
			List<File> fileList = imageMap.get(testName);

			AnimatedGifEncoder encoder = new AnimatedGifEncoder();
			encoder.setRepeat(0); // 设置 GIF 循环次数,0 表示无限循环
			encoder.start(gifPath+ "screenshot.gif");

			for (File file : fileList) {
				BufferedImage image = ImageIO.read(file); // 读取截图
				encoder.setDelay(MAX_FARAME_INTERVAL_MS); // 设置帧延迟,单位为毫秒
				encoder.addFrame(image); // 添加帧
			}
			encoder.finish(); // 完成动画
		}catch (IOException|Error e){
			System.err.println("合成gif图失败");
			e.printStackTrace();
		}
	}

4.监听器完整代码

import com.madgag.gif.fmsware.AnimatedGifEncoder;
import io.qameta.allure.Allure;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.*;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.TestListenerAdapter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

//@Attachment 注解必须放在test文件夹下面
public class ShotListener extends TestListenerAdapter  {
	private static final String PATH = System.getProperty("user.dir")+File.separator+"screenshot"+File.separator;//存储截图根目录

	private static final Map<String, Thread> threadMap = new ConcurrentHashMap<>();//线程列表
	private static final Map<String,Boolean> threadConditionMap = new HashMap<>();//线程状态列表
	private static final Map<String, List<File>> imageMap = new HashMap<>();// 存储截图文件的列表
	private static final long MAX_CAPTURE_TIME_MS = 20000; // 最大捕捉时间 20 秒
	private static final int MAX_FARAME_INTERVAL_MS = 1000; // 每帧播放间隔 1000毫秒


	@Override
	public void onTestStart(ITestResult tr) {

		//判断测试方法是否有gif注解,启动gif截图线程
		if (isGenerateGif(tr)) {
			Map<String, String> map = getUiName(tr);
			rmdir(map.get("gifPath")); // 失败重试时清除上一次的截图
			startGenerateGif(getDriver(tr),map.get("testName"),map.get("gifPath")); // 启动截图线程
		}
	}
	@Override
	public void onTestSuccess(ITestResult tr) {

		//判断测试方法是否有gif注解,生成gif
		if (isGenerateGif(tr)){
			Map<String, String> map = getUiName(tr);
			stopGenerateGif( map.get("testName"),map.get("gifPath"));// 停止截图线程,并生成 GIF 动图
		}
	}

	@Override
	public void onTestSkipped(ITestResult tr) {
		if(isGenerateGif(tr)){
			Map<String, String> map = getUiName(tr);
			Thread thread = threadMap.get(map.get("testName"));
			threadConditionMap.put(map.get("testName"),false);
			if (thread != null && thread.isAlive()) {
				try {
					thread.join(); // 等待截图线程完成
				}
				catch (InterruptedException e) {}
			}
			rmdir(map.get("gifPath")); // 用例跳过时或用例失败重试时(失败重试会进入跳过)删除该测试的截图
		}

	}



	@Override
	public void onConfigurationFailure(ITestResult tr){
		//请求失败截图
		screenshot(getDriver(tr));
	}

	@Override
	public void onTestFailure(ITestResult tr) {
		//请求失败截图
		screenshot(getDriver(tr));

		//判断测试方法是否有gif注解,生成gif
		if (isGenerateGif(tr)){
			Map<String, String> map = getUiName(tr);
			stopGenerateGif(map.get("testName"),map.get("gifPath"));// 停止截图线程,并生成 GIF 动图
		}
	}

	@Override
	public void onFinish(ITestContext context) {
		rmdir(PATH); // 删除总screenshot文件夹
	}


	private boolean isGenerateGif(ITestResult tr){
		// 获取测试方法上的 @GenerateGif 注解
		GenerateGif GenerateGif = tr.getMethod().getConstructorOrMethod().getMethod().getAnnotation(GenerateGif.class);
		return  GenerateGif != null && GenerateGif.value(); // 根据注解决定是否捕捉截图
	}

	private WebDriver getDriver(ITestResult tr){
		//获取浏览器driver
		CaseBase bt = (CaseBase) tr.getInstance();
		return bt.driverBase.getDriver();

	}

	private Map<String, String> getUiName(ITestResult tr) {

		// 获取当前测试类的完整路径
		String fullClassName  = tr.getTestClass().getName();
		// 获取当前测试类的简单名称(不包含包路径)
		String className = fullClassName.substring(fullClassName.lastIndexOf('.') + 1);
		// 获取当前执行的测试方法名
		String methodName  = tr.getMethod().getMethodName();

		String gifPath = PATH + className + File.separator + methodName + File.separator;

		Map<String, String> map = new HashMap<>();
		map.put("testName",className+methodName);
		map.put("gifPath",gifPath);
		return map;
	}

	// 启动截图线程
	private void startGenerateGif(WebDriver driver,String testName,String gifPath){


		Thread  screenshotThread = new Thread(() -> {

			boolean threadStatus = true;
			long startTime = System.currentTimeMillis(); // 记录开始时间
			// 创建 List<File> 作为 Map 的值
			List<File> fileList = new ArrayList<>();

				while (threadStatus) {
					if ( !threadConditionMap.get(testName) || (System.currentTimeMillis() - startTime >= MAX_CAPTURE_TIME_MS)){
						break;
					}else {
						try {
							File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); // 捕捉浏览器视口的截图
							File destination = new File(gifPath+ System.currentTimeMillis() + ".png"); // 截图保存的目标文件
							FileUtils.copyFile(screenshot, destination); // 复制截图到目标文件
							fileList.add(destination); // 将目标文件添加到 List 中
							Thread.sleep(MAX_FARAME_INTERVAL_MS); // 固定时间秒捕捉一次截图
						}catch (InterruptedException e) {
							e.printStackTrace();
						}catch (IOException e) {
							e.printStackTrace(); // 捕捉并打印异常
						}
					}
				}
			imageMap.put(testName,fileList);
		});
		threadConditionMap.put(testName,true);
		threadMap.put(testName,screenshotThread);

		screenshotThread.start(); // 启动线程
	}

	// 停止截图并生成 GIF 动图
	private void stopGenerateGif(String testName,String gifPath) {

		Thread thread = threadMap.get(testName);
		threadConditionMap.put(testName,false);
		if (thread != null && thread.isAlive()){
			try{
				thread.join(); // 等待截图线程完成
			} catch ( InterruptedException e) {
				e.printStackTrace();
			}
		}
		try {
			InputStream content;
			takeGif(testName,gifPath);//生成GIF
			File f= new File(gifPath + "screenshot.gif") ;
			content = new FileInputStream(f);
			Allure.addAttachment("用例执行过程如下:", content);
			content.close();
		}catch (IOException e) {
			e.printStackTrace(); // 捕捉并打印异常
		}
	}

	// 将截图合成 GIF 动图
	public void takeGif(String testName,String gifPath){
		try {
			List<File> fileList = imageMap.get(testName);

			AnimatedGifEncoder encoder = new AnimatedGifEncoder();
			encoder.setRepeat(0); // 设置 GIF 循环次数,0 表示无限循环
			encoder.start(gifPath+ "screenshot.gif");

			for (File file : fileList) {
				BufferedImage image = ImageIO.read(file); // 读取截图
				encoder.setDelay(MAX_FARAME_INTERVAL_MS); // 设置帧延迟,单位为毫秒
				encoder.addFrame(image); // 添加帧
			}
			encoder.finish(); // 完成动画
		}catch (IOException|Error e){
			System.err.println("合成gif图失败");
			e.printStackTrace();
		}
	}

	public void rmdir (String path) {
//		 创建File对象
		File file = new File(path);
//		 判断目录是否存在
		if (file.exists()) {
			// 删除目录
			try {
				FileUtils.forceDelete(file);
			} catch (IOException e) {
				System.err.println("删除文件目录 " + path + " 失败");
				e.printStackTrace();
			}
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值