如何测试Kotlin中使用lateinit Id的服务函数?
错误原因
你遇到的kotlin.UninitializedPropertyAccessException是因为测试中构建的Segmentation对象的id属性是lateinit类型,未被初始化,但在createSegmentFromUserIds函数中,会调用querySegmentService.runUpdateQuery(..., segmentId = segmentation.id),尝试访问这个未初始化的属性,从而抛出异常。
另外,你的测试中同时mock了segmentationRepository和segmentMetaService,但实际上segmentMetaService.saveSegmentInfo的mock已经覆盖了内部的仓库调用,这部分mock属于冗余代码。
修复步骤
1. 初始化Segmentation的id属性
创建测试用的Segmentation实例时,直接为id赋值:
val segment = Segmentation( attribute = filter, name = "segment-name", type = SegmentType.STATIC, appId = "app-id" ).apply { id = "test-segment-id" // 手动初始化lateinit属性 }
2. 移除冗余的mock代码
由于你已经mock了segmentMetaService.saveSegmentInfo的返回值,不需要再mocksegmentationRepository.save,直接删除这行代码:
// 删掉这行:`when`(segmentationRepository.save(any())).thenReturn(segment)
3. 验证依赖方法的调用(推荐)
为了确保测试的完整性,添加对querySegmentService.runUpdateQuery的调用验证:
// 在assertEquals之后添加 verify(querySegmentService).runUpdateQuery(userIds = userIds, appId = "app-id", segmentId = "test-segment-id")
修改后的完整测试代码:
class SegmentServiceTest { private val segmentMetaService = mock(SegmentMetaService::class.java) private val querySegmentService = mock(QuerySegmentService::class.java) private val converterService = mock(ConverterService::class.java) private val userAttributeService = mock(UserAttributeService::class.java) private val segmentConsumerUserInfoProducer = mock(SegmentConsumerUsersInfoProducer::class.java) @Test fun `createSegmentFromUserIds should create a new segment`() { val segmentService = SegmentService( segmentMetaService = segmentMetaService, querySegmentService = querySegmentService, converterService = converterService, userAttributeService = userAttributeService, segmentConsumerUserInfoProducer = segmentConsumerUserInfoProducer ) val userIds = listOf("user-1", "user-2", "user-3") val filter = AndFilter( operations = listOf( AndFilter( operations = listOf( StringListContainsFilter( field = "userId", operationType = StringQueryOperationType.IN, values = userIds ) ) ) ) ) val createSegmentRequest = CreateSegmentRequest( name = "segment-name", attribute = filter, type = SegmentType.STATIC ) val segment = Segmentation( attribute = filter, name = "segment-name", type = SegmentType.STATIC, appId = "app-id" ).apply { id = "test-segment-id" } `when`(segmentMetaService.saveSegmentInfo(createSegmentRequest, "app-id")).thenReturn(segment) val createdSegment = segmentService.createSegmentFromUserIds(userIds = userIds, name = "segment-name", appId = "app-id") assertEquals(segment, createdSegment) verify(querySegmentService).runUpdateQuery(userIds, "app-id", "test-segment-id") } }
可测试性优化建议
1. 修改实体类的id属性类型
避免使用lateinit存储数据库主键,改为可空类型,这样即使未初始化也不会抛出异常,Spring Data MongoDB在保存时会自动填充该值:
@Document(Segmentation.COLLECTION_NAME) class Segmentation( // ... 其他属性 ) { @Id var id: String? = null // 替换lateinit为可空类型 // ... 其他属性 }
2. 简化测试依赖
使用Mockito的注解(如@Mock、@InjectMocks)来简化测试类的依赖初始化,减少手动创建mock的代码:
class SegmentServiceTest { @Mock private lateinit var segmentMetaService: SegmentMetaService @Mock private lateinit var querySegmentService: QuerySegmentService @Mock private lateinit var converterService: ConverterService @Mock private lateinit var userAttributeService: UserAttributeService @Mock private lateinit var segmentConsumerUserInfoProducer: SegmentConsumerUsersInfoProducer @InjectMocks private lateinit var segmentService: SegmentService @BeforeEach fun setUp() { MockitoAnnotations.openMocks(this) } // ... 测试方法 }
内容的提问来源于stack exchange,提问作者arianhf

