自动化测试框架
单元测试工具:JUnit,NUnit,PyTest等 集成测试工具:Jenkins,Bamboo等 用户界面(UI)测试工具:Selenium,TestComplete等 API测试工具:Postman,Swagger等 性能测试工具:LoadRunner,Apache JMeter等 安全测试工具:OWASP ZAP,Nessus等 缺陷跟踪工具:PingCode、JIRA,Bugzilla等 测试管理工具:TestRail,qTest等 持续集成/持续部署(CI/CD)工具:Jenkins,Travis CI等 静态代码分析工具:SonarQube,Pylint等 自动化测试工具:Selenium、Appium等
1.Junit Junit 是一个 Java 语言的单元测试框架。它由 Kent Beck 和 Erich Gamma 建立,逐渐成为源于 Kent Beck 的 sUnit 的 xUnit 家族中最为成功的一个。 Junit 有它自己的 JUnit 扩展生态圈。多数 Java 的开发环境都已经集成了 Junit 作为单元测试的工具。
Junit 是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit 测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit 是一套框架,继承 TestCase 类,就可以用 Junit 进行自动测试了。
2.selenium Selenium 是一个用于 Web 应用程序测试的工具。支持的浏览器包括 IE、Mozilla Firefox、Mozilla Suite 等。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。Selenium 是一套完整的 web 应用程序测试系统,包含了测试的录制(selenium IDE),编写及运行(Selenium Remote Control)和测试的并行处理(Selenium Grid)。Selenium 的核心 Selenium Core 基于 JsUnit,完全由 JavaScript 编写,因此可以用于任何支持 JavaScript 的浏览器上。Selenium 可以模拟真实浏览器,自动化测试工具,支持多种浏览器,爬虫中主要用来解决 JavaScript 渲染问题。
selenium 1.0 包括以下两部分:selenium server、 Client Libraries 组成。
Selenium 2 将浏览器原生的 API 封装成 WebDriver API,可以直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭,安装插件,配置证书之类的),所以就像真正的用户在操作一样。
一、Junit
OCR单元测试Demo
1.添加依赖
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito JUnit Jupiter -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Hamcrest (用于断言) -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
<!-- JSON Path (用于验证 JSON 响应) -->
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
2.测试类代码
package javao.cn.work.ocr;
import com.fasterxml.jackson.databind.ObjectMapper;
import javao.cn.work.ocr.controller.SurveyDataController;
import javao.cn.work.ocr.service.SurveyDataService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;
/**
* @Description SurveyDataController 自动化测试类
* @Author Admin小闫
* @Date 2025/12/26
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("SurveyDataController 单元测试")
public class SurveyDataControllerTest {
private MockMvc mockMvc;
@Mock
private SurveyDataService surveyDataService;
@InjectMocks
private SurveyDataController surveyDataController;
private ObjectMapper objectMapper;
private SimpleDateFormat dateFormat;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(surveyDataController).build();
objectMapper = new ObjectMapper();
dateFormat = new SimpleDateFormat("yyyy-MM-dd");
}
// ==================== 测试数据工厂方法 ====================
/**
* 根据你的数据库结构,创建模拟的文件类型代码数据
* 数据库中 file_code 字段存储的是文件型号,如 "AF150", "AF90"
*/
private List<String> createMockFileCodes() {
return Arrays.asList("AF150", "AF90", "AF120", "AF200", "AF300");
}
/**
* 根据你的数据库结构,创建模拟的一连杆数据
* 数据库中 connecting 字段存储的是一连杆,如 "MP_PL1", "MP_PL2", "MP_PL3"
*/
private List<String> createMockConnectingRods() {
return Arrays.asList("MP_PL1", "MP_PL2", "MP_PL3", "MP_PL4", "MP_PL5");
}
/**
* 根据你的数据库结构,创建模拟的粗糙度数据
* 数据库中 roughness 字段存储的是粗糙度,如 "Rvk", "Rz", "Rk", "Rpk"
*/
private List<String> createMockRoughness() {
return Arrays.asList("Rvk", "Rz", "Rk", "Rpk", "Ra");
}
/**
* 根据你的数据库结构,创建模拟的项目数据
* 数据库中 project 字段存储的是项目,如 "act", "ut"
*/
private List<String> createMockProjects() {
return Arrays.asList("act", "ut", "test", "prod", "dev");
}
/**
* 创建模拟的测量值数据 - 基于你的数据库实际结构
*/
private List<Map<String, Object>> createMockMeasureValues() {
List<Map<String, Object>> values = new ArrayList<>();
Calendar cal = Calendar.getInstance();
cal.set(2024, Calendar.JANUARY, 1, 10, 0, 0);
// 模拟10条测量数据
for (int i = 1; i <= 10; i++) {
Map<String, Object> value = new HashMap<>();
value.put("id", i);
value.put("file_code", "AF150");
value.put("part_counter", "1207M" + (i % 2 + 1)); // 1207M1, 1207M2 交替
value.put("operator", i % 2 == 0 ? "LiuGuoli" : "WangWei");
value.put("connecting", "MP_PL" + ((i % 3) + 1)); // MP_PL1, MP_PL2, MP_PL3 循环
value.put("roughness", i % 2 == 0 ? "Rvk" : "Rz");
value.put("project", i % 2 == 0 ? "act" : "ut");
value.put("measure_value", String.format("%.3f", 0.1 + i * 0.05)); // 如 0.150, 0.200
value.put("measure_time", cal.getTime());
values.add(value);
// 增加时间(每天一条)
cal.add(Calendar.DAY_OF_MONTH, 1);
}
return values;
}
/**
* 创建模拟的零件计数器数据
*/
private List<String> createMockPartCounters() {
return Arrays.asList("1207M1", "1207M2", "1208M1", "1713M1", "1718M2",
"0151M1", "0152M2", "1-0526", "2-0520", "0813M2", "0819M1");
}
// ==================== 参数化测试数据源 ====================
private static Stream<String> validFileCodes() {
return Stream.of("AF150", "AF90", "AF120", "AF200", "AF300");
}
private static Stream<Arguments> validConnectingAndRoughness() {
return Stream.of(
Arguments.of("MP_PL1", "Rvk"),
Arguments.of("MP_PL2", "Rz"),
Arguments.of("MP_PL3", "Rk"),
Arguments.of("MP_PL4", "Rpk"),
Arguments.of("MP_PL5", "Ra")
);
}
private static Stream<Arguments> completeQueryParams() {
return Stream.of(
Arguments.of("AF150", "MP_PL1", "Rvk", "act", "1207M1", "2024-01-01", "2024-01-31"),
Arguments.of("AF90", "MP_PL2", "Rz", "ut", "1207M2", "2024-02-01", "2024-02-28"),
Arguments.of("AF150", "MP_PL3", "Rk", "act", "1208M1", "2024-03-01", "2024-03-31")
);
}
// ==================== 测试用例 ====================
@Test
@DisplayName("GET /ocr/data/filecodes - 成功查询所有文件类型代码")
void testGetAllFileCodes_Success() throws Exception {
// 准备测试数据
List<String> mockData = createMockFileCodes();
// 设置 Mock 行为 - 注意:这里要匹配实际方法的返回类型
when(surveyDataService.getAllFileCodes()).thenReturn(mockData);
// 执行请求并验证
mockMvc.perform(get("/ocr/data/filecodes")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(5))
.andExpect(jsonPath("$.data[0]").value("AF150"))
.andExpect(jsonPath("$.data[1]").value("AF90"));
// 验证方法调用
verify(surveyDataService, times(1)).getAllFileCodes();
}
@Test
@DisplayName("GET /ocr/data/filecodes - 查询结果为空")
void testGetAllFileCodes_EmptyResult() throws Exception {
// 设置 Mock 返回空列表
when(surveyDataService.getAllFileCodes()).thenReturn(Collections.emptyList());
// 执行请求并验证
mockMvc.perform(get("/ocr/data/filecodes"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data").isEmpty());
verify(surveyDataService, times(1)).getAllFileCodes();
}
@ParameterizedTest
@ValueSource(strings = {"AF150", "AF90", "AF120"})
@DisplayName("GET /ocr/data/connecting - 参数化测试:根据文件类型查询一连杆")
void testGetConnectingByFileCode_Parameterized(String fileCode) throws Exception {
// 准备测试数据
List<String> mockData = createMockConnectingRods();
// 设置 Mock 行为
when(surveyDataService.getConnectingByFileCode(eq(fileCode))).thenReturn(mockData);
// 执行请求并验证
mockMvc.perform(get("/ocr/data/connecting")
.param("fileCode", fileCode)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(5));
verify(surveyDataService, times(1)).getConnectingByFileCode(fileCode);
}
@Test
@DisplayName("GET /ocr/data/connecting - 缺少必要参数")
void testGetConnectingByFileCode_MissingRequiredParam() throws Exception {
// 执行请求(不传递 fileCode 参数)
mockMvc.perform(get("/ocr/data/connecting"))
.andExpect(status().isBadRequest());
// 验证服务方法未被调用
verify(surveyDataService, never()).getConnectingByFileCode(anyString());
}
@ParameterizedTest
@MethodSource("validConnectingAndRoughness")
@DisplayName("GET /ocr/data/roughness - 参数化测试:根据文件类型和一连杆查询粗糙度")
void testGetRoughnessByFileCodeAndConnecting_Parameterized(String connecting, String expectedRoughness) throws Exception {
// 准备测试数据
List<String> mockData = Arrays.asList(expectedRoughness);
// 设置 Mock 行为
when(surveyDataService.getRoughnessByFileCodeAndConnecting(eq("AF150"), eq(connecting)))
.thenReturn(mockData);
// 执行请求并验证
mockMvc.perform(get("/ocr/data/roughness")
.param("fileCode", "AF150")
.param("connecting", connecting)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data[0]").value(expectedRoughness));
verify(surveyDataService, times(1))
.getRoughnessByFileCodeAndConnecting("AF150", connecting);
}
@Test
@DisplayName("GET /ocr/data/project - 根据文件类型、一连杆和粗糙度查询项目名")
void testGetProjectByFileCodeConnectingAndRoughness_Success() throws Exception {
// 准备测试数据
List<String> mockProjects = createMockProjects();
// 设置 Mock 行为
when(surveyDataService.getProjectByFileCodeConnectingAndRoughness(
eq("AF150"), eq("MP_PL1"), eq("Rvk")))
.thenReturn(mockProjects);
// 执行请求并验证
mockMvc.perform(get("/ocr/data/project")
.param("fileCode", "AF150")
.param("connecting", "MP_PL1")
.param("roughness", "Rvk")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(5))
.andExpect(jsonPath("$.data[0]").value("act"));
verify(surveyDataService, times(1))
.getProjectByFileCodeConnectingAndRoughness("AF150", "MP_PL1", "Rvk");
}
@ParameterizedTest
@MethodSource("completeQueryParams")
@DisplayName("GET /ocr/data/measurevalues - 参数化测试:完整条件查询测量值")
void testGetMeasureValuesByConditions_Parameterized(
String fileCode, String connecting, String roughness,
String project, String partCounter, String startDate, String endDate) throws Exception {
// 准备测试数据
List<Map<String, Object>> mockValues = createMockMeasureValues();
// 解析日期
Date startTime = dateFormat.parse(startDate);
Date endTime = dateFormat.parse(endDate);
// 执行请求并验证
mockMvc.perform(get("/ocr/data/measurevalues")
.param("fileCode", fileCode)
.param("connecting", connecting)
.param("roughness", roughness)
.param("project", project)
.param("partCounter", partCounter)
.param("startTime", startDate)
.param("endTime", endDate)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(10))
.andExpect(jsonPath("$.data[0].file_code").value("AF150"))
.andExpect(jsonPath("$.data[0].measure_value").isString());
verify(surveyDataService, times(1)).getMeasureValuesByConditions(
fileCode, connecting, roughness, project, partCounter, startTime, endTime);
}
@Test
@DisplayName("GET /ocr/data/measurevalues - 日期格式错误")
void testGetMeasureValuesByConditions_InvalidDateFormat() throws Exception {
// 执行请求(使用错误的日期格式)
mockMvc.perform(get("/ocr/data/measurevalues")
.param("fileCode", "AF150")
.param("connecting", "MP_PL1")
.param("roughness", "Rvk")
.param("project", "act")
.param("partCounter", "1207M1")
.param("startTime", "2024/01/01") // 错误格式
.param("endTime", "2024-01-31"))
.andExpect(status().isBadRequest());
// 验证服务方法未被调用
verify(surveyDataService, never()).getMeasureValuesByConditions(
anyString(), anyString(), anyString(), anyString(), anyString(), any(), any());
}
@Test
@DisplayName("GET /ocr/data/measurevalues - 服务层返回空结果")
void testGetMeasureValuesByConditions_EmptyResult() throws Exception {
// 设置 Mock 返回空列表
when(surveyDataService.getMeasureValuesByConditions(
anyString(), anyString(), anyString(),
anyString(), anyString(), any(), any()))
.thenReturn(Collections.emptyList());
// 执行请求
mockMvc.perform(get("/ocr/data/measurevalues")
.param("fileCode", "AF150")
.param("connecting", "MP_PL1")
.param("roughness", "Rvk")
.param("project", "act")
.param("partCounter", "1207M1")
.param("startTime", "2024-01-01")
.param("endTime", "2024-01-31"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data").isEmpty());
}
// ==================== 异常情况测试 ====================
@Test
@DisplayName("GET /ocr/data/connecting - 服务层抛出异常")
void testGetConnectingByFileCode_ServiceException() throws Exception {
// 设置 Mock 抛出异常
when(surveyDataService.getConnectingByFileCode(anyString()))
.thenThrow(new RuntimeException("数据库连接失败"));
// 执行请求
mockMvc.perform(get("/ocr/data/connecting")
.param("fileCode", "AF150"))
.andExpect(status().is5xxServerError());
verify(surveyDataService, times(1)).getConnectingByFileCode("AF150");
}
@Test
@DisplayName("GET 所有接口 - 验证请求方法不支持")
void testUnsupportedHttpMethods() throws Exception {
// 测试 POST 方法不支持
mockMvc.perform(post("/ocr/data/filecodes"))
.andExpect(status().isMethodNotAllowed());
// 测试 PUT 方法不支持
mockMvc.perform(put("/ocr/data/connecting")
.param("fileCode", "AF150"))
.andExpect(status().isMethodNotAllowed());
// 测试 DELETE 方法不支持
mockMvc.perform(delete("/ocr/data/roughness")
.param("fileCode", "AF150")
.param("connecting", "MP_PL1"))
.andExpect(status().isMethodNotAllowed());
}
// ==================== 边界条件测试 ====================
@Test
@DisplayName("GET /ocr/data/connecting - 超长参数测试")
void testGetConnectingByFileCode_LongParameter() throws Exception {
// 构造超长的 fileCode 参数(JDK 8 方式)
StringBuilder sb = new StringBuilder("AF150");
for (int i = 0; i < 1000; i++) {
sb.append("0");
}
String longFileCode = sb.toString();
// 设置 Mock 返回空列表
when(surveyDataService.getConnectingByFileCode(eq(longFileCode)))
.thenReturn(Collections.emptyList());
// 执行请求
mockMvc.perform(get("/ocr/data/connecting")
.param("fileCode", longFileCode))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray());
verify(surveyDataService, times(1)).getConnectingByFileCode(longFileCode);
}
@Test
@DisplayName("GET /ocr/data/measurevalues - 特殊字符参数")
void testGetMeasureValuesByConditions_SpecialCharacters() throws Exception {
// 包含特殊字符的参数
String specialConnecting = "MP_PL-测试_#@&";
String specialProject = "项目测试-特殊字符@#$%";
// 设置 Mock 返回空列表
when(surveyDataService.getMeasureValuesByConditions(
eq("AF150"), eq(specialConnecting), eq("Rvk"),
eq(specialProject), eq("1207M1"), any(), any()))
.thenReturn(Collections.emptyList());
// 执行请求
mockMvc.perform(get("/ocr/data/measurevalues")
.param("fileCode", "AF150")
.param("connecting", specialConnecting)
.param("roughness", "Rvk")
.param("project", specialProject)
.param("partCounter", "1207M1")
.param("startTime", "2024-01-01")
.param("endTime", "2024-01-31"))
.andExpect(status().isOk());
verify(surveyDataService, times(1)).getMeasureValuesByConditions(
"AF150", specialConnecting, "Rvk", specialProject, "1207M1", any(), any());
}
@Test
@DisplayName("GET /ocr/data/measurevalues - 测试部分参数为空的情况")
void testGetMeasureValuesByConditions_WithEmptyParameters() throws Exception {
// 测试部分参数为空字符串的情况
mockMvc.perform(get("/ocr/data/measurevalues")
.param("fileCode", "AF150")
.param("connecting", "MP_PL1")
.param("roughness", "Rvk")
.param("project", "") // 空字符串
.param("partCounter", "1207M1")
.param("startTime", "2024-01-01")
.param("endTime", "2024-01-31"))
.andExpect(status().isOk());
}
// ==================== 数据库实际数据测试 ====================
@Test
@DisplayName("GET /ocr/data/filecodes - 模拟实际数据库数据")
void testGetAllFileCodes_WithRealData() throws Exception {
// 模拟数据库中实际的 file_code 数据
List<String> realFileCodes = Arrays.asList("AF150", "AF90");
when(surveyDataService.getAllFileCodes()).thenReturn(realFileCodes);
mockMvc.perform(get("/ocr/data/filecodes"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data[0]").value("AF150"))
.andExpect(jsonPath("$.data[1]").value("AF90"));
}
@Test
@DisplayName("GET /ocr/data/connecting - 模拟实际数据库的 connecting 数据")
void testGetConnectingByFileCode_WithRealData() throws Exception {
// 模拟数据库中实际的 connecting 数据
List<String> realConnecting = Arrays.asList("MP_PL1", "MP_PL2", "MP_PL3");
when(surveyDataService.getConnectingByFileCode("AF150")).thenReturn(realConnecting);
mockMvc.perform(get("/ocr/data/connecting")
.param("fileCode", "AF150"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(3))
.andExpect(jsonPath("$.data[0]").value("MP_PL1"))
.andExpect(jsonPath("$.data[2]").value("MP_PL3"));
}
@Test
@DisplayName("GET /ocr/data/roughness - 模拟实际数据库的 roughness 数据")
void testGetRoughnessByFileCodeAndConnecting_WithRealData() throws Exception {
// 模拟数据库中实际的 roughness 数据
List<String> realRoughness = Arrays.asList("Rvk", "Rz", "Rk", "Rpk");
when(surveyDataService.getRoughnessByFileCodeAndConnecting("AF150", "MP_PL1"))
.thenReturn(realRoughness);
mockMvc.perform(get("/ocr/data/roughness")
.param("fileCode", "AF150")
.param("connecting", "MP_PL1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(4))
.andExpect(jsonPath("$.data[0]").value("Rvk"))
.andExpect(jsonPath("$.data[1]").value("Rz"));
}
@Test
@DisplayName("GET /ocr/data/project - 模拟实际数据库的 project 数据")
void testGetProjectByFileCodeConnectingAndRoughness_WithRealData() throws Exception {
// 模拟数据库中实际的 project 数据
List<String> realProjects = Arrays.asList("act", "ut");
when(surveyDataService.getProjectByFileCodeConnectingAndRoughness("AF150", "MP_PL1", "Rvk"))
.thenReturn(realProjects);
mockMvc.perform(get("/ocr/data/project")
.param("fileCode", "AF150")
.param("connecting", "MP_PL1")
.param("roughness", "Rvk"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data.length()").value(2))
.andExpect(jsonPath("$.data[0]").value("act"))
.andExpect(jsonPath("$.data[1]").value("ut"));
}
// ==================== 清理和验证 ====================
@Test
@DisplayName("验证测试完成后的 Mock 状态")
void verifyMockStateAfterTests() {
// 这个方法可以在测试套件中用来验证所有 Mock 的交互
verifyNoMoreInteractions(surveyDataService);
}
}
二、Selenium
官网: https://www.selenium.dev/
计算器图形测试Demo
1.前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简易计算器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
padding: 20px;
}
.calculator {
background-color: #2c3e50;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 400px;
overflow: hidden;
padding: 20px;
}
.display {
background-color: #34495e;
color: #ecf0f1;
padding: 25px 20px;
text-align: right;
border-radius: 10px;
margin-bottom: 20px;
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.previous-operand {
font-size: 1.2rem;
color: #bdc3c7;
min-height: 1.5rem;
word-break: break-all;
}
.current-operand {
font-size: 2.5rem;
font-weight: 600;
word-break: break-all;
line-height: 1.2;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
}
button {
border: none;
padding: 20px 10px;
font-size: 1.5rem;
font-weight: 600;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
}
button:active {
transform: scale(0.95);
}
.number, .decimal {
background-color: #ecf0f1;
color: #2c3e50;
}
.number:hover, .decimal:hover {
background-color: #d5dbdb;
}
.operator {
background-color: #3498db;
color: white;
}
.operator:hover {
background-color: #2980b9;
}
.equals {
background-color: #2ecc71;
color: white;
}
.equals:hover {
background-color: #27ae60;
}
.clear, .delete {
background-color: #e74c3c;
color: white;
}
.clear:hover, .delete:hover {
background-color: #c0392b;
}
.span-two {
grid-column: span 2;
}
.calculator-title {
color: white;
text-align: center;
margin-bottom: 15px;
font-size: 1.8rem;
font-weight: 600;
}
.footer {
color: rgba(255, 255, 255, 0.7);
text-align: center;
margin-top: 20px;
font-size: 0.9rem;
}
@media (max-width: 500px) {
.calculator {
padding: 15px;
border-radius: 15px;
}
button {
padding: 18px 8px;
font-size: 1.3rem;
}
.current-operand {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="calculator">
<div class="calculator-title">简易计算器</div>
<div class="display">
<div class="previous-operand"></div>
<div class="current-operand">0</div>
</div>
<div class="buttons">
<button class="clear span-two">AC</button>
<button class="delete">DEL</button>
<button class="operator" data-operation="÷">÷</button>
<button class="number" data-number="7">7</button>
<button class="number" data-number="8">8</button>
<button class="number" data-number="9">9</button>
<button class="operator" data-operation="×">×</button>
<button class="number" data-number="4">4</button>
<button class="number" data-number="5">5</button>
<button class="number" data-number="6">6</button>
<button class="operator" data-operation="-">-</button>
<button class="number" data-number="1">1</button>
<button class="number" data-number="2">2</button>
<button class="number" data-number="3">3</button>
<button class="operator" data-operation="+">+</button>
<button class="number span-two" data-number="0">0</button>
<button class="decimal" data-decimal>.</button>
<button class="equals" data-equals>=</button>
</div>
<div class="footer">支持加减乘除运算 | 点击按钮或使用键盘输入</div>
</div>
<script>
class Calculator {
constructor(previousOperandElement, currentOperandElement) {
this.previousOperandElement = previousOperandElement;
this.currentOperandElement = currentOperandElement;
this.clear();
}
clear() {
this.currentOperand = '0';
this.previousOperand = '';
this.operation = undefined;
}
delete() {
if (this.currentOperand === '0') return;
if (this.currentOperand.length === 1) {
this.currentOperand = '0';
} else {
this.currentOperand = this.currentOperand.slice(0, -1);
}
}
appendNumber(number) {
// 如果当前显示的是0,且输入的不是小数点,则替换0
if (this.currentOperand === '0' && number !== '.') {
this.currentOperand = number;
return;
}
// 防止输入多个小数点
if (number === '.' && this.currentOperand.includes('.')) return;
this.currentOperand += number;
}
chooseOperation(operation) {
if (this.currentOperand === '') return;
if (this.previousOperand !== '') {
this.compute();
}
this.operation = operation;
this.previousOperand = this.currentOperand;
this.currentOperand = '';
}
compute() {
let computation;
const prev = parseFloat(this.previousOperand);
const current = parseFloat(this.currentOperand);
if (isNaN(prev) || isNaN(current)) return;
switch (this.operation) {
case '+':
computation = prev + current;
break;
case '-':
computation = prev - current;
break;
case '×':
computation = prev * current;
break;
case '÷':
if (current === 0) {
alert("错误:不能除以零");
return;
}
computation = prev / current;
break;
default:
return;
}
// 限制小数位数
this.currentOperand = this.formatNumber(computation);
this.operation = undefined;
this.previousOperand = '';
}
formatNumber(number) {
// 如果是整数,直接返回
if (Number.isInteger(number)) {
return number.toString();
}
// 限制最多显示10位小数
return parseFloat(number.toFixed(10)).toString();
}
updateDisplay() {
this.currentOperandElement.innerText = this.currentOperand;
if (this.operation != null) {
this.previousOperandElement.innerText =
`${this.previousOperand} ${this.operation}`;
} else {
this.previousOperandElement.innerText = this.previousOperand;
}
}
}
// 获取DOM元素
const numberButtons = document.querySelectorAll('[data-number]');
const operationButtons = document.querySelectorAll('[data-operation]');
const equalsButton = document.querySelector('[data-equals]');
const deleteButton = document.querySelector('.delete');
const clearButton = document.querySelector('.clear');
const decimalButton = document.querySelector('[data-decimal]');
const previousOperandElement = document.querySelector('.previous-operand');
const currentOperandElement = document.querySelector('.current-operand');
// 创建计算器实例
const calculator = new Calculator(previousOperandElement, currentOperandElement);
// 绑定事件监听器
numberButtons.forEach(button => {
button.addEventListener('click', () => {
calculator.appendNumber(button.innerText);
calculator.updateDisplay();
});
});
operationButtons.forEach(button => {
button.addEventListener('click', () => {
calculator.chooseOperation(button.innerText);
calculator.updateDisplay();
});
});
equalsButton.addEventListener('click', () => {
calculator.compute();
calculator.updateDisplay();
});
clearButton.addEventListener('click', () => {
calculator.clear();
calculator.updateDisplay();
});
deleteButton.addEventListener('click', () => {
calculator.delete();
calculator.updateDisplay();
});
decimalButton.addEventListener('click', () => {
calculator.appendNumber('.');
calculator.updateDisplay();
});
// 键盘支持
document.addEventListener('keydown', (event) => {
if (event.key >= 0 && event.key <= 9) {
calculator.appendNumber(event.key);
calculator.updateDisplay();
}
if (event.key === '.') {
calculator.appendNumber('.');
calculator.updateDisplay();
}
if (event.key === '+' || event.key === '-' || event.key === '*' || event.key === '/') {
let operation;
switch (event.key) {
case '+': operation = '+'; break;
case '-': operation = '-'; break;
case '*': operation = '×'; break;
case '/': operation = '÷'; break;
}
calculator.chooseOperation(operation);
calculator.updateDisplay();
}
if (event.key === 'Enter' || event.key === '=') {
calculator.compute();
calculator.updateDisplay();
}
if (event.key === 'Backspace') {
calculator.delete();
calculator.updateDisplay();
}
if (event.key === 'Escape') {
calculator.clear();
calculator.updateDisplay();
}
});
// 初始显示
calculator.updateDisplay();
</script>
</body>
</html>
2.测试脚本开发
首先UI自动化测试根据浏览器下载驱动
Selenium+WebDriver 各浏览器驱动下载与使用 - 苏念雨 - 博客园
每个不通的浏览器,浏览器种类,版本,对应的驱动版本都不同 公司火狐浏览器版本140.0.6 需要的geckodriver版本:0.35.0
下载地址:https://github.com/mozilla/geckodriver/releases/download/v0.35.0/geckodriver-v0.35.0-win64.zip
依赖添加
<!-- Selenium 3 -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
脚本开发
package javao.cn;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import java.util.concurrent.TimeUnit;
public class CalculatorSeleniumTest {
private static WebDriver driver;
private static final String CALCULATOR_URL = "file:///D:/360MoveData/Users/programmer3.cc.vwed/Desktop/%E8%AE%A1%E7%AE%97%E5%99%A8.html";
public static void main(String[] args) {
System.out.println("=== 开始计算器自动化测试 ===");
// 设置Firefox驱动路径
System.setProperty("webdriver.gecko.driver", "D:\\快速开开发框架\\jshop-crawler-java_new\\geckodriver.exe");
// 如果geckodriver已经在系统PATH中,可以注释掉上面这行
try {
// 1. 启动浏览器
driver = new FirefoxDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.manage().window().maximize();
// 2. 打开计算器页面
driver.get(CALCULATOR_URL);
System.out.println("已打开计算器页面");
Thread.sleep(2000); // 等待页面加载
// 3. 运行测试用例
runAllTests();
} catch (Exception e) {
System.err.println("测试过程中出现错误: " + e.getMessage());
e.printStackTrace();
} finally {
// 关闭浏览器
if (driver != null) {
try {
Thread.sleep(3000);
driver.quit();
System.out.println("\n浏览器已关闭");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static void runAllTests() throws InterruptedException {
int passed = 0;
int failed = 0;
System.out.println("\n--- 测试用例开始执行 ---");
// 测试1: 验证页面元素
if (testPageElements()) {
System.out.println("✓ 测试1: 页面元素验证 - 通过");
passed++;
} else {
System.out.println("✗ 测试1: 页面元素验证 - 失败");
failed++;
}
// 测试2: 加法测试
if (testAddition(5, 3, "8")) {
System.out.println("✓ 测试2: 加法测试 (5+3=8) - 通过");
passed++;
} else {
System.out.println("✗ 测试2: 加法测试 - 失败");
failed++;
}
// 测试3: 减法测试
if (testSubtraction(9, 4, "5")) {
System.out.println("✓ 测试3: 减法测试 (9-4=5) - 通过");
passed++;
} else {
System.out.println("✗ 测试3: 减法测试 - 失败");
failed++;
}
// 测试4: 乘法测试
if (testMultiplication(6, 7, "42")) {
System.out.println("✓ 测试4: 乘法测试 (6×7=42) - 通过");
passed++;
} else {
System.out.println("✗ 测试4: 乘法测试 - 失败");
failed++;
}
// 测试5: 除法测试
if (testDivision(8, 2, "4")) {
System.out.println("✓ 测试5: 除法测试 (8÷2=4) - 通过");
passed++;
} else {
System.out.println("✗ 测试5: 除法测试 - 失败");
failed++;
}
// 测试6: 清除功能测试
if (testClearFunction()) {
System.out.println("✓ 测试6: 清除功能测试 - 通过");
passed++;
} else {
System.out.println("✗ 测试6: 清除功能测试 - 失败");
failed++;
}
// 测试7: 删除功能测试
if (testDeleteFunction()) {
System.out.println("✓ 测试7: 删除功能测试 - 通过");
passed++;
} else {
System.out.println("✗ 测试7: 删除功能测试 - 失败");
failed++;
}
// 测试8: 小数运算测试
if (testDecimalOperation()) {
System.out.println("✓ 测试8: 小数运算测试 - 通过");
passed++;
} else {
System.out.println("✗ 测试8: 小数运算测试 - 失败");
failed++;
}
// 测试9: 除以零测试
if (testDivisionByZero()) {
System.out.println("✓ 测试9: 除以零测试 - 通过");
passed++;
} else {
System.out.println("✗ 测试9: 除以零测试 - 失败");
failed++;
}
// 测试10: 复杂运算测试
if (testComplexCalculation()) {
System.out.println("✓ 测试10: 复杂运算测试 - 通过");
passed++;
} else {
System.out.println("✗ 测试10: 复杂运算测试 - 失败");
failed++;
}
// 显示测试结果
System.out.println("\n=== 测试结果汇总 ===");
System.out.println("总测试用例: " + (passed + failed));
System.out.println("通过: " + passed);
System.out.println("失败: " + failed);
System.out.println("成功率: " + (passed * 100 / (passed + failed)) + "%");
if (failed == 0) {
System.out.println("\n🎉 所有测试用例通过!");
} else {
System.out.println("\n⚠️ 有 " + failed + " 个测试用例失败");
}
}
// 测试1: 验证页面元素
private static boolean testPageElements() {
try {
// 验证页面标题
String title = driver.getTitle();
if (!"简易计算器".equals(title)) {
System.out.println(" 错误: 页面标题不正确,期望: 简易计算器,实际: " + title);
return false;
}
// 验证计算器标题
WebElement calculatorTitle = driver.findElement(By.className("calculator-title"));
String titleText = calculatorTitle.getText();
if (!"简易计算器".equals(titleText)) {
System.out.println(" 错误: 计算器标题不正确,期望: 简易计算器,实际: " + titleText);
return false;
}
// 验证初始显示
WebElement display = driver.findElement(By.className("current-operand"));
String displayText = display.getText();
if (!"0".equals(displayText)) {
System.out.println(" 错误: 初始显示值不是0,实际: " + displayText);
return false;
}
return true;
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试2: 加法测试
private static boolean testAddition(int a, int b, String expected) {
try {
clearCalculator();
clickNumber(a);
clickOperator("+");
clickNumber(b);
clickEquals();
String result = getCurrentDisplay();
if (expected.equals(result)) {
return true;
} else {
System.out.println(" 错误: " + a + " + " + b + " = " + result + " (期望: " + expected + ")");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试3: 减法测试
private static boolean testSubtraction(int a, int b, String expected) {
try {
clearCalculator();
clickNumber(a);
clickOperator("-");
clickNumber(b);
clickEquals();
String result = getCurrentDisplay();
if (expected.equals(result)) {
return true;
} else {
System.out.println(" 错误: " + a + " - " + b + " = " + result + " (期望: " + expected + ")");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试4: 乘法测试
private static boolean testMultiplication(int a, int b, String expected) {
try {
clearCalculator();
clickNumber(a);
clickOperator("×");
clickNumber(b);
clickEquals();
String result = getCurrentDisplay();
if (expected.equals(result)) {
return true;
} else {
System.out.println(" 错误: " + a + " × " + b + " = " + result + " (期望: " + expected + ")");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试5: 除法测试
private static boolean testDivision(int a, int b, String expected) {
try {
clearCalculator();
clickNumber(a);
clickOperator("÷");
clickNumber(b);
clickEquals();
String result = getCurrentDisplay();
if (expected.equals(result)) {
return true;
} else {
System.out.println(" 错误: " + a + " ÷ " + b + " = " + result + " (期望: " + expected + ")");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试6: 清除功能测试
private static boolean testClearFunction() {
try {
// 输入一些数字
clickNumber(1);
clickNumber(2);
clickNumber(3);
// 点击清除按钮
WebElement clearBtn = driver.findElement(By.className("clear"));
clearBtn.click();
Thread.sleep(500);
// 验证显示是否为0
String result = getCurrentDisplay();
if ("0".equals(result)) {
return true;
} else {
System.out.println(" 错误: 清除后显示 " + result + " (期望: 0)");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试7: 删除功能测试
private static boolean testDeleteFunction() {
try {
clearCalculator();
// 输入1234
clickNumber(1);
clickNumber(2);
clickNumber(3);
clickNumber(4);
// 点击删除按钮
WebElement deleteBtn = driver.findElement(By.className("delete"));
deleteBtn.click();
Thread.sleep(300);
// 验证删除一位后的结果
String result = getCurrentDisplay();
if ("123".equals(result)) {
// 继续删除直到0
deleteBtn.click(); // 删除3
deleteBtn.click(); // 删除2
deleteBtn.click(); // 删除1
Thread.sleep(300);
result = getCurrentDisplay();
if ("0".equals(result)) {
return true;
} else {
System.out.println(" 错误: 多次删除后显示 " + result + " (期望: 0)");
return false;
}
} else {
System.out.println(" 错误: 删除后显示 " + result + " (期望: 123)");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试8: 小数运算测试
private static boolean testDecimalOperation() {
try {
clearCalculator();
// 输入 3.14 + 2.86 = 6
clickNumber(3);
clickDecimal();
clickNumber(1);
clickNumber(4);
clickOperator("+");
clickNumber(2);
clickDecimal();
clickNumber(8);
clickNumber(6);
clickEquals();
String result = getCurrentDisplay();
if ("6".equals(result)) {
return true;
} else {
System.out.println(" 错误: 3.14 + 2.86 = " + result + " (期望: 6)");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试9: 除以零测试
private static boolean testDivisionByZero() {
try {
clearCalculator();
// 输入 5 ÷ 0
clickNumber(5);
clickOperator("÷");
clickNumber(0);
clickEquals();
// 等待alert出现
Thread.sleep(1000);
// 检查alert
try {
Alert alert = driver.switchTo().alert();
String alertText = alert.getText();
if (alertText.contains("不能除以零")) {
alert.accept();
return true;
} else {
System.out.println(" 错误: 警告信息不正确: " + alertText);
alert.accept();
return false;
}
} catch (NoAlertPresentException e) {
System.out.println(" 错误: 除以零时未弹出警告框");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// 测试10: 复杂运算测试
private static boolean testComplexCalculation() {
try {
clearCalculator();
// (12 + 8) × 2 ÷ 5 = 8
clickNumber(1);
clickNumber(2);
clickOperator("+");
clickNumber(8);
clickEquals(); // 20
clickOperator("×");
clickNumber(2);
clickEquals(); // 40
clickOperator("÷");
clickNumber(5);
clickEquals(); // 8
String result = getCurrentDisplay();
if ("8".equals(result)) {
return true;
} else {
System.out.println(" 错误: (12+8)×2÷5 = " + result + " (期望: 8)");
return false;
}
} catch (Exception e) {
System.out.println(" 错误: " + e.getMessage());
return false;
}
}
// ============== 辅助方法 ==============
private static void clickNumber(int number) throws InterruptedException {
String xpath = String.format("//button[@data-number='%d']", number);
WebElement btn = driver.findElement(By.xpath(xpath));
btn.click();
Thread.sleep(200); // 等待点击生效
}
private static void clickOperator(String operator) throws InterruptedException {
String xpath = String.format("//button[@data-operation='%s']", operator);
WebElement btn = driver.findElement(By.xpath(xpath));
btn.click();
Thread.sleep(200);
}
private static void clickEquals() throws InterruptedException {
WebElement equalsBtn = driver.findElement(By.cssSelector("[data-equals]"));
equalsBtn.click();
Thread.sleep(500); // 等待计算结果
}
private static void clickDecimal() throws InterruptedException {
WebElement decimalBtn = driver.findElement(By.cssSelector("[data-decimal]"));
decimalBtn.click();
Thread.sleep(200);
}
private static void clearCalculator() throws InterruptedException {
WebElement clearBtn = driver.findElement(By.className("clear"));
clearBtn.click();
Thread.sleep(300); // 等待清除完成
}
private static String getCurrentDisplay() {
try {
WebElement display = driver.findElement(By.className("current-operand"));
return display.getText();
} catch (Exception e) {
return "";
}
}
// ============== 键盘输入测试 ==============
private static void testKeyboardInput() {
System.out.println("\n--- 键盘输入测试 ---");
try {
clearCalculator();
// 获取显示区域元素
WebElement display = driver.findElement(By.className("current-operand"));
// 点击显示区域获得焦点
display.click();
Thread.sleep(500);
// 使用键盘输入 25+13
display.sendKeys("25+13");
// 按回车键
display.sendKeys(Keys.ENTER);
Thread.sleep(1000);
String result = getCurrentDisplay();
if ("38".equals(result)) {
System.out.println("✓ 键盘输入测试: 25+13=38 - 通过");
} else {
System.out.println("✗ 键盘输入测试: 25+13=" + result + " (期望: 38) - 失败");
}
// 测试ESC键清除
display.sendKeys(Keys.ESCAPE);
Thread.sleep(500);
result = getCurrentDisplay();
if ("0".equals(result)) {
System.out.println("✓ ESC键清除功能 - 通过");
} else {
System.out.println("✗ ESC键清除功能: 显示 " + result + " (期望: 0) - 失败");
}
} catch (Exception e) {
System.out.println("键盘输入测试错误: " + e.getMessage());
}
}
// ============== 边界测试 ==============
private static void testBoundaryCases() {
System.out.println("\n--- 边界测试 ---");
try {
// 测试前导零
clearCalculator();
clickNumber(0);
clickNumber(0);
clickNumber(5);
String result = getCurrentDisplay();
if ("5".equals(result)) {
System.out.println("✓ 前导零处理测试 - 通过");
} else {
System.out.println("✗ 前导零处理测试: 显示 " + result + " (期望: 5) - 失败");
}
// 测试多个小数点
clearCalculator();
clickNumber(3);
clickDecimal();
clickNumber(1);
clickDecimal(); // 再次点击小数点应该无效
clickNumber(4);
result = getCurrentDisplay();
if ("3.14".equals(result)) {
System.out.println("✓ 多个小数点处理测试 - 通过");
} else {
System.out.println("✗ 多个小数点处理测试: 显示 " + result + " (期望: 3.14) - 失败");
}
// 测试0的运算
clearCalculator();
clickNumber(0);
clickOperator("+");
clickNumber(7);
clickEquals();
result = getCurrentDisplay();
if ("7".equals(result)) {
System.out.println("✓ 0的运算测试 - 通过");
} else {
System.out.println("✗ 0的运算测试: 显示 " + result + " (期望: 7) - 失败");
}
} catch (Exception e) {
System.out.println("边界测试错误: " + e.getMessage());
}
}
}
