Tự động hóa giao diện người dùng trên Android - Phần thực hành nâng cao

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.

Thẻ: Android automation spring-boot WebSocket testing-framework

Đăng vào ngày 13 tháng 9 lúc 00:36