Trong phần trước, chúng ta đã tìm hiểu về việc phát triển mở rộng trên nền tảng solopi. Phần này sẽ đi sâu vào nguyên lý hoạt động của phía server.
Kiến trúc hệ thống
Hệ thống sử dụng framework nguồn mở Pear Admin Boot với độ trừu tượng hóa cao nhằm giảm thiểu công sức phát triển bổ sung. Framework này được xây dựng dựa trên hệ sinh thái Spring Boot và tích hợp sẵn các tính năng quản lý quyền truy cập cũng như luồng công việc.
Chức năng chính của server:
- Giao tiếp hai chiều với client thông qua WebSocket
- Theo dõi trạng thái thiết bị kết nối
- Quản lý tập trung kịch bản kiểm thử giữa nhiều thiết bị
- Thực thi kịch bản sau khi xử lý mẫu dữ liệu
- Nhận và hiển thị báo cáo kiểm thử từ thiết bị
Cấu hình giao tiếp WebSocket
Đầu tiên cần cấu hình endpoint cho WebSocket. Với ví dụ dưới đây, địa chỉ kết nối từ client sẽ là ws://xxx.xxx.xxx.xxx:8080/realtime
@Component
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
@Autowired
private MessageHandler messageHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(messageHandler, "/realtime")
.setAllowedOrigins("*");
}
}
Xử lý thông tin thiết bị
Khi nhận được thông tin thiết bị từ client, server sẽ lưu trữ thông tin và đánh dấu trạng thái trực tuyến:
// Chuyển đổi dữ liệu nhận được thành map
Map<String, Object> dataMap = JSONUtil.parseObj(payload);
for (String key : dataMap.keySet()) {
String jsonData = dataMap.get(key).toString();
if ("deviceInfo".equals(key)) {
Device deviceData = JSONUtil.toBean(jsonData, Device.class);
String deviceId = deviceData.getSerialNumber();
updateOnlineStatus(deviceId, session);
Device existingDevice = deviceService.findBySerial(deviceId);
if (existingDevice == null) {
deviceData.setOnline(true);
deviceData.setSessionId(session.getId());
deviceData.setLastUpdated(new Date());
deviceData.setCreated(new Date());
deviceService.save(deviceData);
} else {
deviceData.setId(existingDevice.getId());
deviceData.setOnline(true);
deviceData.setSessionId(session.getId());
deviceData.setLastUpdated(new Date());
deviceService.update(deviceData);
}
}
}
Khi ngắt kết nối, cập nhật trạng thái ngoại tuyến:
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
Device device = deviceService.findBySession(session.getId());
device.setOnline(false);
device.setLastUpdated(new Date());
deviceService.update(device);
updateOfflineStatus(device.getSerialNumber(), session);
super.afterConnectionClosed(session, status);
}
Các phương thức quản lý session:
private static final Map<String, WebSocketSession> SESSION_STORE = new ConcurrentHashMap<>();
private static final Map<String, String> DEVICE_SESSION_MAP = new ConcurrentHashMap<>();
private static final Map<String, String> SESSION_DEVICE_MAP = new ConcurrentHashMap<>();
private void updateOnlineStatus(String deviceId, WebSocketSession session) {
SESSION_STORE.put(deviceId, session);
DEVICE_SESSION_MAP.put(deviceId, session.getId());
SESSION_DEVICE_MAP.put(session.getId(), deviceId);
}
private void updateOfflineStatus(String deviceId, WebSocketSession session) {
SESSION_STORE.remove(deviceId);
DEVICE_SESSION_MAP.remove(deviceId);
SESSION_DEVICE_MAP.remove(session.getId());
}
Quản lý kịch bản kiểm thử
Xử lý thông tin kịch bản với tên kịch bản làm khóa duy nhất:
else if ("testCase".equals(key)) {
JSONArray testCaseArray = JSONUtil.parseArray(jsonData);
for (int i = 0; i < testCaseArray.size(); i++) {
JSONObject testCaseObj = testCaseArray.getJSONObject(i);
TestCase testCase = testCaseObj.toBean(TestCase.class);
TestCase existingCase = testCaseService.findByNameAndPackage(
testCase.getName(), testCase.getAppPackage()
);
if (existingCase == null) {
testCase.setCreated(new Date());
testCase.setModified(new Date());
testCase.setRawData(testCaseObj.toString());
testCaseService.create(testCase);
} else {
testCase.setId(existingCase.getId());
testCase.setModified(new Date());
testCase.setRawData(testCaseObj.toString());
testCaseService.update(testCase);
}
}
}
Triển khai kịch bản kiểm thử
Quy trình thực thi bao gồm: chọn kịch bản → chọn thiết bị → tạo nhiệm vụ → thay thế mẫu → gửi đến thiết bị
Mối quan hệ dữ liệu:
- 1 nhiệm vụ : N thiết bị
- 1 nhiệm vụ : N kịch bản
- 1 nhiệm vụ : N báo cáo
Mã xử lý cốt lõi
@PostMapping("/run")
@ResponseBody
public ResponseEntity<Integer> executeTest(@RequestBody ExecutionRequest request) {
User currentUser = getCurrentUser();
// Tạo nhiệm vụ mới
Task task = new Task();
task.setTaskId(generateId());
task.setTestCaseCount(request.getTestCaseIds().size());
task.setDeviceCount(request.getDeviceIds().size());
task.setCreatedAt(new Date());
task.setUpdatedAt(new Date());
task.setStatus(TaskStatus.PENDING);
task.setCreatorId(currentUser.getId());
task.setName(request.getTaskName());
// Liên kết kịch bản với nhiệm vụ
request.getTestCaseIds().forEach(caseId -> {
TaskTestCaseLink link = new TaskTestCaseLink();
link.setId(generateId());
link.setTestCaseId(caseId);
link.setTaskId(task.getTaskId());
link.setCreatedAt(new Date());
link.setUpdatedAt(new Date());
taskTestCaseLinkService.save(link);
});
// Xử lý mẫu dữ liệu
List<Template> availableTemplates = templateService.findByKeys("login.phone,login.pwd");
List<String> usedValues = new ArrayList<>();
List<String> usedIds = new ArrayList<>();
// Liên kết thiết bị với nhiệm vụ
List<WebSocketSession> targetSessions = new ArrayList<>();
List<WebSocketSession> sessionsNeedingReplacement = new ArrayList<>();
request.getDeviceIds().forEach(deviceId -> {
TaskDeviceLink deviceLink = new TaskDeviceLink();
deviceLink.setId(generateId());
deviceLink.setDeviceId(deviceId);
deviceLink.setTaskId(task.getTaskId());
deviceLink.setCreatedAt(new Date());
deviceLink.setUpdatedAt(new Date());
taskDeviceLinkService.save(deviceLink);
Device device = deviceService.findById(deviceId);
WebSocketSession session = SESSION_STORE.get(device.getSerialNumber());
targetSessions.add(session);
if (request.getDevicesUsingTemplates().contains(deviceId)) {
sessionsNeedingReplacement.add(session);
if (!availableTemplates.isEmpty()) {
List<String> templateIds = availableTemplates.stream()
.map(Template::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
templateIds.removeAll(usedIds);
List<String> templateValues = availableTemplates.stream()
.map(Template::getValue)
.filter(Objects::nonNull)
.collect(Collectors.toList());
templateValues.removeAll(usedValues);
if (!templateValues.isEmpty()) {
String selectedValue = templateValues.get(0);
String selectedId = templateIds.get(0);
usedValues.add(selectedValue);
usedIds.add(selectedId);
// Liên kết thiết bị với mẫu
DeviceTemplateLink deviceTemplate = new DeviceTemplateLink();
deviceTemplate.setId(generateId());
deviceTemplate.setTemplateId(selectedId);
deviceTemplate.setDeviceId(deviceId);
deviceTemplate.setCreatedAt(new Date());
deviceTemplate.setUpdatedAt(new Date());
deviceTemplateLinkService.save(deviceTemplate);
// Liên kết nhiệm vụ với mẫu
TaskTemplateLink taskTemplate = new TaskTemplateLink();
taskTemplate.setId(generateId());
taskTemplate.setTemplateId(selectedId);
taskTemplate.setTaskId(task.getTaskId());
taskTemplate.setCreatedAt(new Date());
taskTemplate.setUpdatedAt(new Date());
taskTemplateLinkService.save(taskTemplate);
}
}
}
});
// Lưu nhiệm vụ
taskService.save(task);
// Gửi kịch bản đến thiết bị
targetSessions.forEach(session -> {
try {
ExecutionPayload payload = new ExecutionPayload();
List<String> testCaseData = new ArrayList<>();
request.getTestCaseIds().forEach(caseId -> {
TestCase testCase = testCaseService.findById(caseId);
if (testCase != null) {
String rawData = testCase.getRawData();
if (rawData != null &&
(rawData.contains("login.phone") || rawData.contains("login.pwd")) &&
sessionsNeedingReplacement.contains(session)) {
rawData = processTemplate(rawData);
}
testCaseData.add(rawData);
}
});
payload.setTestCases(testCaseData);
payload.setTaskId(task.getTaskId());
Map<String, ExecutionPayload> message = new HashMap<>();
message.put("executeTests", payload);
String jsonMessage = JSONUtil.toJsonStr(message);
session.sendMessage(new TextMessage(jsonMessage));
} catch (IOException e) {
e.printStackTrace();
}
});
usedValues.clear();
return ResponseEntity.ok(0);
}
Xử lý báo cáo kiểm thử
Sau khi thực thi xong, thiết bị sẽ tải lên kết quả kiểm thử:
else if (key.contains("testResults")) {
String taskId = key.substring(16);
Task task = taskService.findById(taskId);
String deviceSerial = SESSION_DEVICE_MAP.get(session.getId());
JSONArray resultsArray = JSONUtil.parseArray(dataMap.get(key).toString());
if (task != null) {
for (int i = 0; i < resultsArray.size(); i++) {
JSONObject result = resultsArray.getJSONObject(i);
TestReport report = new TestReport();
report.setTaskId(taskId);
report.setRawContent(dataMap.get(key).toString());
report.setDeviceSerial(deviceSerial);
report.setTestCaseName(result.getStr("testCaseName"));
testReportService.save(report);
}
}
}
Báo cáo cuối cùng được hiển thị trên giao diện người dùng.