ThingsBoard物联网平台白盒测试¶
1. 白盒测试概述¶
1.1 测试目标¶
白盒测试的目标是通过分析ThingsBoard物联网平台的内部代码结构、逻辑流程和实现细节,验证代码的正确性、完整性和质量,确保系统在代码层面满足设计要求。
1.2 测试范围¶
- 代码结构分析
- 逻辑流程测试
- 数据流测试
- 控制流测试
- 代码覆盖率测试
- 静态代码分析
- 动态代码分析
1.3 测试原则¶
- 代码可见性:基于源代码进行测试设计
- 逻辑完整性:验证所有代码路径和分支
- 数据完整性:验证数据处理的正确性
- 异常处理:验证异常情况的处理逻辑
- 性能优化:识别性能瓶颈和优化点
2. 代码覆盖率测试¶
2.1 覆盖率类型¶
2.1.1 语句覆盖率(Statement Coverage)¶
验证每行代码是否被执行
// 示例代码
public class DeviceService {
public Device createDevice(String name, String type) {
if (name == null || name.isEmpty()) { // 语句1
throw new IllegalArgumentException("设备名称不能为空"); // 语句2
}
if (type == null || type.isEmpty()) { // 语句3
type = "Default"; // 语句4
}
Device device = new Device(name, type); // 语句5
return device; // 语句6
}
}
测试用例设计: - 测试用例1:name=null,type=null → 覆盖语句1,2 - 测试用例2:name="TestDevice",type=null → 覆盖语句1,3,4,5,6 - 测试用例3:name="TestDevice",type="Sensor" → 覆盖语句1,3,5,6
2.1.2 分支覆盖率(Branch Coverage)¶
验证每个条件分支是否被执行
public class AuthenticationService {
public boolean authenticate(String username, String password) {
if (username == null || password == null) { // 分支1
return false;
}
if (username.equals("admin") && password.equals("admin123")) { // 分支2
return true;
}
return false; // 分支3
}
}
测试用例设计: - 测试用例1:username=null → 覆盖分支1 - 测试用例2:username="admin", password="admin123" → 覆盖分支2 - 测试用例3:username="user", password="wrong" → 覆盖分支3
2.1.3 路径覆盖率(Path Coverage)¶
验证所有可能的执行路径
public class RuleEngine {
public String processRule(String deviceType, int temperature) {
if (deviceType.equals("Sensor")) { // 路径1开始
if (temperature > 50) { // 路径1.1
return "High Temperature Alert";
} else if (temperature < 0) { // 路径1.2
return "Low Temperature Alert";
} else { // 路径1.3
return "Normal Temperature";
}
} else { // 路径2
return "Unknown Device Type";
}
}
}
测试用例设计: - 测试用例1:deviceType="Sensor", temperature=60 → 路径1.1 - 测试用例2:deviceType="Sensor", temperature=-5 → 路径1.2 - 测试用例3:deviceType="Sensor", temperature=25 → 路径1.3 - 测试用例4:deviceType="Actuator", temperature=30 → 路径2
2.1.4 条件覆盖率(Condition Coverage)¶
验证每个条件的真假值组合
public class DeviceValidator {
public boolean validateDevice(String name, String type, boolean isActive) {
if ((name != null && !name.isEmpty()) &&
(type != null && !type.isEmpty()) &&
isActive) { // 条件1 && 条件2 && 条件3
return true;
}
return false;
}
}
测试用例设计: - 测试用例1:name="Test", type="Sensor", isActive=true → T&&T&&T - 测试用例2:name=null, type="Sensor", isActive=true → F&&T&&T - 测试用例3:name="Test", type=null, isActive=true → T&&F&&T - 测试用例4:name="Test", type="Sensor", isActive=false → T&&T&&F
2.2 覆盖率工具¶
2.2.1 JaCoCo(Java代码覆盖率)¶
<!-- Maven配置 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
2.2.2 Istanbul(JavaScript代码覆盖率)¶
{
"scripts": {
"test": "nyc mocha test/**/*.js",
"coverage": "nyc report --reporter=html"
},
"devDependencies": {
"nyc": "^15.1.0",
"mocha": "^8.2.1"
}
}
2.2.3 Coverage.py(Python代码覆盖率)¶
# 安装coverage
pip install coverage
# 运行测试并生成覆盖率报告
coverage run -m pytest
coverage report
coverage html
3. 静态代码分析¶
3.1 代码质量分析¶
3.1.1 复杂度分析¶
// 高复杂度示例
public class ComplexDeviceManager {
public String processDeviceData(String deviceId, String dataType,
Map<String, Object> data,
List<String> rules) {
if (deviceId != null && !deviceId.isEmpty()) {
if (dataType != null && !dataType.isEmpty()) {
if (data != null && !data.isEmpty()) {
if (rules != null && !rules.isEmpty()) {
for (String rule : rules) {
if (rule.startsWith("temp")) {
if (data.containsKey("temperature")) {
Double temp = (Double) data.get("temperature");
if (temp != null) {
if (temp > 50) {
return "High Temperature Alert";
} else if (temp < 0) {
return "Low Temperature Alert";
} else {
return "Normal Temperature";
}
}
}
}
}
}
}
}
}
return "Invalid Data";
}
}
复杂度问题: - 嵌套层级过深(8层) - 条件判断过多 - 单个方法过长 - 职责不单一
重构建议:
public class DeviceManager {
public String processDeviceData(String deviceId, String dataType,
Map<String, Object> data,
List<String> rules) {
if (!isValidInput(deviceId, dataType, data, rules)) {
return "Invalid Data";
}
return processRules(data, rules);
}
private boolean isValidInput(String deviceId, String dataType,
Map<String, Object> data,
List<String> rules) {
return deviceId != null && !deviceId.isEmpty() &&
dataType != null && !dataType.isEmpty() &&
data != null && !data.isEmpty() &&
rules != null && !rules.isEmpty();
}
private String processRules(Map<String, Object> data, List<String> rules) {
for (String rule : rules) {
if (rule.startsWith("temp")) {
return processTemperatureRule(data);
}
}
return "No Matching Rule";
}
private String processTemperatureRule(Map<String, Object> data) {
Double temp = (Double) data.get("temperature");
if (temp == null) {
return "Temperature Data Missing";
}
if (temp > 50) {
return "High Temperature Alert";
} else if (temp < 0) {
return "Low Temperature Alert";
} else {
return "Normal Temperature";
}
}
}
3.1.2 代码规范检查¶
// 不规范代码示例
public class DeviceService{
private String deviceName;
private int deviceId;
public DeviceService(String name,int id){
this.deviceName=name;
this.deviceId=id;
}
public String getDeviceName(){
return deviceName;
}
public void setDeviceName(String name){
this.deviceName=name;
}
public int getDeviceId(){
return deviceId;
}
public void setDeviceId(int id){
this.deviceId=id;
}
}
规范问题: - 缺少空格和换行 - 命名不规范 - 缺少注释 - 缺少访问修饰符
规范代码:
/**
* 设备服务类
* 负责设备的基本信息管理
*/
public class DeviceService {
private String deviceName;
private int deviceId;
/**
* 构造函数
* @param name 设备名称
* @param id 设备ID
*/
public DeviceService(String name, int id) {
this.deviceName = name;
this.deviceId = id;
}
/**
* 获取设备名称
* @return 设备名称
*/
public String getDeviceName() {
return deviceName;
}
/**
* 设置设备名称
* @param name 设备名称
*/
public void setDeviceName(String name) {
this.deviceName = name;
}
/**
* 获取设备ID
* @return 设备ID
*/
public int getDeviceId() {
return deviceId;
}
/**
* 设置设备ID
* @param id 设备ID
*/
public void setDeviceId(int id) {
this.deviceId = id;
}
}
3.2 静态分析工具¶
3.2.1 SonarQube¶
# sonar-project.properties
sonar.projectKey=thingsboard
sonar.projectName=ThingsBoard IoT Platform
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.java.libraries=target/lib/*.jar
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.junit.reportPaths=target/surefire-reports
3.2.2 PMD¶
<!-- PMD规则配置 -->
<ruleset name="ThingsBoard Rules">
<rule ref="category/java/bestpractices.xml"/>
<rule ref="category/java/codestyle.xml"/>
<rule ref="category/java/design.xml"/>
<rule ref="category/java/errorprone.xml"/>
<rule ref="category/java/performance.xml"/>
</ruleset>
3.2.3 Checkstyle¶
<!-- checkstyle.xml -->
<module name="Checker">
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL"/>
<property name="format" value="\\u00ff"/>
</module>
<module name="AvoidEscapedUnicodeCharacters"/>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
</module>
</module>
</module>
4. 动态代码分析¶
4.1 内存分析¶
4.1.1 内存泄漏检测¶
public class DeviceManager {
private Map<String, Device> deviceCache = new HashMap<>();
private List<DeviceListener> listeners = new ArrayList<>();
public void addDevice(Device device) {
deviceCache.put(device.getId(), device);
notifyListeners(device);
}
public void removeDevice(String deviceId) {
Device device = deviceCache.remove(deviceId);
if (device != null) {
// 潜在内存泄漏:没有从listeners中移除相关监听器
notifyListeners(device);
}
}
private void notifyListeners(Device device) {
for (DeviceListener listener : listeners) {
listener.onDeviceChange(device);
}
}
}
内存泄漏问题: - 设备对象从缓存中移除,但监听器仍持有引用 - 监听器列表可能无限增长
修复方案:
public class DeviceManager {
private Map<String, Device> deviceCache = new HashMap<>();
private Map<String, List<DeviceListener>> deviceListeners = new HashMap<>();
public void addDevice(Device device) {
deviceCache.put(device.getId(), device);
notifyListeners(device);
}
public void removeDevice(String deviceId) {
Device device = deviceCache.remove(deviceId);
if (device != null) {
// 清理相关监听器
deviceListeners.remove(deviceId);
notifyListeners(device);
}
}
public void addDeviceListener(String deviceId, DeviceListener listener) {
deviceListeners.computeIfAbsent(deviceId, k -> new ArrayList<>())
.add(listener);
}
public void removeDeviceListener(String deviceId, DeviceListener listener) {
List<DeviceListener> listeners = deviceListeners.get(deviceId);
if (listeners != null) {
listeners.remove(listener);
if (listeners.isEmpty()) {
deviceListeners.remove(deviceId);
}
}
}
}
4.1.2 性能分析¶
public class DataProcessor {
public List<DeviceData> processData(List<RawData> rawDataList) {
List<DeviceData> result = new ArrayList<>();
for (RawData rawData : rawDataList) {
// 性能问题:在循环中创建大量对象
DeviceData deviceData = new DeviceData();
deviceData.setId(rawData.getId());
deviceData.setTimestamp(rawData.getTimestamp());
// 性能问题:字符串拼接
String processedValue = "";
for (String value : rawData.getValues()) {
processedValue += value + ",";
}
deviceData.setProcessedValue(processedValue);
result.add(deviceData);
}
return result;
}
}
性能问题: - 循环中创建大量对象 - 字符串拼接效率低 - 没有使用对象池
优化方案:
public class DataProcessor {
private final StringBuilder stringBuilder = new StringBuilder();
private final ObjectPool<DeviceData> deviceDataPool = new ObjectPool<>();
public List<DeviceData> processData(List<RawData> rawDataList) {
List<DeviceData> result = new ArrayList<>(rawDataList.size());
for (RawData rawData : rawDataList) {
DeviceData deviceData = deviceDataPool.borrowObject();
try {
deviceData.setId(rawData.getId());
deviceData.setTimestamp(rawData.getTimestamp());
// 使用StringBuilder提高性能
stringBuilder.setLength(0);
for (String value : rawData.getValues()) {
stringBuilder.append(value).append(",");
}
deviceData.setProcessedValue(stringBuilder.toString());
result.add(deviceData);
} finally {
deviceDataPool.returnObject(deviceData);
}
}
return result;
}
}
4.2 动态分析工具¶
4.2.1 JProfiler¶
// JProfiler集成示例
public class ProfiledService {
@Profiled
public void processDeviceData(List<DeviceData> dataList) {
for (DeviceData data : dataList) {
processSingleDevice(data);
}
}
@Profiled
private void processSingleDevice(DeviceData data) {
// 业务逻辑
validateData(data);
transformData(data);
saveData(data);
}
}
4.2.2 VisualVM¶
// VisualVM监控示例
public class MonitoringService {
private final AtomicLong processedCount = new AtomicLong(0);
private final AtomicLong errorCount = new AtomicLong(0);
public void processRequest(Request request) {
try {
// 业务处理
doProcess(request);
processedCount.incrementAndGet();
} catch (Exception e) {
errorCount.incrementAndGet();
throw e;
}
}
public long getProcessedCount() {
return processedCount.get();
}
public long getErrorCount() {
return errorCount.get();
}
}
5. 单元测试¶
5.1 测试用例设计¶
5.1.1 正常流程测试¶
@Test
public void testCreateDevice_Success() {
// Given
String deviceName = "Test Device";
String deviceType = "Sensor";
// When
Device device = deviceService.createDevice(deviceName, deviceType);
// Then
assertNotNull(device);
assertEquals(deviceName, device.getName());
assertEquals(deviceType, device.getType());
assertNotNull(device.getId());
}
5.1.2 异常流程测试¶
@Test
public void testCreateDevice_NullName() {
// Given
String deviceName = null;
String deviceType = "Sensor";
// When & Then
assertThrows(IllegalArgumentException.class, () -> {
deviceService.createDevice(deviceName, deviceType);
});
}
@Test
public void testCreateDevice_EmptyName() {
// Given
String deviceName = "";
String deviceType = "Sensor";
// When & Then
assertThrows(IllegalArgumentException.class, () -> {
deviceService.createDevice(deviceName, deviceType);
});
}
5.1.3 边界值测试¶
@Test
public void testCreateDevice_MaxLengthName() {
// Given
String deviceName = "A".repeat(255); // 最大长度
String deviceType = "Sensor";
// When
Device device = deviceService.createDevice(deviceName, deviceType);
// Then
assertNotNull(device);
assertEquals(deviceName, device.getName());
}
@Test
public void testCreateDevice_ExceedMaxLengthName() {
// Given
String deviceName = "A".repeat(256); // 超过最大长度
String deviceType = "Sensor";
// When & Then
assertThrows(IllegalArgumentException.class, () -> {
deviceService.createDevice(deviceName, deviceType);
});
}
5.2 Mock测试¶
5.2.1 Mock外部依赖¶
@ExtendWith(MockitoExtension.class)
class DeviceServiceTest {
@Mock
private DeviceRepository deviceRepository;
@Mock
private NotificationService notificationService;
@InjectMocks
private DeviceService deviceService;
@Test
public void testCreateDevice_WithNotification() {
// Given
String deviceName = "Test Device";
String deviceType = "Sensor";
Device savedDevice = new Device(deviceName, deviceType);
when(deviceRepository.save(any(Device.class))).thenReturn(savedDevice);
// When
Device result = deviceService.createDevice(deviceName, deviceType);
// Then
verify(deviceRepository).save(any(Device.class));
verify(notificationService).sendDeviceCreatedNotification(result);
assertEquals(deviceName, result.getName());
}
}
5.2.2 参数化测试¶
@ParameterizedTest
@ValueSource(strings = {"Sensor", "Actuator", "Gateway", "Controller"})
public void testCreateDevice_ValidTypes(String deviceType) {
// Given
String deviceName = "Test Device";
// When
Device device = deviceService.createDevice(deviceName, deviceType);
// Then
assertNotNull(device);
assertEquals(deviceType, device.getType());
}
@ParameterizedTest
@CsvSource({
"Sensor, true",
"Actuator, true",
"Gateway, true",
"Invalid, false"
})
public void testIsValidDeviceType(String deviceType, boolean expected) {
// When
boolean result = deviceService.isValidDeviceType(deviceType);
// Then
assertEquals(expected, result);
}
6. 集成测试¶
6.1 组件集成测试¶
@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
class DeviceIntegrationTest {
@Autowired
private DeviceService deviceService;
@Autowired
private DeviceRepository deviceRepository;
@Test
@Transactional
public void testCreateAndRetrieveDevice() {
// Given
String deviceName = "Integration Test Device";
String deviceType = "Sensor";
// When
Device createdDevice = deviceService.createDevice(deviceName, deviceType);
Device retrievedDevice = deviceService.getDeviceById(createdDevice.getId());
// Then
assertNotNull(retrievedDevice);
assertEquals(deviceName, retrievedDevice.getName());
assertEquals(deviceType, retrievedDevice.getType());
}
}
6.2 数据库集成测试¶
@DataJpaTest
class DeviceRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private DeviceRepository deviceRepository;
@Test
public void testFindByName() {
// Given
Device device = new Device("Test Device", "Sensor");
entityManager.persistAndFlush(device);
// When
Optional<Device> found = deviceRepository.findByName("Test Device");
// Then
assertTrue(found.isPresent());
assertEquals("Test Device", found.get().getName());
}
@Test
public void testFindByType() {
// Given
Device device1 = new Device("Device 1", "Sensor");
Device device2 = new Device("Device 2", "Actuator");
Device device3 = new Device("Device 3", "Sensor");
entityManager.persistAndFlush(device1);
entityManager.persistAndFlush(device2);
entityManager.persistAndFlush(device3);
// When
List<Device> sensors = deviceRepository.findByType("Sensor");
// Then
assertEquals(2, sensors.size());
assertTrue(sensors.stream().allMatch(d -> "Sensor".equals(d.getType())));
}
}
7. 测试工具和框架¶
7.1 测试框架¶
7.1.1 JUnit 5¶
@ExtendWith(MockitoExtension.class)
class DeviceServiceJUnit5Test {
@Mock
private DeviceRepository deviceRepository;
@InjectMocks
private DeviceService deviceService;
@BeforeEach
void setUp() {
// 测试前置设置
}
@AfterEach
void tearDown() {
// 测试后置清理
}
@Test
@DisplayName("创建设备 - 成功场景")
void createDevice_Success() {
// 测试实现
}
@Test
@DisplayName("创建设备 - 名称为空")
void createDevice_NullName() {
// 测试实现
}
@Test
@Timeout(value = 5, unit = TimeUnit.SECONDS)
void createDevice_Timeout() {
// 超时测试
}
}
7.1.2 TestNG¶
@Test
public class DeviceServiceTestNGTest {
@BeforeClass
public void setUpClass() {
// 类级别设置
}
@BeforeMethod
public void setUpMethod() {
// 方法级别设置
}
@Test(groups = "unit")
public void testCreateDevice() {
// 测试实现
}
@Test(groups = "integration", dependsOnMethods = "testCreateDevice")
public void testUpdateDevice() {
// 测试实现
}
@DataProvider(name = "deviceData")
public Object[][] getDeviceData() {
return new Object[][] {
{"Sensor", "Temperature Sensor"},
{"Actuator", "Light Controller"},
{"Gateway", "IoT Gateway"}
};
}
@Test(dataProvider = "deviceData")
public void testCreateDeviceWithData(String type, String name) {
// 参数化测试
}
}
7.2 测试工具¶
7.2.1 AssertJ¶
@Test
public void testDeviceAssertions() {
Device device = deviceService.createDevice("Test Device", "Sensor");
// AssertJ断言
assertThat(device)
.isNotNull()
.hasFieldOrPropertyWithValue("name", "Test Device")
.hasFieldOrPropertyWithValue("type", "Sensor")
.hasFieldOrProperty("id")
.hasFieldOrProperty("createdAt");
assertThat(device.getName())
.isNotEmpty()
.hasSize(11)
.startsWith("Test")
.endsWith("Device");
}
7.2.2 WireMock¶
@Test
public void testExternalApiCall() {
// 配置WireMock
stubFor(get(urlEqualTo("/api/devices"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":\"123\",\"name\":\"Test Device\"}")));
// 测试外部API调用
Device device = deviceService.getDeviceFromExternalApi("123");
assertThat(device).isNotNull();
assertThat(device.getId()).isEqualTo("123");
assertThat(device.getName()).isEqualTo("Test Device");
}
8. 测试报告和分析¶
8.1 覆盖率报告¶
<!-- Maven配置生成覆盖率报告 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>**/model/**</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/config/**</exclude>
</excludes>
</configuration>
</plugin>
8.2 测试质量分析¶
// 测试质量指标
public class TestQualityMetrics {
private int totalTests;
private int passedTests;
private int failedTests;
private int skippedTests;
private double codeCoverage;
private double branchCoverage;
private double mutationScore;
public double getTestPassRate() {
return (double) passedTests / totalTests * 100;
}
public double getTestCoverage() {
return codeCoverage;
}
public boolean isQualityAcceptable() {
return getTestPassRate() >= 95.0 &&
codeCoverage >= 80.0 &&
branchCoverage >= 70.0;
}
}
9. 最佳实践¶
9.1 测试设计原则¶
- 单一职责:每个测试用例只验证一个功能点
- 独立性:测试用例之间相互独立
- 可重复性:测试结果可重复
- 自动化:测试用例可自动执行
- 可维护性:测试代码易于维护
9.2 测试命名规范¶
// 测试方法命名:test[MethodName]_[Scenario]_[ExpectedResult]
@Test
public void testCreateDevice_ValidInput_ReturnsDevice() {
// 测试实现
}
@Test
public void testCreateDevice_NullName_ThrowsException() {
// 测试实现
}
@Test
public void testCreateDevice_DuplicateName_ThrowsException() {
// 测试实现
}
9.3 测试数据管理¶
// 测试数据构建器
public class DeviceTestDataBuilder {
private String name = "Default Device";
private String type = "Sensor";
private String description = "Test Device";
public DeviceTestDataBuilder withName(String name) {
this.name = name;
return this;
}
public DeviceTestDataBuilder withType(String type) {
this.type = type;
return this;
}
public DeviceTestDataBuilder withDescription(String description) {
this.description = description;
return this;
}
public Device build() {
return new Device(name, type, description);
}
}
// 使用示例
@Test
public void testCreateDevice() {
Device device = new DeviceTestDataBuilder()
.withName("Temperature Sensor")
.withType("Sensor")
.withDescription("Monitors temperature")
.build();
// 测试实现
}
10. 总结¶
10.1 白盒测试价值¶
- 代码质量保证:确保代码逻辑正确
- 缺陷早期发现:在开发阶段发现缺陷
- 代码覆盖率:验证测试的完整性
- 重构支持:为代码重构提供安全保障
10.2 关键成功因素¶
- 工具支持:使用合适的测试工具
- 团队协作:开发与测试团队密切配合
- 持续改进:不断优化测试策略
- 知识分享:团队内部知识共享
10.3 未来发展方向¶
- AI辅助测试:使用AI技术生成测试用例
- 可视化测试:测试结果可视化展示
- 云端测试:云端测试环境支持
- 实时监控:实时测试监控和告警